Compiled 2026-06-11 from the openai/codex source (codex-rs/protocol),
public parsers of the format (ccusage, tokscale, codex-trace,
codex-wrapped, agent-sessions), and their documented gotchas. As with the
Claude Code notes: defensive parsing only, nothing here is a contract.
$CODEX_HOME (default ~/.codex)/
sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl <- one per session
archived_sessions/YYYY/MM/DD/rollout-*.jsonl <- archived copies
history.jsonl <- prompt history only
A file can exist in both sessions/ and archived_sessions/; count it
once (we key on the path relative to each base, active copy wins).
Every line: {"timestamp": ISO8601, "type": T, "payload": {...}} with T:
| type | what it is |
|---|---|
session_meta |
first line: id, cwd, cli_version, git, and forked_from_id / parent_thread_id when forked |
turn_context |
per-turn settings; canonical source of model and cwd |
response_item |
a Responses-API item (see below) |
event_msg |
UI events; only token_count and turn_aborted matter to us |
compacted |
history compaction |
response_item payload types:
message:roleuser/assistant/developer,contentlist ofinput_text/output_textblocks. User messages whose text starts with<(<user_instructions>,<environment_context>,<system-reminder>) are injected by the harness, not typed.function_call:name(shell,apply_patch, MCP tools...),arguments(a JSON string),call_id.local_shell_callcarriesaction.commandinstead. apply_patch arguments hold the patch text underinput; file paths come from*** Add|Update|Delete File:lines.function_call_output/custom_tool_call_output:call_idplusoutput, which is a plain string or a JSON string like{"output": "...", "metadata": {"exit_code": 1, ...}}. Calls and outputs are flat and joined bycall_id, often lines apart.
event_msg user_message/agent_message duplicate the response_items;
ignore them or you double-count prompts.
event_msg / token_count payload:
{"type":"token_count","info":{
"last_token_usage":{"input_tokens":1000,"cached_input_tokens":250,
"output_tokens":125,"reasoning_output_tokens":75,"total_tokens":1200},
"total_token_usage":{...same shape, cumulative...},
"model_context_window":272000}}- Prefer
last_token_usage(per-turn). Older files only have the cumulativetotal_token_usage: credit the field-wise delta against the previous snapshot. cached_input_tokensis a subset ofinput_tokens; billinput - cachedat the input rate andcachedat the cached rate. Very old entries spell itcache_read_input_tokens.reasoning_output_tokensis already included inoutput_tokens.infocan benull; skip those lines.- Cumulative counters can regress (stale snapshot, compaction, fork reset); on regression reseed the baseline and credit nothing.
- No token telemetry at all before 2025-09-06; sessions parse, cost is 0.
- Early-Sept-2025 files have
token_countbut noturn_context: no model name. We fall back togpt-5for pricing (and theinfo.modelfield on newer token_counts when present). - Oldest format (~2025-08): line one is a bare meta object
(
{"id","timestamp","cwd",...}), the rest are bare response items with no envelope and no timestamps. - Forked sessions (
forked_from_idset) replay the parent's token_count rows at the top of the file; their timestamps predate the fork'ssession_meta, which is how we skip them (baseline only). turn_abortedis the user pressing Esc: we count it as an interrupt.