# Codex Codex is OpenAI's coding-agent CLI (`codex`), an open-source Rust program at [github.com/openai/codex](https://github.com/openai/codex). It persists each session as a "rollout": a JSONL file the CLI both appends to live and replays on resume. This document is derived from the upstream serialization code (open-source code as spec), cross-checked against real local sessions and against txcript's parser in `src/harness/codex.rs`, which is the authoritative mapping into txcript's Common model. ``` ~/.codex/sessions/ └── 2026/08/10/ dated YYYY/MM/DD tree └── rollout-2026-08-10T13-02-51-.jsonl one file per session │ ├── {"timestamp","type":"session_meta","payload":{…}} header ├── {"timestamp","type":"turn_context","payload":{…}} turn boundary ├── {"timestamp","type":"response_item","payload":{…}} protocol log (what the model sees) ├── {"timestamp","type":"event_msg","payload":{…}} display log (what the TUI replays) └── … two logs, interleaved ``` ## On disk Rollouts live under `~/.codex/sessions`, or `$CODEX_HOME/sessions` when `CODEX_HOME` is set (Codex honors the override before its home lookup, and txcript mirrors that in `CodexStore::default_root`). Files are sharded into dated subdirectories `YYYY/MM/DD` by session start time, one file per session, named `rollout--.jsonl` (colons in the timestamp become hyphens for filesystem safety). txcript's discovery walks the tree recursively for `rollout-*.jsonl` files (symlinked directories are not followed, guarding against cycles; symlinked files still list). A file only counts as a session if it contains a `session_meta` line carrying an `id`; discovery parses just those lines and skips message payloads entirely. On load, a missing id falls back to the filename's uuid. ## Dissection of a transcript Every line shares one envelope — upstream's `RolloutLine`: a `timestamp` (RFC 3339, millisecond precision, UTC) plus a `RolloutItem` flattened as `type` + `payload` (some versions add an `ordinal`). Two logs interleave in one file: `response_item` is the protocol log (the exact items the model exchanges) and `event_msg` is the display log (what the TUI renders). Most content exists in both; txcript reads primarily from `response_item` and uses `event_msg` for what only it carries. | Their name | What it is | Maps to | |---|---|---| | `session_meta` | Header: `id`, `timestamp`, `cwd`, `cli_version`, `git.branch`, `model_provider`, instructions | `Meta` (id, timestamp, cwd, git_branch, cli_version, model) | | `turn_context` | Turn boundary: `turn_id`, `model`, cwd, sandbox/approval policy | Model attribution for the turn's assistant messages | | `response_item` / `message` | User or assistant message; `content` array of `input_text` / `output_text` / `input_image` (data URL) | `Message` with `Text` / `Image` blocks | | `response_item` / `reasoning` | Reasoning item: `summary` array of `summary_text`, opaque `encrypted_content` | Assistant `Thinking` block (summary text only) | | `response_item` / `function_call` | Tool call; `arguments` is JSON-in-a-string, paired by `call_id` | `ToolUse`; `exec_command` / `shell` normalize to `Bash` | | `response_item` / `function_call_output` | Tool result mirror | `ToolResult` — kept only if no canonical result exists (see below) | | `response_item` / `custom_tool_call` (+ `_output`) | Freeform-input tool, notably `apply_patch` | `Edit` (single-hunk update), `Write` (single add), else `ApplyPatch` raw | | `response_item` / `web_search_call` | Server-side search; `action` object, `call_id` often absent | `WebSearch` `ToolUse` | | `event_msg` / `exec_command_end` | Canonical shell result: `aggregated_output`, `exit_code` | `ToolResult` (`is_error` from nonzero exit) | | `event_msg` / `web_search_end` | Search result, carries the `call_id` the call may lack | `ToolResult`, plus call-id pairing | | `event_msg` / `token_count` | Usage snapshot: `info.last_token_usage` | `Usage` on the turn's last assistant text | | `event_msg` / `task_started` / `task_complete` | Turn lifecycle | Triggers model/usage backfill onto that turn | | everything else (`compacted`, `world_state`, `agent_message`, `user_message`, …) | Mirrors or non-conversational state | Skipped in Common; preserved verbatim at the native level | Translation is a single stateful pass, not a per-line map. `turn_context` / `task_started` set the current turn; assistant messages inherit that turn's model, and `token_count` + `task_complete` backfill usage onto the turn's last assistant text. Tool calls pair with results by `call_id` — except web search, where the call often has no `call_id`: txcript matches the `web_search_end` event by the serialized `action` object and adopts its id (falling back to a synthetic `web_search:N`). Because shell and custom-tool results appear twice — a rich `event_msg` and a plain `function_call_output` mirror — the mirror is dropped whenever a canonical result with the same `call_id` exists. User messages that are pure scaffolding (``, ``, etc.) are dropped; `system` / `developer` roles are not turns. A synthetic `response_item` line, shaped like the real thing: ```json {"timestamp":"2026-08-10T20:05:52.101Z","type":"response_item","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":[\"bash\",\"-lc\",\"cargo test\"]}","call_id":"call_abc123"}} ``` That becomes an assistant `ToolUse` with tool `Bash { command: "cargo test" }` — argv arrays of the form `["bash"|"sh"|"zsh", "-lc"|"-c", cmd]` collapse to the inner command. ## Caveats - **Lossiness in Common.** `encrypted_content` on reasoning items, sandbox/approval context, rate limits, and all display-only events don't survive `to_common`. The native `Vec` representation keeps every payload as raw JSON, so native ↔ disk round-trips are lossless. - **`apply_patch` is best-effort.** Only a lone single-hunk update maps to `Edit` and a lone file add to `Write`; multi-file, multi-hunk, delete, and move patches stay as a raw `ApplyPatch` with the touched paths listed. - **Resume is picky.** `from_common` must emit `model_provider: "openai"` in `session_meta` — current Codex resolves a null provider to the empty name and fails resume with ``Model provider `` not found``. `base_instructions` may be null (defaults substitute). Foreign tool names are normalized to OpenAI's `[A-Za-z0-9_-]+` requirement when written: unsupported characters become `_`, and an empty name becomes `tool`. This destination-only normalization is not reversible, and distinct foreign names can converge (for example, `a.b` and `a/b`). - **Version drift.** Newer rollouts add fields (`ordinal`, `session_id`, `parent_thread_id`, structured `source`) and kinds (`world_state`, `compacted`, `inter_agent_communication`). Unknown envelope fields land in a flattened `extra` map and unknown kinds are carried verbatim, so drift degrades to skipped-in-Common rather than parse failure. - **Hostile input.** Session ids are validated as path components before save-path construction; malformed lines are skipped by the JSONL parser rather than aborting the file; a `session_meta` without an id disqualifies a file from discovery instead of producing a broken session. - **Duplicate results are by design.** Seeing both `exec_command_end` and a matching `function_call_output` in a file is normal; only one becomes a `ToolResult`. ## References - Upstream envelope (`RolloutLine`, `RolloutItem` with `tag = "type", content = "payload"`): (accessed 2026-08-10) - Upstream writer (dated-tree layout, `rollout--.jsonl`, millisecond UTC timestamps): (accessed 2026-08-10) - Payload item types (`ResponseItem`, `EventMsg`, `SessionMeta`, `TurnContext`): (accessed 2026-08-10) - Authoritative mapping in this repo: `src/harness/codex.rs` (codec, store, tool normalization), with shape fixtures and aggregation assertions in `tests/integration/codex.rs`. Last verified: 2026-08-10, against src/harness/codex.rs and real local sessions.