---
title: Transcript formats
description: The Claude Code and Codex on-disk JSONL formats, field by field
---

# Transcript formats

Every measurement here was induced from live transcripts on camden, not from vendor documentation.
Counts are from bounded random samples and are there to show relative frequency, not as inventory.
Re-derive them with `ca2profile`; do not trust a number in this file that contradicts a live scan.

The two formats are structurally different and the decoders share nothing but the output model.

| | Claude Code | Codex |
| --- | --- | --- |
| Shape | one record per message, append-only | event-sourced lifecycle stream |
| Turn identity | `sessionId` + `parentUuid` chain + `promptId` | explicit `turn_id`, `thread_id`, `root_turn_id` |
| Item timing | absent; derive from record timestamps | native `started_at_ms` / `completed_at_ms` |
| TTFT | **absent** | native `time_to_first_token_ms` |
| Cost | native USD in `cost-state` | **absent** |
| Rate limits | absent | native, on every `token_count` event |
| Sub-agents | same file, `isSidechain` + `agentId` | separate file, `session_meta` spawn block |
| Bytes per session | low | roughly 6x, dominated by encrypted reasoning and duplicated tool output |

## Claude Code

### Record types

`assistant` and `user` carry the conversation. Everything else is harness state.

```
assistant  user  attachment  last-prompt  ai-title  mode  permission-mode  atis-latch
bridge-session  queue-operation  started  result  system  file-history-snapshot
file-history-delta  cost-state  agent-setting  pr-link
```

`system` subtypes: `turn_duration`, `stop_hook_summary`, `compact_boundary`, `away_summary`,
`local_command`, `model_fallback`, `api_error`, `agents_killed`, `scheduled_task_fire`,
`informational`, `bridge_status`.

`attachment` has roughly 50 subtypes and is effectively a harness event stream:
`total_tokens_reminder`, `output_style`, `hook_success`, `task_reminder`, `skill_listing`,
`deferred_tools_delta`, `environment`, `queued_command`, `prompt_snapshot`, `structured_output`,
`auto_mode`, `model`, `session_context`, `nested_memory`, `compact_file_reference`,
`max_turns_reached` among others.

### Per-record context

Present on `assistant` and `user`: `uuid`, `parentUuid`, `sessionId`, `timestamp`, `cwd`,
`gitBranch`, `version`, `entrypoint` (`cli` | `sdk-py`), `userType`, `isSidechain`, `agentId`,
`effort`, `slug`, `requestId`.

Attribution fields on `assistant` name what drove the response: `attributionAgent`,
`attributionSkill`, `attributionPlugin`, `attributionMcpServer`, `attributionMcpTool`.

`user` adds `promptId`, `permissionMode`, `promptSource`, `toolUseResult`, `toolDenialKind`,
`sourceToolAssistantUUID`, `isCompactSummary`, `isMeta`.

### `message.usage`

```
input_tokens  output_tokens  cache_read_input_tokens  cache_creation_input_tokens
cache_creation{ephemeral_1h_input_tokens, ephemeral_5m_input_tokens}
output_tokens_details{thinking_tokens}
server_tool_use{web_search_requests, web_fetch_requests}
service_tier  speed  inference_geo
iterations[]   <- per-iteration token breakdown inside one response
```

`cache_creation` splitting 1h from 5m ephemeral, and `iterations[]`, are finer than anything the
native exporter emits.

### Content blocks

`assistant` blocks are `tool_use`, `thinking`, `text`. `user` blocks are `tool_result`, `text`,
`image`. `stop_reason` is `tool_use`, `end_turn` or `stop_sequence`.

### `toolUseResult`

Shape varies by tool and the discriminator is not always a `type` field.

- Bash always has `stdout`, `stderr`, `interrupted`, `isImage`, `noOutputExpected`. Sometimes
  `returnCodeInterpretation`, `gitOperation` (commit sha, push branch), `timedOutAfterMs`,
  `backgroundTaskId`, `persistedOutputPath`, `dangerouslyDisableSandbox`.
- Edits carry `filePath`, `oldString`, `newString`, `structuredPatch` with hunk line counts, and
  `userModified`. Line churn is derivable without shipping the diff.
- `type` is present for `text`, `create`, `update`, `image`, `file_unchanged`.
- `durationMs` exists on a minority of results only. **Do not build tool latency on it.**

