fix(coding-agent): cancelling a turn now stops its subagents - #35
Conversation
|
Reviewed against #25 with an independent re-run ( Mechanics verified: the cascade sits at the end of One decision to surface prominently for the maintainer, since it is this fork's largest deliberate reversal of upstream semantics so far: upstream Prime PR PrimeIntellect-ai#346 pinned that a turn interrupt does NOT cancel active RLM children, and this PR inverts that characterization test. With fire-and-forget as the documented spawn pattern, an interrupt intended to redirect the parent now also kills a long-running child admitted by an earlier turn. The analysis that no host-visible marker separates awaited from detached runs is correct, and the chosen cut (all unfinished spawn runs die; retained sessions survive; Approving from the review side; merge gate remains maintainer approval per PYLON.md. Reviewed by Fable 5 via Claude Agent SDK. |
Every user-facing cancel path routes through AgentSession.requestAbort(), which suspended the scheduler and aborted the parent's provider stream but left in-flight RLM child runs streaming; only abort() cascaded. Cancelling a turn therefore left children consuming provider capacity until they finished or idled out. requestAbort() now performs the same cascade abort() always did, after the scheduler suspension so cancelled runs are abandoned for quiescence and cannot inject a late terminal notice. abort() inherits it instead of repeating it. Retained child sessions are addressable background subagents and keep running; cancel those individually with cancelRlmChildRun. fixes #25
e86c09e to
8fb195b
Compare
Problem
AgentSession.requestAbort()suspended the scheduler and aborted retry, compaction, branch summary, bash, refine, and the parent's provider stream, but it did not cancel active RLM child runs — onlyabort()called_cancelActiveRlmChildRuns. Every user-facing cancel routes throughrequestAbort(): interactive ctrl-c viaAgentConnection.abort, ACP/in-processabortandabortAndClearQueue, and daemonabortandabort_and_clear_queue. Cancelling a parent turn therefore left its children streaming until they finished or idled out, holding provider concurrency and burning the user's subscription. This is the Prime-side half of Meridian's incomplete parent-to-child cancellation failure mode (#22).Fix
requestAbort()now performs the same cascadeabort()always did, andabort()inherits it instead of repeating it. Placement matters: the cascade runs after the scheduler suspension, so_cancelRlmChildRunroutes each cancelled run through_abandonRlmRunForQuiescence. Cancelled children leave no unsettled quiescence work, cannot report success, and cannot inject a late terminal notice into a later turn. Grandchildren are covered becauserun.abort()calls the child's ownabort(), which cascades recursively. No daemon command, event, or schema change — this is entirely session-local.abortForUpdateRestart()is unchanged.I did not thread a
cascadeoption. All five user-facing call sites want the same behavior, so a flag would only be ceremony, and the prompt-lifecycle route the issue floated would add a second cancellation owner for no additional coverage.Which children die
_activeRlmChildRuns) is cancelled, including one admitted by an earlier turn under the fire-and-forget patterndocs/rlm-runtime.mdrecommends. There is no narrower correct cut:RLMSpawnHandleconfirms admission only and never carries the child's answer, so no Python cell ever awaits a child result and the host has no marker separating an awaited child from a detached one. What ties a run to the session is the quiescence barrier and the terminal notice it injects into a later parent turn. Cancel is the user's stop-everything gesture, anddispose(),abort(), andabortForUpdateRestart()already cut all active runs; this wires the user's cancel to the semantic that already existed. Surgical control stays available throughrlm.delete_subagent()and the interactive per-child stop._rlmChildSessions) keep running. A retained child has already finished its spawn run and stays alive only soagent_message,rlm.list_subagents, and inspectors can keep addressing it; any work it is doing belongs to a later explicit request, not to the cancelled turn.abort()never terminated them either, so cascading into them would be a strictly larger change than the bug requires, and the user already hascancelRlmChildRun/rlm.delete_subagentto kill a specific one. Prime also exposes no detached or background spawn mode —rlm.runaccepts onlyname,model, andthinking— so retained-versus-active is the only real distinction available to decide on. Pinned by a test.Both outcomes are now documented in
packages/coding-agent/docs/rlm-runtime.md.Upstream
Not superseded. At
PrimeIntellect-ai/prime-agent@c382f098requestAbort()still leaves children running, and upstream PR PrimeIntellect-ai#346 pinned that with the characterization test "does not cancel active rlm children when only the parent turn is interrupted", which this PR inverts. Upstream PR PrimeIntellect-ai#464 (closed unmerged) made per-child cancellation explicit but never bound it to a turn abort; open PR PrimeIntellect-ai#1253 cancels child runs on kernel host teardown, which is teardown-scoped rather than turn-scoped. Recorded asturn-scoped-subagent-cancellation→ redesign in.pylon/features.yaml, with the evidence in.pylon/upstream-review.md.Validation
packages/coding-agent/test/suite/regressions/25-request-abort-rlm-cascade.test.ts(2 tests, faux provider): an in-flight child's provider stream is cut, the run status goesrunning→cancelled→ untracked, no further child request reaches the faux provider (callCountstays 1, one queued response unconsumed), no terminal notice or pending next-turn message is produced, a freshwaitForRlmQuiescence()resolves, and the next parent turn runs. Second test pins retained-child survival. Verified to fail on the pre-change implementation.agent_messagecoverage is unchanged.test/agent-session-recursion.test.ts112/112. A 17-file affected batch across abort, RLM, subagent, queue, prompt, compaction, ACP, and correlated-lifecycle suites: 396/396.npm run checkclean.Claude Opus via Claude Agent SDK.