Skip to content

Commit 9af0e6c

Browse files
committed
fix(subagent): attach the send_input followup reply after an interrupt
1 parent a13cf88 commit 9af0e6c

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

‎src/subagent/agent-fleet.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,26 @@ describe("interrupt_agent unblocks wait_agents", () => {
14701470
expect(fleetRecords.peek(worker.id)?.status).toBe("interrupted");
14711471
});
14721472

1473+
test("completeAfterInterrupt attaches the followup reply to the interrupted lane", () => {
1474+
const sessions = createSubAgentSessionStore();
1475+
const fleetRecords = createFleetMailbox(sessions);
1476+
const worker = sessions.start({
1477+
description: "looping",
1478+
agentId: "explorer",
1479+
brief: "b",
1480+
retained: true,
1481+
});
1482+
sessions.markRunning(worker.id);
1483+
fleetRecords.register(worker.id);
1484+
fleetRecords.noteFollowup(worker.id);
1485+
fleetRecords.interrupt(worker.id);
1486+
fleetRecords.completeAfterInterrupt(worker.id, "followup reply");
1487+
// The overlay stays put, but the reply the send_input followup handoff
1488+
// passes in must surface to the parent through peek/wait.
1489+
expect(fleetRecords.peek(worker.id)?.status).toBe("interrupted");
1490+
expect(fleetRecords.peek(worker.id)?.report).toBe("followup reply");
1491+
});
1492+
14731493
test("rejected send_input followup clears the lane so wait collects interrupted salvage", async () => {
14741494
const gate = deferred<RunSubAgentResult>();
14751495
const followupGate = deferred<string>();

‎src/subagent/agent-fleet.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ interface FleetOverlay {
171171
lastWaitStatus?: WaitJSONStatus;
172172
/** Report/error already copied into a wait or occupancy payload. */
173173
bodyHanded?: boolean;
174+
/** Reply handed to completeAfterInterrupt — the settled follow-up's
175+
* reply, kept on the overlay because an interrupt/close overlay can pin
176+
* the projected status so the session payload alone would hide it. */
177+
report?: string;
174178
tombstoned?: boolean;
175179
hint?: string;
176180
/** Mirror of the session's failure record so it survives eviction. */
@@ -312,9 +316,11 @@ class FleetMailbox {
312316
/**
313317
* The interrupt followup finished. Clear the followup and queued overlays
314318
* so wait projects the settled session, but keep a close/interrupt overlay —
315-
* a followup reply must not undo interrupt_agent or close_agent.
319+
* a followup reply must not undo interrupt_agent or close_agent. The reply
320+
* itself lands on the overlay so interrupt/close-pinned lanes still hand it
321+
* to the parent; a session payload report keeps precedence.
316322
*/
317-
completeAfterInterrupt(id: string, _report?: string): void {
323+
completeAfterInterrupt(id: string, report?: string): void {
318324
const existing = this.records.get(id);
319325
if (existing === undefined || existing.collected === true) return;
320326
let changed = false;
@@ -326,6 +332,10 @@ class FleetMailbox {
326332
delete existing.forceQueued;
327333
changed = true;
328334
}
335+
if (report !== undefined && report.length > 0) {
336+
existing.report = report;
337+
changed = true;
338+
}
329339
if (changed) this.sessions?.wake();
330340
}
331341

@@ -462,6 +472,11 @@ class FleetMailbox {
462472
...(overlay.bodyHanded === true ? { bodyHanded: true } : {}),
463473
...(overlay.tombstoned === true ? { tombstoned: true } : {}),
464474
...(overlay.hint !== undefined ? { hint: overlay.hint } : {}),
475+
// A tombstone already compacted the payload; the overlay reply goes
476+
// with it so compaction keeps bounding memory.
477+
...(overlay.tombstoned !== true && overlay.report !== undefined
478+
? { report: overlay.report }
479+
: {}),
465480
...(payload?.report !== undefined ? { report: payload.report } : {}),
466481
...(payload?.error !== undefined && status === "failed"
467482
? { error: payload.error }

0 commit comments

Comments
 (0)