### Durations are derived

There is no model-side duration anywhere. Two derivations, both sound, both needing a provenance
label:

1. **Tool duration** = `tool_result` record timestamp minus the `tool_use` block's `assistant`
   record timestamp. Match on `tool_use_id`. This produces sane distributions (observed Bash p50
   1.8s / p95 18.7s, Grep p50 0.03s, Read p50 0.2s) and it is the highest-value derived signal.
2. **Response duration** = `assistant` record timestamp minus the previous record's timestamp
   (observed p50 2.7s, p90 13.8s, p99 52.7s). It conflates queueing, generation and harness
   overhead, so label it as derived and never call it TTFT.

`system:turn_duration` gives `durationMs` and `messageCount` for a whole turn, natively.

### `cost-state`

```json
{"totalCostUSD":101.98,"totalAPIDuration":8015680,"totalAPIDurationWithoutRetries":8014822,
 "totalToolDuration":2697581,"totalLinesAdded":5214,"totalLinesRemoved":389,
 "totalDuration":5099082,"startTime":1789236087240,"hasUnknownModelCost":false,
 "modelUsage":{"<model>":{"inputTokens":...,"outputTokens":...,"thinkingTokens":...,
   "cacheReadInputTokens":...,"cacheCreationInputTokens":...,"webSearchRequests":...,
   "costUSD":...}}}
```

**It is present in only a minority of sessions** (observed roughly a quarter, and it looks
version-gated). It is cumulative for the session, so it must be emitted as a gauge or a
last-value-wins observation, never summed across records. `hasUnknownModelCost` means the total
undercounts; carry it.

Model keys can carry a context-window suffix such as `claude-opus-5[1m]`. Do not assume the key
equals `message.model`.

### `system:compact_boundary`

`compactMetadata` gives `trigger`, `preTokens`, `postTokens`, `cumulativeDroppedTokens`,
`durationMs`, `preCompactDiscoveredTools[]` and the preserved-segment uuids.

### Errors

`isApiErrorMessage` plus `apiErrorStatus` on `assistant` (429 and 529 observed), and `is_error` on a
`tool_result` block. `system:api_error` and `system:model_fallback` exist as their own records.

## Codex

### Record types

Top-level `type` is one of `response_item`, `event_msg`, `token_usage_record`, `turn_context`,
`session_meta`, `world_state`, `compacted`, `inter_agent_communication_metadata`. The first two
carry a nested `payload.type`, and the pair is the real discriminator.

```
event_msg:      item_completed  token_count  task_started  task_complete  turn_aborted
                sub_agent_activity  agent_message  agent_reasoning  patch_apply_end
                mcp_tool_call_end  web_search_end  user_message  context_compacted
                thread_settings_applied  thread_goal_updated  image_generation_end
response_item:  reasoning  message  function_call  function_call_output  custom_tool_call
                custom_tool_call_output  agent_message  tool_search_call  tool_search_output
```

### `event_msg:task_complete` - the highest-value record in either format

```json
{"turn_id":"...","started_at":...,"completed_at":...,
 "duration_ms":630322,"time_to_first_token_ms":5131}
```

Native TTFT and native turn duration. Observed TTFT p50 6.5s / p95 17.8s; turn duration p50 310s /
p95 5018s. `event_msg:turn_aborted` is the same shape plus `reason`, and is the interrupted case.

### `event_msg:task_started`

Carries `turn_id`, `started_at`, `model_context_window`, `collaboration_mode_kind`, and a
**`trace_id` that is already 32 hex characters**. Use it as the OTel trace id directly rather than
hashing one.

### `event_msg:item_completed`

One per item, with `thread_id`, `turn_id`, `started_at_ms`, `completed_at_ms` and the item body.
This is a span with real boundaries. Item types and observed durations:

```
Reasoning            p50 1335ms   p95 10531ms
CommandExecution     p50    1ms   p95 22653ms   max 2052709ms
AgentMessage         p50 1315ms   p95  3855ms
FileChange           p50    1ms
SubAgentActivity     p50    0ms
CollabAgentToolCall  p50 30001ms  p95 64001ms    (inter-agent wait, not work)
McpToolCall          p50 1768ms
ContextCompaction    p50 62043ms  p95 167473ms
UserMessage  Extension  ImageView
```

