Every multi-agent run I ship writes an accounting file: how many tokens the run burned, how long each spawn took, how much of that time waited on a human. For a long time all three came out marked confidence: unavailable. To get a real answer I combed the transcripts by hand after the run finished. Slow, error-prone, different every time.
So I built the instrumentation to capture them automatically. The first thing it taught me: the numbers were already wrong, and they had been wrong every time anyone reconstructed them by hand.
The numbers were wrong before anyone looked
Claude Code stores a transcript as JSONL, one line per content block, and every line of the same assistant message carries an identical cumulative usage object. Sum usage across lines, the obvious thing to do, and you count each message once per block it contains. The error is silent, and it scales with how much the model wrote per turn.
On one real transcript, 75 assistant lines collapsed to 33 distinct message IDs. Naive sum: 6,223,502 tokens. Deduplicated by message ID: 2,786,193. A ratio of 2.23. On a subagent transcript the numbers were 1,654,348 against 606,616, a ratio of 2.73. On this run's own transcript, 1,839,770 naive against 914,546 deduplicated, 2.01x.
Every ratio sits above 2x. The multiplier changes per transcript with message density; the direction never does. Naive summing always overcounts, always by a lot. An earlier reconstruction I'd done by hand, before any of this was automated, carried a confirmed 2.3 to 2.4x overcounting error for exactly this reason. The number people had been quoting was roughly double the real one, and nobody had noticed, because nothing about the wrong number looked wrong.
That is the shape of every failure in this piece. None of them announce themselves.
Why agents can't tell you
Deduplication fixes the count, but it doesn't say where the count comes from. The obvious answer is to ask the agents. Each subagent hands back a summary when it finishes, so have it report its own token count there. This cannot be made to work, for a structural reason.
The Conductor, the orchestrator that spawns the specialists, never sees a subagent's transcript. It sees the final handoff message and nothing else. The tokens a subagent burned live in a transcript file the Conductor has no access to, so any count it quotes is borrowed from something it never saw. Worse, that borrowed number lands next to numbers the agent can check, with no marker telling them apart: a file that mixes measured values and guesses and looks uniform.
I named this the anti-gate-theater partition and made it a rule: each metric is written only by the component that can actually observe it. If the Conductor can't see a subagent's tokens, the Conductor doesn't write them. Something that can see them does.
The observability partition
That rule splits the work in two.
The Conductor writes only what it directly observes. It stamps wall-clock timestamps on the lines where it starts and finishes a spawn. It sets a rework: true flag on a spawn's start line when that spawn is redoing work a previous spawn already attempted, because it is the only component that knows a redo from a fresh task. That's it. Timestamps and a flag.
Everything derived from transcript content, token and turn counts, is written by hooks. A hook fires at the harness layer, below the agents, and receives the finished transcript as a file path. It reads what the Conductor cannot.
When a specialist finishes, the harness fires a SubagentStop hook. The hook reads the subagent's own transcript, deduplicates by message ID, sums the usage, and appends one line to the run's log:
SPAWN-TOKEN-EVENT: {"attempt_id":"architect-1","agent_id":"aae17f...","at":"2026-07-05T00:01:00Z","turns":11,"tokens":{"input":131,"cache_creation":17839,"cache_read":66779,"processed":84749,"output":699}}
The attempt_id is the pairing key, tying this token record back to the spawn event the Conductor wrote when it launched the agent. If the hook can't find an Attempt ID in the prompt, it writes attempt_id: null with a note, and the record surfaces in the accounting file as unattributed instead of being dropped. A number with no home is visible. A number silently discarded is not.
One thing had to be true for this to work: the hook has to finish before the close-out pass reads the log. It does. SubagentStop runs synchronously. On a live probe, the hook's payload was on disk at T+0 and the parent's next action began at T+9 seconds. The token line is there before anyone looks for it.
This is the Bureau, my multi-agent framework, but the partition isn't specific to it. Any orchestration layer where one process spawns others it can't see into has the same problem and the same answer: the thing that can observe a number is the thing that writes it.
Five tests passed. The hook wrote nothing.
The hook I just described shipped, passed its whole test suite, and did nothing in production.
To find the run directory, the SubagentStop hook pulls the first user message out of the transcript, the spawn prompt, which carries the RUN_DIR: line it needs. The selector read .role == "user" to find it.
Real Claude Code subagent transcripts don't put role at the top level. A user line looks like this:
{"type":"user","message":{"role":"user","content":"..."}}
The top-level .role is null. The selector matched zero lines. The hook found no user message, so no RUN_DIR: string, so it exited 0 and wrote nothing. No error. No warning. A clean exit that produced no output.
Now the part that makes it a genuine trap. The hook had five test fixtures, and all five passed. They passed because every one had synthesized its user lines in the flat shape the hook expected, {"role":"user","content":"..."}, the same wrong schema the code assumed. Code and fixtures were wrong in the same direction, so the fixtures confirmed the bug instead of catching it. Green across the board, for a hook that did nothing.
There is only one way to catch this, and it isn't more fixtures written the same way. It's feeding the thing a real input. When someone finally ran a real subagent transcript through the hook and counted the SPAWN-TOKEN-EVENT lines, the count was zero. That was the whole diagnosis.
The fix and what it proves
The selector correction was small:
map(select(.type? == "user")) | .[0].message.content
The fixtures were regenerated from the real nested schema. But the fix that mattered wasn't either of those. It was an end-to-end proof fixture: feed the hook a real-schema transcript with known content, then assert that exactly one SPAWN-TOKEN-EVENT line lands in the log. It doesn't check the hook's internal logic. It checks the one thing you actually care about, that a real input produces a real output, which is the exact thing every unit fixture had failed to check.
The failure class generalizes past transcript parsing. Any time you build a fixture from the same document that guided the implementation, the fixture and the code can be wrong together and agree with each other forever. A schema you read once and encoded in both places is a single point of failure wearing two hats. The only fixture that breaks the tie is one whose input you didn't invent.
The deferred-exact problem
One number resisted honest capture longer than the rest: the Conductor's own tokens.
The Conductor's tokens come from a different hook, a Stop hook that fires when the main session finishes a turn. In an autonomous run the Conductor chains everything into one long turn and yields only at the very end. Close-out, where the accounting is computed, is its last action inside that turn, and the Stop hook fires after the turn ends. So when close-out reads the log to total everything up, the Conductor's own final-leg record does not exist yet. It's about to, but it isn't there.
The design refuses to lie about this gap. At close-out the run recorded processed_total 33,914,195 and labeled it partial, with only a mid-run final:false conductor leg present at 109 turns. The Conductor's share is never labeled exact while the final leg is missing. Then the turn ended, the Stop hook fired, appended the final:true line (now 121 turns, the complete session), and re-ran the accounting: the total rose to 38,612,740, and the Conductor's share within it, 34,176,808, flipped to exact. No human touched it.
Getting there took two tries. The Challenger, the review agent, caught that the first version of this fix was broken in a way the tests wouldn't show. The original design had the Conductor delete its pointer file at close-out as cleanup. But that pointer is how the post-close-out Stop fire finds the run. With the pointer already gone, the Stop hook looked for the run, found nothing, and the self-refresh it was supposed to trigger was dead code. The honest-degrade half worked; the reach-exact half could never run.
The fix moved ownership of the deletion. The Conductor no longer removes the pointer; the Stop hook does, on its first fire after closure, once it has appended final:true and refreshed the accounting. Every later fire finds no pointer and exits 0. The capture happens exactly once and nothing leaks past it.
What the first instrumented run showed
The run that built all of this was the first run measured by it. The accounting file it produced was written by the same code the run had just shipped. So the instrument measured its own construction, and the honest thing it reported was that it was incomplete.
processed_total: 38,612,740, confidence partial, with a note saying 27 specialist spawns have no matched token event. Of that total, the Conductor accounts for 34,176,808, exact, across 121 turns and one session, with 33,368,241 of it cache reads. That is 88.5% of everything captured. The remaining 11.5% is two late specialist spawns, 4,435,932 together, and they're unattributed: their prompts went out before the Attempt ID line was part of the spawn template. The feature that would have paired them hadn't shipped yet.
The other two headline numbers read zero, and both are honest zeros:
rework_ratio: 0, partial. Five spawns in this run were flagged as rework in the state data. All five ran before the hooks were live, so none of them produced a token event, so the numerator sums to zero. The rework was real. The measurement of it wasn't there yet.active_spawn_time_s: 0, partial. The wall-clock timestamps that feed this metric are part of what this round of work added to the Conductor's behavior. The planning-phase spawns predate them, so there was nothing to sum.
27 of 27 specialist spawns went unmatched. The hook that would have caught them wasn't registered until the last phase of the very run that built it, and the two records it did catch had no Attempt ID to bind them to a spawn.
Partial is the honest answer
Look at the failures together. The naive sum overcounted by more than 2x and looked fine. Self-reporting can't work and looks like it should. The token hook passed every test and did nothing, and the Conductor's tokens weren't there when the accounting ran. Every one of them is silent. Not one throws an error or trips a test written the ordinary way.
The fix was the same each time. Write only what you can directly observe. Test that observation end to end, with a real input, asserting the output you actually want. Everything else, the fixtures that agree with your assumptions, the numbers agents report about themselves, the totals that look clean, will lie to you quietly and pass while doing it.
Which is why the last thing worth defending is the word partial. rework_ratio: 0 on a run with five documented rework spawns. active_spawn_time_s: 0 on a run with 27 specialist dispatches. Read cold, those look like bugs. They're the opposite. They're the file telling you exactly what it couldn't see, instead of handing you a confident number that happens to be wrong. A partial with an honest note is worth more than an exact that isn't. If you build measurement code, that's the property to protect. Stop asking your agents what they cost. Build the thing that can see it, and let it tell you, in the same breath, what it still can't.