Skip to content

Commit 345fea5

Browse files
committed
fix(subagent): read shutdown-with-report lanes as done, not cancelled
1 parent 081c161 commit 345fea5

3 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎src/subagent/lifecycle.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,34 @@ describe("WorkerLifecycle projections", () => {
2222
[{ state: "failed", error: "boom" }, "failed"],
2323
[{ state: "cancelled", error: "stop" }, "cancelled"],
2424
[{ state: "shutdown" }, "cancelled"],
25+
[{ state: "shutdown", report: "ok" }, "done"],
2526
];
2627
for (const [lifecycle, status] of cases) {
2728
expect(projectStripStatus(lifecycle)).toBe(status);
2829
}
2930
});
3031

32+
test("shutdown after completion reads done; a close error reads cancelled even with a salvage report", () => {
33+
// CL-9924: close_agent after a completed report must not read as
34+
// cancelled. Report presence alone is not enough — an interrupted run's
35+
// salvage attaches via attachReport, so a shutdown that was closed
36+
// mid-run/interrupted carries the close error and must stay cancelled.
37+
expect(projectStripStatus({ state: "shutdown", report: "ok" })).toBe(
38+
"done",
39+
);
40+
expect(
41+
projectStripStatus({
42+
state: "shutdown",
43+
report: "salvage",
44+
error: "Closed by close_agent",
45+
}),
46+
).toBe("cancelled");
47+
expect(
48+
projectStripStatus({ state: "shutdown", error: "Closed by close_agent" }),
49+
).toBe("cancelled");
50+
expect(projectStripStatus({ state: "shutdown" })).toBe("cancelled");
51+
});
52+
3153
test("verb lifecycleStatus does not leak cancelled or failed", () => {
3254
expect(projectLifecycleStatus({ state: "pending_init" })).toBe(
3355
"pending_init",

‎src/subagent/lifecycle.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,17 @@ export function projectStripStatus(lifecycle: WorkerLifecycle): StripStatus {
3535
case "failed":
3636
return "failed";
3737
case "cancelled":
38-
case "shutdown":
3938
return "cancelled";
39+
case "shutdown":
40+
// CL-9924: a shutdown that carries a completed report and no close
41+
// error was closed after finishing — read it as done. A close error
42+
// means the lane was closed mid-run / after interrupt / after cancel
43+
// (closeOne stamps "Closed by close_agent" on live shutdowns, and
44+
// attachReport's salvage never removes it), so report presence alone
45+
// is not enough: interrupted-close salvage would otherwise read done.
46+
return lifecycle.report !== undefined && lifecycle.error === undefined
47+
? "done"
48+
: "cancelled";
4049
}
4150
}
4251

‎src/tui/chrome-state.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,34 @@ describe("formatAgentsPanel", () => {
348348
]);
349349
});
350350

351+
test("a done-mapped shutdown lane paints done, not cancelled", () => {
352+
// CL-9924: close_agent after a completed report projects strip status
353+
// "done" (via projectStripStatus on the stored shutdown lifecycle); the
354+
// terminal row must paint that as " · done", never " · cancelled".
355+
const rows = formatAgentsPanel(
356+
[
357+
{
358+
agentId: "a",
359+
description: "reported then closed",
360+
status: "done",
361+
currentToolStartedAt: null,
362+
finishedAt: NOW - 1_000,
363+
},
364+
],
365+
undefined,
366+
NOW,
367+
);
368+
expect(rows).toEqual([
369+
{
370+
label: "● a reported then closed",
371+
tail: " · done",
372+
stalled: false,
373+
kind: "lane",
374+
status: "done",
375+
},
376+
]);
377+
});
378+
351379
test("linger expires — terminal rows drop after the window", () => {
352380
expect(
353381
formatAgentsPanel(

0 commit comments

Comments
 (0)