`CommandExecution` carries `command`, `parsed_cmd`, `cwd`, `process_id`, `exit_code`, `status`,
`source`, `duration`, `stdout`, `stderr`, `aggregated_output`. Exit codes for every shell command
are here and nowhere else.

`McpToolCall` carries `server`, `tool`, `arguments`, `readOnlyHint`, `duration`, `result`.
`FileChange` carries a `changes` map keyed by absolute path with add/update/delete and content.

### Duplication trap

`response_item:custom_tool_call` / `custom_tool_call_output` and `event_msg:item_completed` describe
the **same** tool call. So do `response_item:agent_message` and `event_msg:agent_message`, and
`response_item:reasoning` and the `Reasoning` item. **Deduplicate on item id or every tool call is
counted twice.** Prefer `item_completed`: it is the only one with timing.

### `token_usage_record`

```json
{"thread_id":"...","turn_id":"...","session_id":"...","root_turn_id":"...",
 "response_id":"resp_...",
 "usage":{...}, "turn_token_usage":{...}, "thread_token_usage":{...}}
```

Three rollups, pre-computed: this response, this turn so far, this thread so far. The usage keys are
`input_tokens`, `cached_input_tokens`, `cache_write_input_tokens`, `output_tokens`,
`reasoning_output_tokens`, `total_tokens`.

`response_id` is the join key to codex-lb's wire capture. **Verified live**: three ids taken from a
current `codex-personal` session all resolved into the same-hour
`conversation-archive/<hour>.jsonl.gz`.

### `event_msg:token_count`

Carries `total_token_usage`, `last_token_usage`, `model_context_window`, and the full rate-limit
block:

```json
"rate_limits":{"limit_id":"codex","primary":{"used_percent":54.0,"window_minutes":10080,
 "resets_at":...},"secondary":null,
 "credits":{"has_credits":false,"unlimited":false,"balance":"0"},
 "plan_type":"prolite","rate_limit_reached_type":null,"spend_control_reached":null}
```

It fires very frequently. Sample it for metrics rather than emitting a record per event.

### `session_meta`

`session_id`, `cli_version`, `originator`, `model_provider`, `thread_source`, `forked_from_id`,
`parent_thread_id`, `history_mode`, `multi_agent_version`, `context_window.window_id`, and for a
spawned agent `source.subagent.thread_spawn{parent_thread_id, depth, agent_path, agent_nickname,
agent_role}`.

`model_provider` is **not stable**: both `codex-lb` and `openai` appear across sessions in the same
namespace. Never infer the provider for a namespace; read it per session.

### `turn_context`

`model`, `effort`, `cwd`, `workspace_roots`, `approval_policy`, `sandbox_policy`,
`permission_profile`, `personality`, `collaboration_mode`, `summary`, `timezone`, `current_date`.
This is where a turn's model and effort come from; the usage records do not carry them.

### `event_msg:sub_agent_activity` and `response_item:agent_message`

`sub_agent_activity` gives `agent_thread_id`, `agent_path`, `kind`, `occurred_at_ms`.
`response_item:agent_message` gives `author` and `recipient` as agent paths. Together they are the
full multi-agent message graph.

### `compacted`

`window_number`, `first_window_id`, `previous_window_id` and the replacement history. Context-window
lineage across a long session.

### Version drift is real

Record shapes vary by `cli_version`. Sessions exist in the same namespace with no
`token_usage_record` at all. A decoder that assumes a record type is present will silently produce
nothing rather than fail. **The drift probe is not optional here**; treat an absent record type as a
finding, not a default.

## Volumes

Sampled and extrapolated by bytes over a 30-day window. Use for sizing, re-measure before committing
to a budget.

| Namespace | Files/30d | Bytes/30d |
| --- | --- | --- |
| codex-personal | 3389 | 20.6 GB |
| codex-work | 857 | 4.6 GB |
| claude-personal | 1810 | 1.9 GB |
| codex-mobile | 201 | 1.8 GB |
| claude-work | 804 | 1.0 GB |

Roughly 4M records per month in total, Codex-dominated. A content-free log tier is on the order of
1.5 to 3 GB per month; shipping bodies is most of the 30 GB.
