Subagent transcripts record start-of-stream `output_tokens` and no `stop_reason` for most turns

Status Open
Reported on v2.1.281
Maintainer reply None cached
Activity 4 comments · opened Sep 28, 2026

What's wrong

In subagent transcripts (~/.claude/projects/<project>/<session>/subagents/agent-*.jsonl), most assistant turns are written before the response finishes streaming. Every line of the turn carries the message.usage from the start of the stream, so output_tokens stays at its initial value (1 to 59) even when the reply is thousands of tokens long. The final stop_reason and the final usage are never written for that turn.

Main-session transcripts are not affected.

Evidence

Claude Code 2.1.281 and 2.1.282 on Linux, 24 to 27 September 2026, on a Claude subscription:

  • 402 of 539 subagent turns (75%) have message.stop_reason: null on every line, no usage.output_tokens_details, and identical usage on every line. Their output_tokens range from 1 to 59.
  • The other 137 subagent turns have stop_reason set (end_turn or tool_use) on their final line, carry output_tokens_details, and their output_tokens grows on that line.
  • There are no exceptions to that split: a turn either has all three signs of a finished write or none of them.
  • Main sessions: 2 of 2,004 turns show the same pattern.
  • It affects every subagent type (built-in and custom), foreground and background alike.
  • One example: a reply of about 11,700 characters was recorded with output_tokens: 2.
  • No other file under ~/.claude carries the final count. For a background run, the parent's tool result reports isAsync with no totals. For a foreground run, its usage repeats only the subagent's last turn.
  • The OpenTelemetry claude_code.api_request event for the same request_id does carry the final output_tokens, so the correct figure exists at runtime; it just never reaches the transcript.

Why it matters

Anything that reads transcripts to measure usage or cost, including community usage tools and our own, under-counts subagent output. Output tokens are the most expensive class, so the under-count is large and silent.

Expected

Each subagent assistant turn's final transcript line carries the response's final usage and stop_reason, as main-session transcripts do.

Steps to reproduce

  1. Run a session that dispatches several subagents, some in the background.
  2. In the subagent JSONL files, group assistant lines by requestId.
  3. Count the turns whose lines all have stop_reason: null and identical usage. Compare their output_tokens against the length of their content.

View original on GitHub ↗

3 Comments

stephenc123 · 10 days ago

Same bug here on macOS with Amazon Bedrock as the provider, so it isn't specific to Linux or subscriptions. I can also narrow when it started: last good 2.1.277, first affected 2.1.278, and it's still present in 2.1.289.

Sub-agent responses where every line has stop_reason: null (one install, ~46k responses):

  • 2.1.259–2.1.277: 0.0–1.1% per version (2.1.277: 3 of 455)
  • 2.1.278: 223/261 (85.4%) · 2.1.280: 505/565 (89.4%) · 2.1.282: 675/881 (76.6%) · 2.1.284: 1,724/1,843 (93.5%) · 2.1.285: 569/621 (91.6%) · 2.1.287: 573/698 (82.1%) · 2.1.288: 3,614/3,985 (90.7%) · 2.1.289: 1,561/1,764 (88.5%)
  • Main-session responses: 6 of 27,481 across all versions

Two independent checks show the transcript is short, not the API response:

  • Provider side: I joined transcript requestIds to Bedrock's own per-request records (CloudTrail) for one 24 h window. Main-session requests matched exactly (1,217/1,217). Sub-agent requests: 1,018,953 output tokens provider-side against 23,407 in the transcripts.
  • Claude Code's own accounting: in an affected 2.1.284 session, the parent transcript's cost-state line reports 877,111 output tokens, while the transcripts sum to 302,119. An unaffected 2.1.271 session with sub-agents agrees within 0.2%.

Impact: for sub-agent-heavy work, summing transcript usage gave about 60% of the true input+output over 24 h.

andpich · 9 days ago

Additional data from a second Linux install (VS Code extension, x64, Claude subscription, heavy use of background subagents). This points to the regression starting at different versions on different installs.

