Skip to content

Commit ec0c9b7

Browse files
committed
Unblock wait_agents on soft interrupt
Soft interrupt leaves the run in flight, and wait status treats interrupted+inFlight as running so resume cannot collect a stale stamp. Flip the wait mailbox overlay from interrupt_agent (same as send_input interrupt:true) so wait returns interrupted without waiting for settle.
1 parent 8837873 commit ec0c9b7

3 files changed

Lines changed: 14 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
4646
longer swallow `--*` or `-h` as their option values.
4747
- `wait_agents` no longer collects a stale completed or interrupted stamp
4848
when a followup is already in flight.
49+
- `interrupt_agent` flips the wait mailbox so soft interrupt unblocks
50+
`wait_agents` while the background run is still in flight.
4951
- Credential-refresh and auth send failures tell the user to log in again
5052
instead of suggesting `/model`.
5153

src/subagent/agent-fleet.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1033,8 +1033,10 @@ describe("interrupt_agent unblocks wait_agents", () => {
10331033
});
10341034
const id = spawned.agent_id as string;
10351035

1036-
// interruptOne is wait-terminal via session interrupted; collect freezes it.
1036+
// Soft interrupt leaves the run in flight; the mailbox overlay is what
1037+
// makes wait terminal (same path interrupt_agent takes).
10371038
expect(deps.sessions.interruptOne(id).ok).toBe(true);
1039+
deps.fleetRecords.interrupt(id);
10381040
expect(deps.fleetRecords.peek(id)?.status).toBe("interrupted");
10391041

10401042
const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 });
@@ -1116,6 +1118,10 @@ describe("interrupt_agent unblocks wait_agents", () => {
11161118
fleetRecords.register(worker.id);
11171119
sessions.registerInterrupt(worker.id, () => {});
11181120
sessions.interruptOne(worker.id);
1121+
// Mirror interrupt_agent: soft interrupt alone projects as running while
1122+
// in-flight, so the mailbox must flip for wait to see "interrupted".
1123+
fleetRecords.interrupt(worker.id);
1124+
fleetRecords.interrupt(worker.id);
11191125

11201126
const wait = createWaitAgentsTool({ sessions, fleetRecords });
11211127
const waited = await callTool(wait, { targets: [worker.id], timeout_ms: 1000 });

src/subagent/lifecycle-tools.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export type CloseAgentToolDeps = LifecycleToolDeps & {
121121
fleetRecords: FleetMailboxHandle;
122122
};
123123

124-
/** interrupt_agent stamps session interrupted; wait JSON projects that lifecycle. */
124+
/** interrupt_agent stamps session interrupted and flips the wait mailbox overlay. */
125125
export type InterruptAgentToolDeps = LifecycleToolDeps & {
126126
fleetRecords: FleetMailboxHandle;
127127
};
@@ -299,6 +299,10 @@ export function createInterruptAgentTool(deps: InterruptAgentToolDeps): AgentToo
299299
`Error: cannot interrupt "${target}" (status: ${outcome.status}).`,
300300
);
301301
}
302+
// Soft interrupt leaves the run in flight; projectWaitStatus treats
303+
// interrupted+inFlight as running so resume cannot collect a stale stamp.
304+
// Flip the wait mailbox overlay here (same as send_input interrupt:true).
305+
deps.fleetRecords.interrupt(target);
302306
return lifecycleResult(
303307
call.id,
304308
JSON.stringify({ agent_id: target, status: "interrupted" satisfies AgentLifecycleStatus }),

0 commit comments

Comments
 (0)