[BUG] StopFailure fires for internal helper queries and subagent failures on the parent session; under low-priority mode every excluded helper 429 reports a failed turn that never happened
Summary
StopFailure fires for API errors in Claude Code's own internal helper queries (prompt suggestion, away summary, memory extraction, agent summary) and for background subagent failures, always with the parent session_id. Under low-priority mode those helpers are excluded from retry, so while a usage limit is exhausted each one dies on a 429 and fires StopFailure at an idle prompt. Any hook consumer that reads the event as "the turn ended on an API error" (its documented meaning) shows a failed turn that never happened.
Environment
- Claude Code 2.1.258, native binary, Windows 11
- Interactive REPL,
bypassPermissions,/low-priorityenabled after hitting the 5-hour limit StopFailurehook wired to a small command hook that forwards the payload to a desktop notifier
What happened
- Main loop launched nine background agents and parked ("waiting on 9 agents",
Stopwith a non-emptybackground_tasks). Seven agents died on429 rate_limit; each death firedStopFailureon the parent session, and each failed task-notification woke the main loop, which hit the same 429 and firedStopFailureagain. FourteenStopFailurehooks in about a minute for one underlying limit. - Enabled
/low-priority. The main loop and agents resumed correctly. - Two minutes later, with the main loop still parked and no user input, three more
StopFailurehooks fired (error: "rate_limit", noerror_details) about 20-30 s apart. No transcript line, no subagent transcript line, and no task output records any API error in that window. The next entry in the session transcript is a completed-agent task-notification six minutes later.
Why (from the 2.1.258 bundle)
- The forked helper queries all go through the same query generator as the main turn: the
Gvfork helper (querySourceinprompt_suggestion,away_summary,extract_memories,agent_summary,skipTranscript: true) iteratesFO, which runs the main loop generator (o1n, both branches). - That generator's API-error exit calls the
StopFailureemitter (fOe) for anyisApiErrorMessage, usingsession.id. The only skip is a delegated-observation subagent context. Nothing distinguishes a fork, a subagent, or the main turn in the payload; the schema is base fields pluserror,error_details,last_assistant_message. - The low-priority retry allow-list (
JZ:agent:*,compact,hook_agent,hook_prompt,side_question,web_search_tool,web_fetch_apply,repl_sampling,auto_mode*,chrome_mcp,artifact_comment_reply, plus the builtin agent keys) excludes those helpers. For an excluded source the 429 handler returns null, so the request errors instead of waiting. - Result: with the limit exhausted, every helper fork fails immediately and fires
StopFailure, while the real turn is fine.
Which helper fired in step 3 is inferred (the payload carries nothing to tell, and forks skip the transcript); the timing matches returning to an idle prompt.
Expected
StopFailure means the turn ended on an API error. Helper forks and background subagent deaths are not the turn ending. The docs describe it that way and consumers (auto-continue hooks, dashboards, notifiers) depend on it.
Suggested fix
Either of these would do; both would be best:
- Do not fire
StopFailurefromskipTranscriptforks, or more generally from non-mainquerySources. A subagent death already surfaces through its failed task-notification and the re-woken main loop's own outcome. - Add
query_source(andagent_idwhen present) to theStopFailurepayload so consumers can filter, the waySubagentStopalready carriesagent_id.
Separately, the pre-low-priority cascade in step 1 is worth a look: each dead agent re-wakes a main loop that cannot succeed, so one exhausted limit produces 2N StopFailure hooks.