subagent: make interrupt cancel a goal run, not just its turn
`interrupt` killed the child and trusted that to end the run. It didn't: the default signal is SIGINT, and `claude --print` handles SIGINT by writing its terminal `result` event and exiting **zero**. A zero exit is `TurnEnd::Complete`, so `spawn_and_track`'s killed-turn early return — the code that was supposed to stop the run — never fired, and the loop went straight on to spawn the next goal turn. An interrupted run carried on to completion; the interrupt changed nothing about the outcome. Measured, not inferred: `claude --print --verbose --output-format stream-json`, SIGINT'd mid-turn, exits 0 on every run. So record the reason instead of inferring it. `interrupt` writes `StopReason::Cancelled` — the same path `goal_reached`/`need_help` already use — while it still holds the `running` lock, so there is no instant in which the name has lost its cancel handle but not yet gained its stop reason. `plan_after_turn` checks stop reasons ahead of the goal, so the run stops whichever way the child ends up exiting. `force`'s SIGKILL still produces a `TurnEnd::Killed`, and `status` still prefers the kill record it already had. `continue`'s budget reset is untouched: it clears the stop reason and hands back a fresh allowance, which is what makes a cancel a pause an operator can undo. The test spawns the real continuation loop against a fake claude that exits 0 on SIGINT, interrupts it, and asserts turn two never starts — it reports `left: 2` without the fix.
This commit is contained in:
parent
11ec050ca8
commit
33da51382e
3 changed files with 217 additions and 24 deletions
|
|
@ -268,11 +268,13 @@ impl SubagentMcp {
|
|||
}
|
||||
|
||||
#[tool(
|
||||
description = "Signal a currently-running subagent session to stop. Only works once \
|
||||
it's actually running — a `start`/`continue` still in its brief window before the \
|
||||
process is confirmed spawned refuses interrupt too (nothing to signal yet; retry \
|
||||
shortly), same as a name with nothing tracked at all. `force: true` for SIGKILL, \
|
||||
otherwise SIGINT."
|
||||
description = "Cancel a currently-running subagent session. This stops the whole run, \
|
||||
not just the turn in flight: a session started with a `goal` will not be \
|
||||
re-prompted toward it afterwards, and `continue` is what restarts it. Only works \
|
||||
once it's actually running — a `start`/`continue` still in its brief window before \
|
||||
the process is confirmed spawned refuses interrupt too (nothing to signal yet; \
|
||||
retry shortly), same as a name with nothing tracked at all. `force: true` for \
|
||||
SIGKILL, otherwise SIGINT."
|
||||
)]
|
||||
fn interrupt(&self, Parameters(args): Parameters<InterruptArgs>) -> String {
|
||||
match session::interrupt(&self.state, &args.name, args.force) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue