From d17b3aecad7a6bd6fd73f3c1e83c99393f40e1a4 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 01:45:27 +0000 Subject: [PATCH 1/2] test(cmux-next): a foreground subagent whose turn completed reads done (red) Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bd2-60d2-70e1-8179-d3aa717c8828&host=huge-blue --- .../subagent-stopped-after-cancel.test.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/webviews/test/subagent-stopped-after-cancel.test.ts b/webviews/test/subagent-stopped-after-cancel.test.ts index 8f3cc4546b6b..e9e963e221d8 100644 --- a/webviews/test/subagent-stopped-after-cancel.test.ts +++ b/webviews/test/subagent-stopped-after-cancel.test.ts @@ -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, ]; @@ -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" }], ]), ), @@ -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"), @@ -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"), From ec3dac625e7b822381ee6c586a76b88d81aab62c Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 22 Oct 2026 01:50:24 +0000 Subject: [PATCH 2/2] cmux NEXT subagents: a completed turn ends its foreground subagents, so Running counts only live ones Leo's Inspector read "5 running" with about 3 live: a foreground subagent whose end the harness never sent read running for good once its turn completed. A completed turn waited on its foreground subagents, so they read done; background ones run on. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bd2-60d2-70e1-8179-d3aa717c8828&host=huge-blue --- webviews/src/agent-session/acpmux/direct.ts | 2 +- .../src/agent-session/acpmux/subagents/subagentFold.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/webviews/src/agent-session/acpmux/direct.ts b/webviews/src/agent-session/acpmux/direct.ts index 60f4bc0b08d2..567e6eee0903 100644 --- a/webviews/src/agent-session/acpmux/direct.ts +++ b/webviews/src/agent-session/acpmux/direct.ts @@ -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) { diff --git a/webviews/src/agent-session/acpmux/subagents/subagentFold.ts b/webviews/src/agent-session/acpmux/subagents/subagentFold.ts index 502a97206b08..10b764e83096 100644 --- a/webviews/src/agent-session/acpmux/subagents/subagentFold.ts +++ b/webviews/src/agent-session/acpmux/subagents/subagentFold.ts @@ -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"]);