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
2 changes: 1 addition & 1 deletion webviews/src/agent-session/acpmux/direct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2572,7 +2572,7 @@ export class AcpmuxDirectClient {
return;
}
if (event.dir === "mux") {
// A stopped turn or a gone agent ends the subagents it would never report again.
// A stopped or completed turn, or a gone agent, ends the subagents it would never report again.
const left = subagentsLeft(event.kind, msg);
if (left && this.subagents.stopRunning(left, event.at)) this.takeSubagentRows();
if (event.kind === BOOKMARKS_KIND) {
Expand Down
7 changes: 5 additions & 2 deletions webviews/src/agent-session/acpmux/subagents/subagentFold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,19 @@ export class SubagentFold {

/// Which running subagents nothing will end any more, and how they read: `turn` only the
/// current turn's foreground ones, else every one.
export type SubagentsLeft = { state: "cancelled" | "disconnected"; turn: boolean };
export type SubagentsLeft = { state: "completed" | "cancelled" | "disconnected"; turn: boolean };

/// Whom acpmux's record `kind` leaves running with no one to end them: a stopped turn's
/// foreground subagents (`cancelled`; the harness keeps background ones running), a turn the
/// foreground subagents (`cancelled`; the harness keeps background ones running), a completed
/// turn's foreground ones whose end the harness never sent (`completed`: the turn waited on them;
/// they read running for good, Leo's Inspector counted 5 running with about 3 live), a turn the
/// daemon lost (`outcome_unknown`), or, when the agent process behind the chat went away or
/// started again (an exit or stop, a reopen or resume, a new or ended host), every one, as
/// acpmux's own process table reads those records (hub/processes.rs).
export function subagentsLeft(kind: string, msg: any): SubagentsLeft | undefined {
if (kind === "turn_result" && msg?.detail === "outcome_unknown") return { state: "disconnected", turn: true };
if (kind === "turn_result" && msg?.status === "cancelled") return { state: "cancelled", turn: true };
if (kind === "turn_result" && msg?.status === "completed") return { state: "completed", turn: true };
return HARNESS_GONE.has(kind) ? { state: "disconnected", turn: false } : undefined;
}
const HARNESS_GONE = new Set(["exited", "stopped", "reopened", "resumed", "host_started", "host_ended"]);
Expand Down
28 changes: 24 additions & 4 deletions webviews/test/subagent-stopped-after-cancel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ test("a subagent the restarted agent no longer reports reads disconnected", asyn
] as Step[]) {
const steps: Step[] = [
...turn("Run the audit in the background"),
spawned("bg", "Audit"),
background("bg", "Audit"),
["mux", "turn_result", { status: "completed" }],
restart,
];
Expand All @@ -174,13 +174,33 @@ test("a subagent the restarted agent no longer reports reads disconnected", asyn
}
});

test("a foreground subagent whose turn completed reads done, never running for good", async () => {
// Leo's Inspector read "5 running" with about 3 live: a foreground subagent's end the harness
// never sent kept it running after its turn completed. A foreground subagent cannot outlive a
// turn that completed (the turn waits on it); a background one runs on.
const rows = await connect(
fakeDaemon(
record([
...turn("Review and watch"),
spawned("fg", "Reviewer"),
background("bg", "Watch"),
["mux", "turn_result", { status: "completed" }],
]),
),
);
expect(states(rows())).toEqual([
["Reviewer", "completed"],
["Watch", "running"],
]);
});

test("a background subagent that still reports reads running", async () => {
// Its turn ended normally: it runs on in the background.
const quiet = await connect(
fakeDaemon(
record([
...turn("Audit in the background"),
spawned("bg", "Audit"),
background("bg", "Audit"),
["mux", "turn_result", { status: "completed" }],
]),
),
Expand Down Expand Up @@ -212,7 +232,7 @@ test("a background subagent that still reports reads running", async () => {
test("a turn the daemon lost reads its own subagents disconnected", async () => {
const steps: Step[] = [
...turn("Audit in the background"),
spawned("bg", "Audit"),
background("bg", "Audit"),
["mux", "turn_result", { status: "completed" }],
...turn("Review it"),
spawned("fg", "Reviewer"),
Expand All @@ -229,7 +249,7 @@ test("a stop ends only its own turn's subagents, and never a background one", as
const steps: Step[] = [
// Still running from an earlier turn: it went on in the background.
...turn("Audit in the background"),
spawned("old", "Audit"),
background("old", "Audit"),
["mux", "turn_result", { status: "completed" }],
...turn("Watch the build and review"),
background("bg", "Watch"),
Expand Down