fix: robust subagent completion propagation#13321
fix: robust subagent completion propagation#13321ASidorenkoCode wants to merge 1 commit intoanomalyco:devfrom
Conversation
Fixes parent session hanging when a subagent completes but the prompt loop doesn't exit. Three changes: 1. `await` the `SessionCompaction.prune()` call that was fire-and-forget, eliminating a race between prune writes and the following stream reads. 2. Add diagnostic logging when the loop exit condition fails (assistant finished but user message ID is newer), making stuck loops visible. 3. Add event-driven recovery in the task tool: subscribe to child message updates, and if the child's assistant message reaches a terminal state (finish or error) but the prompt doesn't resolve within 3s, force-cancel to unblock the parent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
Reproduction & verificationSimulating the stuck conditionTo reliably reproduce the race condition, I injected an abort-aware delay in if (result === "stop") {
if (session.parentID) {
log.warn("INJECTED DELAY: child loop stuck after processor returned stop", { sessionID })
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 120_000)
abort.addEventListener("abort", () => { clearTimeout(timer); resolve() }, { once: true })
})
}
break
}The delay is abort-aware so that Before (without fix, with injected delay)Log confirmed the child was stuck: Parent waited forever — had to be killed. After (with fix, no injected delay)Log confirmed clean exit: Additional findingDuring testing, discovered that processor errors set |
|
The following comment was made by an LLM, it may be inaccurate: No duplicate PRs found |
Summary
Fixes parent session hanging indefinitely when a subagent (Task tool) completes but
SessionPrompt.prompt()never resolves. Fixes #9003, #10802, #11865, #6792.Three targeted changes:
awaittheprune()call (prompt.ts:653) — was fire-and-forget, racing with the immediately followingMessageV2.stream()reads.prompt.ts:305-312) — the loop exit condition silently continued when assistant finished but user message ID was newer. Now logs a warning so this case is visible.task.ts) — subscribe to childMessageV2.Event.Updated; if the child reaches a terminal state (finish or error) but the prompt doesn't resolve within 3s, force-cancel to unblock the parent. Not a timeout — only fires with positive proof of completion.