Skip to content

Commit 4d3d15b

Browse files
committed
fix(tui): no-op occupancy abort after bridge dispose
1 parent c280998 commit 4d3d15b

3 files changed

Lines changed: 44 additions & 5 deletions

File tree

‎src/subagent/fleet-dry-drive.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function shouldDriveOpenTasks(input: {
7777
);
7878
}
7979

80-
function isPromiseLike(value: unknown): value is Promise<unknown> {
80+
export function isPromiseLike(value: unknown): value is Promise<unknown> {
8181
return typeof value === "object" && value !== null && "then" in value;
8282
}
8383

‎src/tui/runtime-bridge.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,6 +2221,47 @@ describe("fleet-dry open-task drive (CL-7540)", () => {
22212221
);
22222222
});
22232223

2224+
test("pending occupancy abort after dispose does not idle or drain", async () => {
2225+
await withTestRenderer(
2226+
async (h) => {
2227+
const shell = createAppShell(h.renderer, {
2228+
terminal: { columns: 80, rows: 24 },
2229+
wireKeys: false,
2230+
run: "idle",
2231+
});
2232+
const port = createRecordingPort();
2233+
const bridge = attachSessionBridge(shell, port);
2234+
const prompt =
2235+
"The fleet has gone dry. Remaining open tasks:\n- t1: keep going (todo)\n";
2236+
let resolveDrive: ((ok: boolean) => void) | undefined;
2237+
bridge.setDryOpenTaskDriver(() => {
2238+
bridge.beginSystemContinuation(prompt);
2239+
return new Promise((resolve) => {
2240+
resolveDrive = resolve;
2241+
});
2242+
});
2243+
bridge.submit("dispatch workers", "immediate");
2244+
bridge.handle({ type: "fleet", running: 1 });
2245+
settleToollessTurn(bridge);
2246+
bridge.handle({ type: "fleet", running: 0 });
2247+
expect(shell.session.run).toBe("busy");
2248+
bridge.submit("when it finishes, summarize", "queue");
2249+
expect(badgeCount(shell.session)).toBe(1);
2250+
port.clear();
2251+
bridge.dispose();
2252+
const runAfterDispose = shell.session.run;
2253+
expect(runAfterDispose).toBe("busy");
2254+
resolveDrive?.(false);
2255+
await Promise.resolve();
2256+
expect(shell.session.run).toBe(runAfterDispose);
2257+
expect(badgeCount(shell.session)).toBe(1);
2258+
expect(port.calls.some((c) => c.op === "deliver")).toBe(false);
2259+
shell.dispose();
2260+
},
2261+
{ width: 80, height: 24 },
2262+
);
2263+
});
2264+
22242265
test("occupancy send abort does not swallow a later matching inbound", async () => {
22252266
await withTestRenderer(
22262267
async (h) => {

‎src/tui/runtime-bridge.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ import {
9999
pendingAskWakeText,
100100
type PendingAskWake,
101101
} from "../subagent/fleet-report.js";
102+
import { isPromiseLike } from "../subagent/fleet-dry-drive.js";
102103

103104
/** Tool name a sub-agent dispatch call carries — its row gets live progress. */
104105
const SPAWN_AGENT_TOOL_NAME = "spawn_agent";
@@ -1225,10 +1226,6 @@ function occupancyHold(bag: BridgeBag): boolean {
12251226
return bag.liveFleet > 0 || bag.awaitingContinuationInference;
12261227
}
12271228

1228-
function isPromiseLike(value: unknown): value is Promise<unknown> {
1229-
return typeof value === "object" && value !== null && "then" in value;
1230-
}
1231-
12321229
function releaseRunToIdle(shell: AppShell, bag: BridgeBag): void {
12331230
shell.session = setRunState(shell.session, "idle");
12341231
bag.awaitingContinuationInference = false;
@@ -1272,6 +1269,7 @@ function settleRunToIdle(shell: AppShell, bag: BridgeBag): void {
12721269
if (isPromiseLike(driven)) {
12731270
bag.droveOpenTasksThisDry = true;
12741271
const abortPendingDrive = (): void => {
1272+
if (bag.disposed) return;
12751273
bag.droveOpenTasksThisDry = false;
12761274
if (shell.session.run !== "busy") return;
12771275
bag.lastSentMessage = "";

0 commit comments

Comments
 (0)