ADR-035 — Token usage is measured from the local session transcripts
Status: accepted (2026-10-25, spec 099)
Context
Section titled “Context”The run records of spec 087 (ADR-032) say how long a run took, not what it consumed. The cost-reduction work after
spec 099 (#141, #142, #144) needs a baseline. The user is on a subscription, so there is no per-run invoice. The
only per-call source is the transcript Claude Code writes on the local machine:
~/.claude/projects/<cwd-slug>/<session>.jsonl, with each subagent in
<session>/subagents/agent-<id>.jsonl and its agent-<id>.meta.json holding agentType. Each assistant line holds
message.id, message.model and message.usage. A streamed message repeats over several lines with the same
message.id. One main session can drive several runs at once (/app:ship, a fleet), and a run does not know which
session it is part of unless it is told.
Decision
Section titled “Decision”- Source. Usage comes only from the local transcripts, read at run end by
cplat(moduleruns_usage). Nothing else is called, and nothing from the transcripts is kept except counts, model ids, agent types, phase names, run and session ids and timestamps. Prompts, tool input and output, file contents and subagent descriptions are read past and never kept. - Which session. A run started inside a Claude Code session claims that session through
$CLAUDE_CODE_SESSION_ID(set in the Bash tool environment since Claude Code 2.1.132). Runs that launch their own sessions (headlesswithclaude -p --session-id <uuid>,benchmarkwith its own clone directory) own them. The claims are kept in a local ledger next to the active records (<state dir>/usage/sources.json), never in the store. - Shared sessions. A subagent counts in exactly one run: the claiming run whose window holds its first call,
picked by its working directory, then by the latest-started run. A main-session call that falls inside the windows
of
nclaiming runs counts1/nin each, under the rolemain (shared). Every call of a session is counted once across all records. - Cost. Costs are estimates from a price table kept in the platform repo (
scripts/cplat/prices.json: USD per million tokens for input, cache write, cache read and output, plus the date the prices were taken). An unpriced model costsunknown, never 0. The table is updated by hand. - Never in the way. Collection has a 10 s budget, runs outside the record lock, and never changes a run’s
outcome, exit code or output. What it could not read is stated in
usage.incomplete.
Consequences
Section titled “Consequences”- Every run record of a run started in a Claude Code session gains a
usagesection. Runs started outside one (a plain terminal) carry none and are counted as “no usage data” in the report. - Usage measures only this machine: a run whose sessions ran elsewhere has no usage.
- Output tokens are taken as the transcripts report them. The transcripts under-report them, so output cost is a lower bound and is labelled that way.
- A change in the transcript layout breaks the measurement, not the run. It shows up as
incompletereasons. - New models need a row in the price table. Until then their cost is
unknownand the totals saycost_complete: false.
Not included
Section titled “Not included”- Budgets and their enforcement (#144), and any change that lowers token use.
- Reconciling the estimates with an invoice.