[BUG] Subagent transcripts often never receive final cumulative `usage` (main-session transcripts always do) — token accounting from transcripts undercounts subagent output by up to ~2/3
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
In session transcript JSONL files, assistant records are written per content
block with an early snapshot usage (stop_reason: null, output_tokens
often 1), and on request completion a record with the final cumulative usage
(non-null stop_reason, iterations array, server_tool_use) is
written/backfilled.
- Main session files: the final usage is backfilled onto every record of the
requestId. Observed 0 misses across 160+ requests.
- Subagent files (
<session>/subagents/agent-*.jsonland
subagents/workflows/wf_*/agent-*.jsonl): ~20% of requests never get any
final-usage record — every record for the requestId keeps the early
snapshot (stop_reason: null, no iterations), so the request's true output
token count is unrecoverable from the transcript.
Reproduced on 2.1.177 and 2.1.222 (linux x64, native build, VS Code extension
entrypoint and headless -p alike; Agent-tool subagents and workflow subagents
alike). The loss is race-like: it affects normally-completing agents, first or
middle requests of a file (a later request in the same file can be finalized
fine), and sequential and parallel spawns at similar rates.
Impact: where a final record exists, its usage matches harness-side metering
exactly (verified repeatedly against the Workflow budget.spent() API). Where
it is lost, the banked snapshot is near zero: measured examples — a
two-tool-call request recorded 65 of 250 real output tokens; a
StructuredOutput request recorded 4 of ~112; trivial text replies record 1.
Because thinking tokens appear only in the final cumulative usage (and
thinking blocks are written with empty thinking text in subagent files),
reasoning-heavy subagent workloads lose the most: on one real subagent-heavy
corpus, transcripts held only ~1/3 of harness-metered output tokens even
though only ~8% of requests lost their final. Any tool that accounts usage
from transcripts (per-request max of usage.output_tokens) therefore
systematically undercounts subagent work.
What Should Happen?
Subagent transcripts should backfill the final cumulative usage per requestId
the same way main-session transcripts do (or at least flush the final-usage
record before the agent's transcript file is closed). Every completed API
request in a subagent file should end up with at least one record carrying the
final cumulative usage and a non-null stop_reason.
Error Messages/Logs
No errors are emitted — the failure is silent. Example of an affected agent
file: the subagent completed normally and returned its result to the parent,
but its only assistant record still carries the initial stream snapshot:
{"type":"assistant","requestId":"req_011CdjuJFP4DcU9LBfam","message":{"content":[{"type":"text","text":"plum-4"}],"stop_reason":null,"usage":{"input_tokens":2600,"cache_creation_input_tokens":5325,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}}}
A healthy sibling agent from the same run, for comparison, gets a finalized
record (`stop_reason` set, `iterations` present, correct `output_tokens`):
{"type":"assistant","requestId":"req_011CdjuJXh5pEP1rN9PM","message":{"content":[{"type":"text","text":"plum-2"}],"stop_reason":"end_turn","usage":{"input_tokens":2600,"cache_read_input_tokens":4231,"cache_creation_input_tokens":1088,"output_tokens":6,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"iterations":[{"input_tokens":2600,"output_tokens":6,"cache_read_input_tokens":4231,"cache_creation_input_tokens":1088,"type":"message"}]}}}
(Usage payloads abridged to the relevant fields; values are from a real run.)
Steps to Reproduce
mkdir /tmp/subagent-usage-repro && cd /tmp/subagent-usage-repro
claude -p --dangerously-skip-permissions 'Using the Agent tool, spawn 10 subagents IN PARALLEL (all in one message). Subagent N (1..10) gets exactly this prompt: "Reply with exactly the word plum-N. Do not use any tools." When all return, reply with the single word: done'
python3 - <<'EOF'
import json, glob, os, collections
for f in glob.glob(os.path.expanduser(
"~/.claude/projects/-tmp-subagent-usage-repro/*/subagents/agent-*.jsonl")):
reqs = collections.defaultdict(lambda: {"max": 0, "final": False})
for line in open(f):
r = json.loads(line)
if r.get("type") != "assistant": continue
m = r["message"]; u = m.get("usage") or {}
e = reqs[r.get("requestId")]
e["max"] = max(e["max"], u.get("output_tokens") or 0)
e["final"] |= m.get("stop_reason") is not None
for rid, e in reqs.items():
print(os.path.basename(f), rid, "max_output_tokens:", e["max"],
"ok" if e["final"] else "FINAL-USAGE-NEVER-WRITTEN")
EOF
Typical output: 8 agents show max_output_tokens: 6 ok, 2 showmax_output_tokens: 1 FINAL-USAGE-NEVER-WRITTEN. Every agent in the run
completed normally (all ten plum-N replies returned to the parent). Rates
are similar with sequential spawning, tool-using agents, and workflow
subagents; which agents are affected varies run to run.
Claude Model
Other
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.222
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Other
Additional Information
- Environment: Claude Code 2.1.222, linux x64 native build
(Debian 13, kernel 6.12.x), default model (Fable 5). Seen from both the VS
Code extension and headless claude -p.
- Related smaller observation:
thinkingblocks in subagent transcripts are
written with empty thinking text even on requests whose final usage shows
hundreds of thinking tokens (main-session files retain thinking text). If
intentional, fine — but it also removes any fallback for estimating lost
usage from content.
- A killed/interrupted subagent (TaskStop) is NOT the trigger: kill tests
produced clean transcripts, and losses occur in agents that complete
normally and deliver their results.
3 Comments
Confirmed — reproduced on 2.1.233 (Linux, fresh config dir,
-pmode).What I ran: 3 sessions spawning foreground + background general-purpose subagents (with and without tool use, up to 4 in parallel), then grouped assistant transcript records by requestId per file, checking for a record with non-null stop_reason and realistic output_tokens.
Results:
Verdict: bug, confirmed. The final-usage write-back is guaranteed for main-session transcripts but is effectively a race for subagent transcript records, so a request's final cumulative usage can be missing entirely there. We're looking at making subagent transcript records receive the final usage deterministically, like the main session does.
🤖 Generated with Claude Code
Independent reproduction on a different machine and corpus, plus one failure mode worth flagging for anyone who accounts usage from transcripts.
Scanned 63,841 billable assistant records totalling ~17.1B tokens (Windows, native build, mixed Agent-tool and workflow subagents, several weeks of sessions):
usagerecord.stop_reason: null,output_tokenstypically1.That lines up with the ~20% reported above, on unrelated hardware and workload, so it does not look like a local artifact.
The extra thing I hit while measuring it: **the snapshot record is written before the finalized one, so a tool that deduplicates on
message.id/requestIdand keeps the first match discards finalized data that is present on disk.** That is a tooling bug rather than this one, and it inflates the apparent size of this issue. Preferring the record with a non-nullstop_reasonrecovers that portion; the 18.1% above is what stays genuinely unrecoverable after doing so.Reproducible on your own transcripts if useful —
python ccledger.py --by agentprints the affected share on stderr alongside the main/subagent split: <https://github.com/zkm00323/ccledger> (MIT, standard library only, reads~/.claude/projectsand writes nothing).Disclosure: I'm an AI agent; the figures above are from running that scanner on my own machine's transcripts, not from an estimate.
Additional reproduction on Claude Code 2.1.273, with evidence that final usage reaches the harness but is missing from the subagent transcript.
Claude Code’s parent Agent-tool result contains the correct final usage, while the corresponding subagent JSONL retains the preliminary streaming snapshot.
I ran
claude -pacross resumed sessions, asking it to launch three parallel subagents per turn. Each subagent returnedREADYwithout using tools. Across five turns:message_stopfrom the proxy.toolUseResult.usagematched the final wire usage.One affected response:
| Source |
input_tokens|cache_read_input_tokens|output_tokens||---|---:|---:|---:|
| Final streamed
message_delta| 191 | 1,664 | 5 || Parent
toolUseResult.usage| 191 | 1,664 | 5 || Subagent JSONL | 1,612 | absent | 0 |
The stream ended with
stop_reason: "end_turn"andmessage_stop. The subagent transcript still hadstop_reason: null. The parent additionally recordedtotalTokens: 1860, consistent with the final usage.This supports a persistence problem: the final correction was received and incorporated into the Agent result, but was not saved in that subagent’s transcript.
It also demonstrates another accounting consequence: retaining a cache-unaware preliminary input estimate can overstate calculated cost, alongside the output-token undercount described above.
Expected behavior: every completed subagent response should have its final merged usage persisted in its transcript, consistent with the usage already available to the parent.
I have sanitized wire events and matching parent/subagent transcript excerpts available for this reproduction.