SubagentStart/SubagentStop counts stay mismatched: SubagentStop not emitted for subagents that terminate during an API-error burst

Status Open
Reported on v2.1.212
Maintainer reply None cached
Activity 3 comments · opened Jul 17, 2026

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?

During a run that spawned many parallel subagents (agent_type=general-purpose, spawnDepth: 1, model haiku), a burst of API errors caused ~14 subagents to terminate early. When I reconcile the hooks by counting SubagentStart vs SubagentStop per session, the counts never balanced: 7 subagents produced a SubagentStart but never a matching SubagentStop, and the mismatch stayed stuck indefinitely — even though the docs state SubagentStop fires on API errors.

The key finding: the loss is not random. In the same session, the pre-burst wave lost 0 of 7 stops, while the burst wave lost 7 of 8 — a systematic, condition-dependent failure to emit SubagentStop, not stochastic transport loss.

How I can observe this externally
SubagentStart/SubagentStop are delivered as HTTP POST to a local listener (http://localhost:5080/api/hook/claude/{session_id}, 5 s timeout), so I can reconcile start/stop counts independently of any TUI.

Evidence (receiver logs ⨯ transcript reconciliation)

| | Count |
|---|---|
| SubagentStart received | 16 |
| SubagentStop received | 13 (9 matching a start + 4 orphan stops with no start) |
| Subagents with a start but no stop | 7 |
| "terminated early due to an API error" in transcript | 14 |

The loss is condition-dependent, not random:

| Wave | Time | Missing SubagentStop |
|---|---|---|
| Wave 1 (before the API-error burst) | 17:52–17:56 | 0 / 7 (all paired) |
| Wave 2 (during/after the burst) | 17:57–18:05 | 7 / 8 |

The receiver was healthy — this is not delivery-side slowness on my end:

  • Every hook POST in the window was served in ≤177 ms (most < 50 ms), far under the 5 s hook timeout.
  • Hook POSTs kept arriving every minute (no receive gap); the 13 stops that were sent all arrived and were logged.
  • The 7 missing stops have no POST at all on the receiver (no request-start line, no "session not found" line) — they never hit the endpoint.

Honest caveat: the missing-stop set is best described as "subagents in the burst window," not strictly "subagents that errored" — 5 of 7 ended abnormally, but 2 ended cleanly (stop_reason: end_turn) and still produced no SubagentStop. This suggests the orchestration teardown that emits SubagentStop was itself disrupted by the burst. Secondary anomaly: one agent produced a SubagentStop with no preceding SubagentStart — the pairing is not always internally consistent.

Relationship to existing issues
The symptom (a "running" indicator stuck after a subagent exits) is reported at scale — #60095, #64303, #72711, #58637, #63094 — but all from the TUI / task-registry / reaping side. This report is about a different layer: the SubagentStop hook itself not being emitted, seen by simply counting the hooks, with quantified burst-correlation. I did not find an existing issue on the hook-emission angle (SubagentStop-tagged issues cover wrong target #70151, subagent identification #7881, payload contents #75979 — none about non-emission).

Likely why this is under-reported: actually reconciling SubagentStart/SubagentStop counts from an external hook consumer is uncommon. Most users only ever see the TUI symptom; if you don't count the hooks yourself, a dropped SubagentStop is invisible — even though the underlying failure is the same one those threads describe.

Questions

  1. Is SubagentStop expected to fire when a subagent terminates early on an API error? The docs say yes; observation says no during a burst.
  2. Is SubagentStop emission dependent on the parent/orchestration completing a normal teardown? That would explain the clean (end_turn) agents that also lost their stop.
  3. Would you consider a success/failure (or reason) field on SubagentStop, or a SubagentStopFailure counterpart, so consumers can reconcile abnormal exits?

Getting a definitive fire-side vs delivery-side answer
Reproduce with claude --debug capturing hook execution to distinguish "hook never fired" from "hook fired but POST failed." The hard part is reproducing the API-error burst on demand.

What Should Happen?

Every subagent that emits a SubagentStart should emit exactly one matching SubagentStop with the same agent_id — including subagents that terminate early on API errors (per the documented firing conditions: normal completion, API errors during execution, user interruption, worktree cleanup). Counting SubagentStart vs SubagentStop across a session should balance out.

Today it does not under an API-error burst: the counts stay mismatched, so any consumer that pairs or counts the two hooks by agent_id drifts permanently, with no self-healing signal (main Stop/UserPromptSubmit interleave during multi-subagent turns and can't be used as an "all subagents done" barrier).

Error Messages/Logs

Hook receiver reconciliation for one session (times local, 2026-07-17):

  SubagentStart received ......................... 16
  SubagentStop received .......................... 13   (9 matched + 4 orphan stops with no start)
  Started but NO SubagentStop .................... 7
      a049932c64544e325  a537b0336fb70748f  acf964cdeae71300f
      a865332cb3829be83  a06c53f00b7cbe7ad  a3bc69726d2c48208
      a01c9300c899f6690
  "terminated early due to an API error" in transcript .... 14 occurrences

Receiver was healthy in the same window:
  - /api/hook/... POST durations: max 177 ms, most < 50 ms (hook timeout is 5 s)
  - POSTs arrived every minute; all 13 delivered stops were logged
  - The 7 missing stops have NO "Request starting POST /api/hook/..." line at all — the POST never reached the endpoint

Example transcript record for one of the missing-stop agents (main session .jsonl):
  "agentId":"a049932c64544e325","agentType":"general-purpose",
  "content":[{"type":"text","text":"Agent terminated early due to an API error: API Error: ..."}]

Per-agent transcripts (<session>/subagents/agent-<id>.jsonl) confirm all 7 stopped writing
by ~18:05; 2 of them ended with stop_reason "end_turn" (clean) yet still produced no SubagentStop.

Steps to Reproduce

Note up front: reliably reproducing needs an API-error burst — that's the hard part. Without the burst (the pre-burst wave in my session), SubagentStart/SubagentStop pairing is perfect. The steps below are the setup that produced the mismatch.

  1. Configure Claude Code hooks so that SubagentStart and SubagentStop are delivered somewhere you can count them by agent_id — e.g. an HTTP endpoint (type:http) or a command hook that appends each event to a file.
  2. Run a task that fans out into many parallel subagents (agent_type=general-purpose) — e.g. a multi-agent workflow / a batch of Task calls. In my case ~15 subagents across two waves within a single turn.
  3. Have it running while the Anthropic API is throwing errors. In my session ~14 subagents hit Agent terminated early due to an API error. (This is the condition that's hard to force on demand.)
  4. After the turn ends, count SubagentStart vs SubagentStop received.

Expected: the two counts balance (every started subagent produces a stop).
Actual: 7 subagents had a SubagentStart but no SubagentStop; the mismatch never reconciles. All 7 fell in the burst window; the pre-burst wave in the same session paired 0 missing / 7.

To capture whether the hook fired but failed to deliver vs never fired, run with claude --debug so hook execution is logged.

Claude Model

None

Is this a regression?

I don't know

Last Working Version

Claude Code Version

2.1.212 (Claude Code)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Xterm

Additional Information

Terminal/Shell is set to "Xterm" because the session runs inside TerminalHub — a Blazor app that hosts xterm.js over Windows ConPTY (the underlying shell is PowerShell). Hooks are injected via Claude Code startup options and delivered over HTTP (type:http) to TerminalHub's local endpoint, which is how I can count SubagentStart/SubagentStop externally and independently of the TUI.

The subagents were spawned by a multi-agent workflow; each agent's .meta.json shows spawnDepth: 1, model: haiku.

View original on GitHub ↗

3 Comments

toyuvalo · 25 days ago

Corroborating from a second Windows host — the "batch death with no signal" pattern is real and reproducible in heavy multi-agent use (Windows 10 Enterprise 10.0.19045, versions up to current 2.1.222, subscription OAuth, direct API).

My variant of your external-observer finding: during long fan-outs, a burst-condition event takes out multiple background subagents at once with no surfaced signal — the parent just stops hearing from them. Concrete incident: 4 parallel research subagents simultaneously at 0 bytes for 13+ minutes, no error anywhere, killed manually. Your SubagentStart/SubagentStop reconciliation (7 of 8 stops lost in the burst wave vs 0 of 7 pre-burst) matches what I see from the outside: the loss is condition-dependent, not stochastic.

The adjacent delivery bug makes it worse: subagents that complete successfully during/around such windows sometimes never surface their results — the finished output sits in the on-disk task transcript while the parent waits indefinitely (see also #78782, #75035, #78338). I've had to run a custom UserPromptSubmit hook that scans the tasks directory for completed-but-unsurfaced agents and injects their output paths, plus a standing "verify via artifact timestamps, never trust silence" rule for swarms >1h. That reconciliation-from-disk is exactly what the harness should do itself when a stop event or completion notification is lost.

Also possibly related to the burst trigger: I've filed #84273 for OAuth-rollover killing every background agent with 401 simultaneously (10 agents inside a 15-second window, timestamped) while the parent session — which refreshed successfully — kept working. If your API-error burst coincided with a token refresh window, these may share a mechanism. Additional local datapoint on the "silent" part: 60 of 259 background-agent .output files on this machine (23%) are zero bytes — spawned agents that emitted nothing and produced no death notification; observed cost was work sitting stopped for 8–22 hours across three batch die-offs on 2026-08-03→05 before a human noticed.

zio3 · 25 days ago

@toyuvalo Thanks for the corroboration and for filing #84273. You asked whether my API-error burst coincided with a token refresh window — I went back and checked, and can now answer that directly.

Result: both of my bursts were 529 Overloaded, not OAuth rollover (401).

I reconciled the actual error text of every Agent terminated early due to an API error record in the main-session transcripts (.jsonl):

| Burst | Window (local / UTC) | Errors | SubagentStop loss |
|---|---|---|---|
| Burst 1 (wave 2 of this report) | 7/17 17:57–18:05 JST (09:01:05–09:05:02 UTC) | 5 × API Error: 529 Overloaded — nothing else | 7 of 8 |
| Burst 2 (repro, same setup) | 7/21 16:38–17:05 JST (07:38–08:05 UTC) | 21 × API Error: 529 Overloaded — nothing else | 7 more start-only agents |

Meanwhile the only 401 in the entire session transcript is a single occurrence a month earlier (6/20, Please run /login · API Error: 401 Invalid authentication credentials — an ordinary expired login, nowhere near either burst).

Implication: spawn-time token capture (#84273) may well explain your simultaneous 401 die-offs, but my case occurred independently of OAuth. So I'd frame the shared mechanism one level up: during any window where API errors cluster, the orchestration/teardown path that emits SubagentStop gets taken down along with the subagents — a 401 burst (yours) and a 529 burst (mine) both land in the same failure mode. The trigger condition is "a burst," not "auth."

That framing is also consistent with the original observation here: 7/8 stops lost inside the burst vs 0/7 before it, including two agents that finished cleanly (stop_reason: end_turn) and still never produced a SubagentStop.

DanielMJ511 · 5 days ago

Another possible trigger for the same symptom, with weaker evidence than the report above — offering it because it suggests the cause may not be API-error-specific.

A SubagentStop hook that appends one line per spawn to a log file recorded 6 builder lines where 7 builder spawns occurred. The 7th was a builder that, per the orchestrator's own independently-written record, "stalled on watchdog after completing the work." No API errors in that session.

Observed: the line count, and the orchestrator's separate record of the stall (written to a different file, by a different writer, so the two records fail independently).

Inferred, not measured: that the spawn count was 7, and that the hook failed to fire rather than failing some other way. I have no SubagentStart/SubagentStop reconciliation like yours — the missing line is the only direct evidence, and I have not excluded the hook firing and failing to write.

Posting in case the watchdog-stall path is worth checking alongside the error path. If the underlying cause is "abnormal termination skips SubagentStop" rather than anything specific to API errors, these are the same bug.

Practical consequence for anyone using this hook as a record: its counts are a lower bound, and the error only points one way — a lost spawn reads as fewer spawns, never as a gap.