Subagent requests where no line carries a non-null stop_reason (unique message.id, ~69k requests, 2026-08-06 → 2026-10-06):

| Version | Requests without final usage |
|---|---|
| 2.1.220 – 2.1.241 | 4 – 23 % (the older race from #84223) |
| 2.1.247 – 2.1.267 | 44 – 92 % |
| 2.1.269 – 2.1.274 | 0.0 – 0.7 % |
| 2.1.278 | 0.3 % (3 / 865) |
| 2.1.280 | 0.7 % (13 / 1,816) |
| 2.1.281 | 0.0 % (0 / 3,118) |
| 2.1.282 | 88.7 % (635 / 716) |
| 2.1.283 | 96.2 % (661 / 687) |
| 2.1.284 | 91.3 % (981 / 1,075) |
| 2.1.285 | 96.7 % (3,993 / 4,128) |
| 2.1.286 | 96.1 % (1,923 / 2,001) |
| 2.1.289 | 96.1 % (424 / 441) |

Main-session requests: 0 % in every version.

Two things differ from the report above:

  • On this install, 2.1.278 – 2.1.281 were clean and the jump came with 2.1.282. Above, 2.1.278 was already the first affected version.
  • On this install, 2.1.247 – 2.1.267 were also heavily affected (44 – 92 %). Above, 2.1.259 – 2.1.277 were at 0 – 1.1 %.

So the onset does not seem to be a single version boundary; maybe a staged rollout, a flag, or workload-dependent timing.

More observations:

  • Affected requests show 60 – 760 visible characters per recorded output token. Finalized requests with the same models show 1.0 – 1.6.
  • For background Agent calls, the parent toolUseResult carries no usage at all. Its keys are agentId, canReadOutputFile, description, isAsync, outputFile, prompt, resolvedModel and status. The parent therefore offers no fallback either.
  • The OTel workaround is confirmed here too. claude_code.api_request carries request_id, which equals the transcript requestId. Its four token fields matched finalized transcript records exactly on 2.1.281 (CLI) and 2.1.291 (extension binary), including subagent requests (query_source: agent:builtin:general-purpose).
  • Impact on this install, estimated from content length (likely a lower bound, because thinking tokens are not visible in subagent files): Claude Code output for September 2026 was about +47 % above what the transcripts record.

Related: #84223 (older, race-like variant at ~20 %).

fisiu123 · 2 days ago

Same issue on Windows 11, desktop app (entrypoint: claude-desktop), so it is not Linux/macOS-only. Data from one user's local transcripts, 2026-09-21 → 2026-10-11. Responses are deduplicated by message.id + requestId.

Responses with a closing row (non-null stop_reason) / all responses:

| Version | Main session | Subagents |
|---|---:|---:|
| 2.1.275 | 420/420 | 38/57 |
| 2.1.280 | 6542/6543 | 14708/14750 |
| 2.1.284 | 5507/5507 | 3435/7646 |
| 2.1.286 | 921/922 | 75/416 |
| 2.1.288 | 1552/1553 | 335/1634 |
| 2.1.289 | 1848/1848 | 714/2960 |
| 2.1.293 | 3563/3565 | 1223/3566 |
| 2.1.295 | 1144/1145 | 86/194 |

Findings:

  • In our data 2.1.280 was still ~99.7% complete for subagents. The drop starts in 2.1.284. Main sessions are unaffected in every version.
  • Input and cache fields are reliable even on the start-of-stream row. We found 15 840 responses that have both a start row and a closing row. In 15 838 of them, input_tokens, cache_creation_input_tokens and cache_read_input_tokens are identical on both rows. Only output differs: median 7 tokens on the start row vs 588 on the closing row.
  • Missing output_tokens_details.thinking_tokens in subagents matches the missing closing rows almost 1:1. Per week it is 50 / 4451 / 6108 responses without thinking tokens vs 50 / 4452 / 6113 without a closing row.

Practical effect: weekly subagent output looked like it fell from 13.8M to 3.1M tokens, while output per fully measured subagent response stayed flat (~1176 → 1286).

Showing cached comments. Read the full discussion on GitHub ↗