Skip to content

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 CodeCodex
Shapeone record per message, append-onlyevent-sourced lifecycle stream
Turn identitysessionId + parentUuid chain + promptIdexplicit turn_id, thread_id, root_turn_id
Item timingabsent; derive from record timestampsnative started_at_ms / completed_at_ms
TTFTabsentnative time_to_first_token_ms
Costnative USD in cost-stateabsent
Rate limitsabsentnative, on every token_count event
Sub-agentssame file, isSidechain + agentIdseparate file, session_meta spawn block
Bytes per sessionlowroughly 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

{"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

{"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

{"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:

"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.

NamespaceFiles/30dBytes/30d
codex-personal338920.6 GB
codex-work8574.6 GB
claude-personal18101.9 GB
codex-mobile2011.8 GB
claude-work8041.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.