Skip to content

Commit b585335

Browse files
committed
Clear leftover interrupted stop reason on follow-up begin
A successful resume kept the prior interrupt stamp, so wait_agents returned done with stop_reason interrupted. That looked like interrupted-incomplete and could start another successor.
1 parent c513fa2 commit b585335

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

‎src/subagent/lifecycle-tools.test.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,59 @@ describe("resume_agent", () => {
369369
expect(results[0]!.report).toBe("second report");
370370
});
371371

372+
test("interrupt then successful resume wait is done without leftover interrupted stop_reason", async () => {
373+
const sessions = createSubAgentSessionStore();
374+
const fleetRecords = createFleetMailbox(sessions);
375+
const worker = sessions.start({
376+
description: "worker",
377+
agentId: "a",
378+
brief: "b",
379+
retained: true,
380+
});
381+
sessions.markRunning(worker.id);
382+
sessions.registerInterrupt(worker.id, () => {});
383+
let finish: (reply: string) => void = () => {};
384+
sessions.registerFollowup(
385+
worker.id,
386+
() =>
387+
new Promise<string>((resolve) => {
388+
finish = resolve;
389+
}),
390+
);
391+
fleetRecords.register(worker.id);
392+
393+
const interruptAgent = createInterruptAgentTool({ sessions, fleetRecords });
394+
const resumeAgent = createResumeAgentTool({ sessions, fleetRecords });
395+
const wait = createWaitAgentsTool({ sessions, fleetRecords });
396+
397+
const interruptWaiting = callTool(wait, { targets: [worker.id], timeout_ms: 2000 });
398+
await callTool(interruptAgent, { target: worker.id });
399+
const interruptedWait = await interruptWaiting;
400+
expect(interruptedWait.timed_out).toBe(false);
401+
const interruptedResults = interruptedWait.results as {
402+
status: string;
403+
stop_reason?: string;
404+
}[];
405+
expect(interruptedResults[0]!.status).toBe("interrupted");
406+
expect(interruptedResults[0]!.stop_reason).toBe("interrupted");
407+
408+
const resumed = await callTool(resumeAgent, { target: worker.id, message: "continue" });
409+
expect(resumed.status).toBe("running");
410+
411+
const waiting = callTool(wait, { targets: [worker.id], timeout_ms: 2000 });
412+
finish("resumed report");
413+
const collected = await waiting;
414+
expect(collected.timed_out).toBe(false);
415+
const results = collected.results as {
416+
status: string;
417+
report?: string;
418+
stop_reason?: string;
419+
}[];
420+
expect(results[0]!.status).toBe("done");
421+
expect(results[0]!.report).toBe("resumed report");
422+
expect(results[0]!.stop_reason).not.toBe("interrupted");
423+
});
424+
372425
test("resume followup rejection invokes close; close_agent tears down leftover", async () => {
373426
const sessions = createSubAgentSessionStore();
374427
const fleetRecords = createFleetMailbox(sessions);

‎src/subagent/session-store.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,7 @@ export function createSubAgentSessionStore(
753753
mutate(id, (s) => {
754754
s.lifecycle = { state: "running" };
755755
delete s.finishedAt;
756+
delete s.stopReason;
756757
});
757758
};
758759
const endFollowupTurn = (id: string, restore: "completed" | "interrupted"): void => {

0 commit comments

Comments
 (0)