bough
What is in a Codex rollout file All documents

What is actually in a Codex CLI rollout file

OpenAI Codex CLI records every session as a JSONL file under ~/.codex/sessions and, like Claude Code, does not document what goes in it. These are notes from reading my own and from fixing the places where bough had it wrong.

The corpus behind them is small: 3 rollout files, 242 lines, 7.4 MB, from Codex Desktop 0.153.4. So treat the shapes as reliable and the counts as illustrative. Every shape below was traced to code that was getting it wrong, which is a better guarantee than volume.

If you have read the same notes for Claude Code, the headline is that both formats replay records they already wrote, and the mistakes each one invites are different.

Where the files live

~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl

Dated directories rather than one per project. The working directory is inside the file, on session_meta.cwd, so grouping by project means reading the first line of every rollout rather than listing a directory.

The replay spans files, not lines

Claude Code replays inside one transcript. Codex writes an entirely new rollout when a session resumes and replays the earlier items into it, under the ids they already had.

This matters because the obvious defence does not work. Deduplicate within a file and you catch none of it: the first copy of the repeated item is in a different file. Group rollouts by session first, then deduplicate, or a resumed session comes back as several sessions with its early prompts counted once per resume.

Cached tokens are inside the input count, not beside it

A usage record reads like four independent figures. It is not:

{"input_tokens":28739,"cached_input_tokens":28032,"output_tokens":11,"total_tokens":28750}

Look at total_tokens. It is input_tokens plus output_tokens, and nothing else. That is what tells you the 28,032 cached tokens are already part of the 28,739, rather than something to add on top.

Add the two together and you have counted the cache twice. On one real project that turned 1.59M tokens into 3.04M, which is not a discrepancy anybody would notice by eye. The fresh input is input_tokens - cached_input_tokens, which here is 707 out of 28,739: on a long conversation almost everything is cache.

Claude Code reports these figures already separated. A parser reading both agents cannot use one rule for the pair, which is exactly the sort of thing that looks like a detail and is not.

Usage is also reported twice

Newer rollouts carry both a token_usage_record line per response and an event_msg of type token_count saying the same thing. Read both and every figure doubles again, on top of the cache problem.

Prefer the record. It carries a response_id, which is what lets a replayed response be recognised as one already counted, and the event carries no id at all. Older rollouts have only the event, so it still has to be read as a fallback.

Pair a tool call to its result by call_id

Codex issues tool calls in parallel and the results come back interleaved, so the next output is usually not the answer to the last call.

Both the call and its output carry the same call_id, and that is the only thing joining them. Match on arrival order instead and an unrelated command's exit code decides whether a git commit counted. Worth saying plainly because the naive version works fine on a session where nothing overlaps, which is most of the small ones you will test against.

Sub-agents are where the ids get strange

Spawning an agent writes a rollout whose session_meta.session_id is the parent's, while its own id differs and parent_thread_id names the parent.

Group on session_id alone and the sub-agent's prompts and tokens vanish into the agent that spawned it. Group on id alone and a resumed session splits into several. Both fields are needed, and parent_thread_id is what tells the two cases apart.

A sub-agent's rollout also has no user message in it at all. Nobody typed anything: the work arrives as an agent_message from whoever spawned it. Count only user messages as prompts and the whole session comes back empty, and an empty session usually gets dropped, so the work disappears rather than being merely mislabelled.

And agents message each other in both directions. A reply from a sub-agent is also an agent_message, so treating every one as new work gives the parent an extra turn each time a sub-agent reports back. Only NEW_TASK opens work, and the envelope says which is which.

What one agent asks another is encrypted

The brief in a spawn_agent call and the payload of an agent_message are Fernet tokens: base64url, beginning gAAAAA, a few hundred characters with no spaces. There is nothing in there to read.

What matters is not printing one as though it were something a person wrote. That is how a wall of ciphertext ended up as a label on a diagram here. The envelope around the payload is plain text and carries a Task name: line, which is the honest thing to show instead.

One more trap in the same area: a single user record can hold several injected blocks. The desktop app sends a plugin catalogue, the environment and the permissions as separate input_text chunks of one user message. Test only how the joined text begins and whichever block arrived first decides the answer, so a record that is entirely machine-generated reads as a prompt and every session gains one nobody typed.

The fuller version

These notes are kept next to the code that depends on them, at the Codex rollout format, along with the record types and field names. The Claude Code format is written up the same way.