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. SometimesreturnCodeInterpretation,gitOperation(commit sha, push branch),timedOutAfterMs,backgroundTaskId,persistedOutputPath,dangerouslyDisableSandbox. - Edits carry
filePath,oldString,newString,structuredPatchwith hunk line counts, anduserModified. Line churn is derivable without shipping the diff. typeis present fortext,create,update,image,file_unchanged.durationMsexists 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:
- Tool duration =
tool_resultrecord timestamp minus thetool_useblock'sassistantrecord timestamp. Match ontool_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. - Response duration =
assistantrecord 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.
| 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.