[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

Status Open
Reported on v2.1.258
Maintainer reply None cached
Activity 0 comments · opened Sep 2, 2026

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-priority enabled after hitting the 5-hour limit
  • StopFailure hook wired to a small command hook that forwards the payload to a desktop notifier

What happened

  1. Main loop launched nine background agents and parked ("waiting on 9 agents", Stop with a non-empty background_tasks). Seven agents died on 429 rate_limit; each death fired StopFailure on the parent session, and each failed task-notification woke the main loop, which hit the same 429 and fired StopFailure again. Fourteen StopFailure hooks in about a minute for one underlying limit.
  2. Enabled /low-priority. The main loop and agents resumed correctly.
  3. Two minutes later, with the main loop still parked and no user input, three more StopFailure hooks fired (error: "rate_limit", no error_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 Gv fork helper (querySource in prompt_suggestion, away_summary, extract_memories, agent_summary, skipTranscript: true) iterates FO, which runs the main loop generator (o1n, both branches).
  • That generator's API-error exit calls the StopFailure emitter (fOe) for any isApiErrorMessage, using session.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 plus error, 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 StopFailure from skipTranscript forks, or more generally from non-main querySources. A subagent death already surfaces through its failed task-notification and the re-woken main loop's own outcome.
  • Add query_source (and agent_id when present) to the StopFailure payload so consumers can filter, the way SubagentStop already carries agent_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.

View original on GitHub ↗