[Bug] Agent stuck in infinite loop with backgrounded sleep timers after subagent fan-out completes

Status Open
Reported on v2.1.236
Maintainer reply None cached
Activity 3 comments · opened Aug 31, 2026

Bug Description
Title: [subagents] Agent polls subagent completion with background sleeps, never terminates.

Summary.
An agent that fans out to subagents waits for them by spawning sleep N; echo ok as backgrounded Bash commands. Every backgrounded command that finishes emits a task-notification, and each notification re-invokes the agent — so the wait mechanism is also a wake-up generator. With overlapping sleeps queued, the agent cannot reach a terminal state even after its work is complete: it wakes, sees nothing to do, replies, and the next timer fires. It is not self-limiting and ends only when a human kills it.

Impact.
Observed in one /code-review run: 33 minutes wall clock (06:55:24Z → 07:28:12Z) of which the last ~5 minutes were pure timer noise. The agent issued 42 backgrounded sleep calls with durations 150–550 s, received 19 task-notifications, and produced 8 replies reading "Background timer only. Review complete; deliverable unchanged." The final deliverable was ready at 07:23:44Z; the run had to be stopped manually at ~07:28. Cost is real — each wake-up is a full cached-context request — and the user cannot tell a stuck agent from a working one, since progress output looks identical.

Reproduction.

  1. Invoke a skill that fans out to many subagents (/code-review max <MR url>, launched as a background fork). 11 subagents were spawned here.
  2. Observe that the agent, instead of ending its turn, issues backgrounded sleep commands to wait for them.
  3. Each expiring sleep re-invokes the agent; on each wake it observes that some subagents are still running and issues another sleep.
  4. After the final report is produced, the queued sleeps keep firing. The agent never terminates on its own.

Deterministic given a fan-out wide enough that the first sleep expires before the slowest subagent finishes.

Root cause.

  1. Foreground sleep is blocked by the Bash tool, so the only wait the agent reaches for is a backgrounded one.
  2. A backgrounded Bash command that completes emits a task-notification that re-invokes the agent — correct for real work, wrong for a no-op timer. N overlapping sleeps ⇒ N wake-ups, all after the fact.
  3. The agent does not rely on automatic subagent-completion notification, although the Agent tool documents it ("you'll be notified when one completes"). On each wake it saw unfinished finders and added another sleep — a positive feedback loop that outlived the work.
  4. Monitor — the documented primitive for this — was not unavailable but unused: its schema was fetched via ToolSearch("select:Monitor") at 07:08:19Z, nine seconds before the first backgrounded sleep at 07:08:28Z, and it was never called. This is a nudging failure, not a capability gap.
  5. There is no terminal state while timers are pending; only user-initiated TaskStop ends the run.

In sum: a blocked foreground primitive pushes the agent to a backgrounded one, the backgrounded one generates wake-ups, each wake-up reproduces the condition that created it, and no terminal state breaks the cycle. Any one of the four removed would have stopped the loop.

Proposed fixes (in order of cost):

  1. In the built-in code-review skill, end the turn after fan-out instead of polling — subagent completion re-invokes the agent by itself.
  2. Do not emit a task-notification for a backgrounded command that is a bare timer, or coalesce pending timer notifications into a single wake-up.
  3. Strengthen the Bash tool description: it already says foreground sleep is blocked and points at Monitor, but does not forbid backgrounded sleep-as-wait — which is the workaround agents actually find.
  4. Allow the agent to reach a terminal state with only no-op timers pending, or auto-cancel outstanding background sleeps once it produces a final result.

Incidental finding: the same run also reached for TaskStop on one subagent (07:26:15Z), suggesting the agent had no reliable read on what was still alive.

Claude Code 2.1.236, macOS (darwin 25.5.0), 2026-08-31. Timestamps and counts extracted from the session transcript tasks/a74f05be829431bf7.output; no file:line references — the skill and harness are not open source.

Environment Info

  • Platform: darwin
  • Terminal: WarpTerminal
  • Version: 2.1.236
  • Feedback ID: 0b7e9465-a08c-4b4d-9035-e5d48de60b04

Errors

[]

View original on GitHub ↗

3 Comments

kcarriedo · 1 month ago

The root cause you're describing - backgrounded sleep commands as a wait mechanism generating their own wake-ups - is a real design trap. The agent can't use foreground sleep (it blocks the tool), reaches for backgrounded sleep, and the backgrounded sleep's completion notification is indistinguishable from a subagent completing actual work. So the agent wakes up, sees the timers, and adds more.

A few observations from running similar fan-out patterns:

The most reliable workaround before a native fix lands is replacing the sleep-based polling with file-based signaling. Each subagent writes a sentinel file when it finishes (/tmp/agents/<task-id>.done). The orchestrator checks for those files with a single Bash glob rather than spawning new sleep processes. No new notifications, no cascade.

The deeper issue is that there's no way for an agent to tell the difference between "a subagent completed" and "a timer I set expired" in the current notification model. A notification type field on task completion events would let the orchestrator ignore timer noise and only wake on meaningful events.

The 33-minute session where the last 5 minutes were pure timer noise at real token cost is a good concrete example of why this matters - it's not just a UX annoyance.

Related: #76021 documents a similar notification-flooding issue in a different pattern (duplicate completion notifications on background tasks) that might share the same event-dispatch layer.

Ezra144israel · 1 month ago

The wait that keeps waking the agent is not a wait. Foreground sleep was blocked so it kept backgrounding sleep, and Monitor was already there unused. I keep the same goal and drop that route instead of retrying it.

https://github.com/Ezra144israel/governed-agent-skills/blob/main/skills/reasoning-doctrine/references/find-a-way.md

webby-box · 1 month ago

Backgrounded sleep-as-wait that wakes the agent is a positive feedback loop, not a wait. Once the deliverable exists, pending no-op timers shouldn't keep the run alive — file sentinels (or Monitor) beat sleep storms every time.

The scary part is UX: progress output looks identical whether it's still working or just chewing cached-context wakeups. Hard terminal state after final report + cancel outstanding timer jobs would stop the bleed without needing a human to TaskStop.