[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

Status Open
Reported on v2.1.177
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Aug 5, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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-*.jsonl and

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 show
max_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: thinking blocks 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.

View original on GitHub ↗

3 Comments

bcherny collaborator · 1 month ago

Confirmed — reproduced on 2.1.233 (Linux, fresh config dir, -p mode).

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:

  • Main transcript: 12/12 requests had the final cumulative usage backfilled onto every record (0 snapshot-only leftovers).
  • Subagent transcripts: 18 of 24 requests kept early per-block records with placeholder usage (output_tokens ≤ 6), and under 4 parallel background subagents 1 of 24 requests never received the final usage on any record — the accounting-breaking case you describe. The full-miss rate is timing/load-dependent, consistent with your higher fractions on heavier workloads.

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

zkm00323 · 1 month ago

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):

  • main-session files: 0 requests left without a finalized usage record.
  • subagent files: 5,484 of 30,288 requests (18.1%) carry only the mid-stream snapshot — stop_reason: null, output_tokens typically 1.

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/requestId and 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-null stop_reason recovers 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 agent prints the affected share on stderr alongside the main/subagent split: <https://github.com/zkm00323/ccledger> (MIT, standard library only, reads ~/.claude/projects and 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.

MoridinBG · 28 days ago

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 -p across resumed sessions, asking it to launch three parallel subagents per turn. Each subagent returned READY without using tools. Across five turns:

  • 2 of 15 subagent responses retained preliminary usage in their transcripts.
  • All 15 received final usage followed by message_stop from the proxy.
  • All 11 parent responses had finalized usage matching the captured stream.
  • Both affected subagents completed successfully, and their parent toolUseResult.usage matched 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" and message_stop. The subagent transcript still had stop_reason: null. The parent additionally recorded totalTokens: 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.