Agent loop: a parallel subagent result that lands after the final turn is never consumed — and the 2.1.218 nudge does not cover it (only fires on empty turns)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 3, 2026

Environment

  • claude-agent-sdk (Python) 0.2.128, bundled Claude Code CLI 2.1.220
  • macOS (arm64), model claude-sonnet-5
  • Pattern: one user request → model dispatches 3 parallel Agent (subagent) calls in one turn → each runs one tool and returns

Summary

When N subagents run in parallel, the last result to arrive can land after the loop has already decided the turn is over. It is appended to the transcript as a user message, but no further model turn is ever scheduled, so it never enters any API request. The user receives the model's earlier interim sentence — "still waiting for X" — as the final answer, while X's result is sitting on disk, complete.

No error. No warning. Every subagent reports completed, every result is persisted, exit is clean.

This is not fixed by 2.1.218

We upgraded specifically because of:

2.1.218 — Fixed an engine teardown race that could start and abandon a phantom turn

After upgrading to CLI 2.1.220, a controlled 5-run reproduction did not reproduce (windows 2.7–3.4s, plus one at 29ms). We were about to close this as fixed.

Then it reproduced in normal use on 2.1.220.

The two observations disagree, and the pattern in how they disagree points at the gate condition.

In our 5 controlled runs the model's turn happened to be silent (no text emitted) — and in each case we observed the CLI inject:

[Your previous response had no visible output.
 Please continue and produce a user-visible response.]

after which the model went on to produce the full summary. That injection is what made the runs pass.

In both real-world failures the model had emitted text (an interim "still waiting for X" sentence) — and the injection never happened.

⇒ The self-heal appears to be gated on "did this turn produce any visible text", not on "is there an unconsumed task-notification". If the model said anything at all, it does not fire.

That is consistent with the 2.1.218 note being aimed at thinking-only responses — a different failure mode from ours. Our case is out of its scope: the turn is not empty, it just isn't final.

Evidence — two real occurrences, same shape

Occurrence B (on 2.1.220)agent_calls=3, num_turns=7, final answer 109 chars:

32  subagent_progress  #1 done/completed
33  tool/result Agent  dt=39.6      ← result #1
34  subagent_progress  #2 done/completed
35  tool/result Agent  dt=37.4      ← result #2
36  subagent_progress  #3 done/completed
37  tool/result Agent  dt=37.0      ← result #3 — all three now present
38  text/delta   "<result #2 summary> … still waiting for <#3>."
39  complete

Occurrence A (on 2.1.205) — same shape, final answer 24 chars, output_tokens: 26. Transcript timestamps:

36.669  assistant: "<#2> received, still waiting for <#3>."   ← becomes final answer
36.811  user: task-notification (#3 completed)                 ← appended 132ms later
        (no further assistant turn — ever)

Why the model is not at fault

At the moment it generated that sentence it was correct — #3 genuinely had not arrived (36.669 < 36.811). Nothing in the session said anything wrong. One turn simply never happened.

Also note "all three results were in the model's context" is false: being in the transcript ≠ being in a request. The third is a dangling user message that was never sent.

Trigger profile

Among N parallel subagents, when the last two complete within roughly one model-turn duration of each other. In occurrence B the three dt values were 39.6 / 37.4 / 37.0 — all above 37s, spread only 2.6s. Probe/measurement-style tasks naturally cluster their completion times, so this window is not exotic.

Both of our occurrences came from ordinary use, days apart, by a real user.

Impact

Silent data loss at the user-visible layer. The work ran and was billed (3 subagents, ~70s wall clock), the results exist on disk, and the user gets none of them — just a sentence saying the system is still waiting for something that already arrived.

Suggested fix

Make the terminal condition "no pending tasks AND no unconsumed task-notification messages", and schedule one more turn when a notification is appended after the previous turn ended.

If changing the loop is not straightforward, a narrower option: extend the existing nudge's condition to also fire when there are unconsumed task-notifications, regardless of whether the turn produced text. The injection mechanism already exists; only the gate is too narrow.

Failing both, please surface unconsumed notifications in ResultMessage so hosts can detect the state instead of emitting a clean completion.

Reproduction note

Blind reproduction is unreliable — and misleading. A naive repro tends to produce silent turns, which the nudge rescues, yielding a false "fixed" result (this happened to us, 0/5).

To actually hit it, the main prompt must force the model to narrate interim status, so the turn contains text and the nudge stays out of the way. Suggested shape: 3 probes sleeping ~35 / 37 / 38.5s, with a prompt that requires reporting each line's result as it arrives.

Not claimed

  • We have not traced the loop's code path. The "unconsumed notification" characterisation and the gate condition above are inferred from observed behaviour (transcripts + which runs did and did not receive the injection), not from reading the implementation.
  • Whether a notification arriving during generation vs after it ends is deterministic or probabilistic — the reproduction above is intended to pin that down.

View original on GitHub ↗

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