Document · doc-0001

Verified source facts

Facts established by reading live data on camden, not from vendor documentation. Each one cost real work to establish and several contradict what a reasonable person would assume. Re-verify before depending on one; do not re-derive one that is still true.

The codex-lb join exists and works

Three resp_... ids taken from a current codex-personal session all resolved into the same-hour /opt/codex-lb/data/codex-lb/conversation-archive/<hour>.jsonl.gz. The id in token_usage_record.response_id is the same id space codexlb2otel keys Turn.ResponseID on.

It is still optional, for three reasons that are all independent of each other:

Both clients already export their own OTel, to the same collector

This was the assumption most likely to produce a duplicated metric set.

This service exports to the same localhost:4318 and therefore needs no Grafana credential.

What native telemetry does not cover

The archive itself

49 GB, roughly 23,000 files, ten namespaces:

claude-personal 7571   codex-personal 6807   claude-work 6911   codex-work 1267
codex-mobile 373   claude-mobile 49   codex-security 11   codex-or 3
codex-standalone-gfmbp 153   codex-standalone-mbp16 128

agent-session-metrics.timer already publishes five-minute aggregates about the archive: file counts, byte coverage, index freshness, parser errors. Do not re-emit its series. This service owns the contents of the transcripts, not the state of the store.

/opt/agent-session-index/sessions.sqlite3 is a derived SQLite/FTS projection that deliberately excludes tool calls, tool results and reasoning. That is precisely the most valuable operational signal, which is why this service tails the JSONL rather than reading the index.

Version drift is real and silent

Record shapes vary by cli_version. Sessions exist in codex-personal with no token_usage_record at all, and current sessions on 0.155.1 have them on every response. A decoder that assumes a record type is present produces nothing rather than failing, which is indistinguishable from a quiet day. An absent record type is a finding.

Measured distributions

From bounded random samples. They are here so an implementer can tell a plausible result from a broken one, not as inventory.

Codex, native: TTFT p50 6.5s / p95 17.8s. Turn duration p50 310s / p95 5018s. Item durations: Reasoning p50 1.3s, CommandExecution p95 22.7s, ContextCompaction p50 62s / p95 167s, CollabAgentToolCall p50 30s (which is inter-agent wait, not work, and will distort tool latency if counted as a tool).

Claude Code, derived by pairing tool_use to tool_result on tool_use_id: Bash p50 1.8s / p95 18.7s, Read p50 0.2s, Edit p50 0.62s, Grep p50 0.03s, WebSearch p50 7.2s, AskUserQuestion p50 238s (human think time, which falls out for free). Assistant record inter-arrival p50 2.7s / p90 13.8s / p99 52.7s. That is not TTFT.

cost-state appeared in 62 of 250 sampled claude-personal files. Roughly a quarter of sessions, and it looks version-gated.

Volumes, 30 days

codex-personal  3389 files  20.6 GB
codex-work       857 files   4.6 GB
claude-personal 1810 files   1.9 GB
codex-mobile     201 files   1.8 GB
claude-work      804 files   1.0 GB

Roughly 4M records per month, 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.

Two traps worth stating plainly

find -newermt on the hot store selects by mtime, which reflects the archive sync rather than when the session happened. Files named 2026-08-24 come back as recently modified. Select by the filename date when you mean recency of the conversation.

event_msg:item_completed and the matching response_item:* describe the same item. Counting both doubles every tool call and nothing about the totals looks wrong.

View the source file on GitHub