Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/subagent/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,34 @@ describe("WorkerLifecycle projections", () => {
[{ state: "failed", error: "boom" }, "failed"],
[{ state: "cancelled", error: "stop" }, "cancelled"],
[{ state: "shutdown" }, "cancelled"],
[{ state: "shutdown", report: "ok" }, "done"],
];
for (const [lifecycle, status] of cases) {
expect(projectStripStatus(lifecycle)).toBe(status);
}
});

test("shutdown after completion reads done; a close error reads cancelled even with a salvage report", () => {
// CL-9924: close_agent after a completed report must not read as
// cancelled. Report presence alone is not enough — an interrupted run's
// salvage attaches via attachReport, so a shutdown that was closed
// mid-run/interrupted carries the close error and must stay cancelled.
expect(projectStripStatus({ state: "shutdown", report: "ok" })).toBe(
"done",
);
expect(
projectStripStatus({
state: "shutdown",
report: "salvage",
error: "Closed by close_agent",
}),
).toBe("cancelled");
expect(
projectStripStatus({ state: "shutdown", error: "Closed by close_agent" }),
).toBe("cancelled");
expect(projectStripStatus({ state: "shutdown" })).toBe("cancelled");
});

test("verb lifecycleStatus does not leak cancelled or failed", () => {
expect(projectLifecycleStatus({ state: "pending_init" })).toBe(
"pending_init",
Expand Down
11 changes: 10 additions & 1 deletion src/subagent/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,17 @@ export function projectStripStatus(lifecycle: WorkerLifecycle): StripStatus {
case "failed":
return "failed";
case "cancelled":
case "shutdown":
return "cancelled";
case "shutdown":
// CL-9924: a shutdown that carries a completed report and no close
// error was closed after finishing — read it as done. A close error
// means the lane was closed mid-run / after interrupt / after cancel
// (closeOne stamps "Closed by close_agent" on live shutdowns, and
// attachReport's salvage never removes it), so report presence alone
// is not enough: interrupted-close salvage would otherwise read done.
return lifecycle.report !== undefined && lifecycle.error === undefined
? "done"
: "cancelled";
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/tui/chrome-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,34 @@ describe("formatAgentsPanel", () => {
]);
});

test("a done-mapped shutdown lane paints done, not cancelled", () => {
// CL-9924: close_agent after a completed report projects strip status
// "done" (via projectStripStatus on the stored shutdown lifecycle); the
// terminal row must paint that as " · done", never " · cancelled".
const rows = formatAgentsPanel(
[
{
agentId: "a",
description: "reported then closed",
status: "done",
currentToolStartedAt: null,
finishedAt: NOW - 1_000,
},
],
undefined,
NOW,
);
expect(rows).toEqual([
{
label: "● a reported then closed",
tail: " · done",
stalled: false,
kind: "lane",
status: "done",
},
]);
});

test("linger expires — terminal rows drop after the window", () => {
expect(
formatAgentsPanel(
Expand Down
Loading