Subagent's run_in_background completion notification is never delivered if the agent ends its turn

Status Open
Maintainer reply None cached
Activity 1 comment · opened Aug 18, 2026

Summary

A run_in_background Bash task launched by a subagent is registered at session scope, not as a child of that agent. Two consequences compound:

  1. The agent is deemed to have no live children, so it may complete the moment it ends its turn.
  2. The completion notification is only ever delivered to an agent that is alive. A completed subagent gets no further turn, so the notification is queued indefinitely and reaches nobody — not the originating agent, not the coordinator.

The notification is not lost: reviving the agent by any means delivers the queued notification on the following round, even minutes later. The tool result promises "You will be notified when it completes", and for a turn-ending subagent that promise is not kept. The work finishes, nobody is told, and a human eventually notices the stall.

The main session is wakeable from idle (7/7 background tasks launched by the coordinator woke it whilst it had ended its turn). The asymmetry is the defect: an identical call is safe for the main loop and a silent trap for a subagent.

Evidence

| Run | Setup | Task exited | Delivered? |
|---|---|---|---|
| iso1 | subagent ends turn, coordinator silent | 14:52:04 | No — 5m20s silence |
| iso2 | as above | 15:00:08 | No — 4m47s silence |
| iso3 | as above | 15:00:19 | No — 4m36s silence |
| live1 | subagent stays alive across completion | 15:07:58 | Yes, 15:08:14, on its next tool round |
| int1 | coordinator resumes at T+25s, agent ends turn again | 15:07:34 | No — 4m45s silence |
| flush1 | coordinator resumes 2m43s after completion | 15:14:31 | Yes, 15:17:29, one round after the revival |

In every failing run the harness itself had already written [exited with code 0] into tasks/<id>.output — the exit was observed; only the delivery failed.

flush1 is the sharpest case: at 15:17:14 the reviving message arrived and the agent confirmed no notification was present; 15 seconds later the queued notification was delivered. The queue survives, it simply has no live recipient.

Direct evidence that background tasks are session-scoped: at one agent's SubagentStop, the payload's background_tasks list contained a task launched by a different agent, with status running.

Reproduction

  1. From a main session, spawn a subagent whose entire brief is: launch sleep 90; echo done with run_in_background: true, then end its turn immediately without polling.
  2. The coordinator sends nothing and waits 5 minutes.
  3. Observe: tasks/<id>.output gains [exited with code 0] at ~T+90s; the subagent's transcript has no entry after its final turn; the coordinator receives no notification.
  4. Now send the subagent any message. The queued task notification is delivered on the round after the message — proving it was retained the whole time.

Two related findings from instrumenting SubagentStop

While building a local mitigation I captured hook payloads. Two things may be useful to you, and one of them constrains the fix options below.

1. SubagentStop fires for events that are not an agent. Over 47 measured events across three concurrent sessions, 41 carried an empty agent_type and an ephemeral per-event agent_id whose agent_transcript_path was never written to disk at all. These fire at the turn cadence of a backgrounded subagent (measured at a rigid 31.4–32.8s across nine consecutive fires, including one gap of exactly two periods) and cease when that subagent stops. The remaining 6 events were real agents: agent_type set, transcript present.

This matters for anyone writing a SubagentStop hook: the payload gives no positive signal that an event is a real agent stop, so a hook must infer it from agent_type being non-empty. If the ephemeral events are internal bookkeeping, consider not surfacing them to hooks, or tagging them so they can be distinguished.

2. A shell task in background_tasks does not name its launcher. A two-agent controlled run captured this entry at a sibling agent's stop, while the launching agent's task and the coordinator's were both live:

{"id": "<task-id>", "type": "shell", "status": "running",
 "description": "Sleep 150 seconds then print completion marker",
 "command": "sleep 150; echo DONE"}

id, type, status, description, command — no owner, parent, or launching-agent field. Three shell tasks from three different launchers appeared identically in one stopping agent's payload. So a hook cannot attribute a task to its launcher from the payload; the only available source is intersecting background_tasks with the agent's own agent_transcript_path.

Note this also constrains suggested fix 2 below: re-routing an orphan "tagged with the originating agent id" would require recording an owner that is not currently exposed.

Also worth documenting: background_tasks is not only shell tasks — it lists live subagents too, as {"id": ..., "type": "subagent", "status": "running", "agent_type": ...}, including an entry for the stopping agent itself.

Suggested fixes, in order of preference

  1. Scope a background task to the agent that launched it, and defer that agent's completion whilst it has live children — the behaviour the notification's own wording already implies ("fires each time this agent stops with no live background children of its own").
  2. Failing that, re-route an orphaned task's completion to the parent session, which is wakeable from idle, tagged with the originating agent id. (See finding 2 — this needs an owner field that does not exist today.)
  3. At minimum, stop promising "You will be notified when it completes" to a subagent, or warn when an agent ends its turn holding an undelivered task.

Workaround for anyone hitting this

  • As a subagent: never end a turn awaiting a background task. Either stay alive across the completion (short bounded foreground calls — proven to work), or end the turn naming the task id and output path so the coordinator can collect it.
  • As a coordinator: when a subagent's last message names a pending task, arm your own background wait (the main session is wakeable from idle), then resume the subagent by message — that alone flushes its queued notification.

A local SubagentStop hook that refuses the stop whilst the stopping agent's own task is unfinished converts the silent stall into a loud one. It cannot compel the correct outcome, because the retry carries stop_hook_active=true and must be honoured or you build a loop.

Environment

  • Claude Code on macOS (darwin 25.4.0)
  • Opus coordinator, Sonnet and Haiku subagents
  • Measured 2026-08-18; observed 3× in one working session before it was characterised

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