Skip to content

Commit bb203f8

Browse files
committed
Drop the occupancy continuation echo when the send aborts
beginSystemContinuation queues the occupancy string on pendingEchoes. Abort without a matching message.received left that echo in place, so a later inbound with the same text could be swallowed. Pop that continuation echo on abort without wiping unrelated operator echoes.
1 parent 4292aee commit bb203f8

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

src/tui/runtime-bridge.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,6 +1626,48 @@ describe("fleet-dry open-task drive (CL-7540)", () => {
16261626
);
16271627
});
16281628

1629+
test("occupancy send abort drops the continuation echo so a later matching inbound paints", async () => {
1630+
await withTestRenderer(
1631+
async (h) => {
1632+
const shell = createAppShell(h.renderer, {
1633+
terminal: { columns: 80, rows: 24 },
1634+
wireKeys: false,
1635+
run: "idle",
1636+
});
1637+
const port = createRecordingPort();
1638+
const bridge = attachSessionBridge(shell, port);
1639+
try {
1640+
const occupancy =
1641+
"The fleet has gone dry. Remaining open tasks:\n- t1: keep going (todo)\n";
1642+
const operator = "dispatch workers";
1643+
bridge.submit(operator, "immediate");
1644+
const userRowsAfterSubmit = shell.streamLog.filter((r) => r.role === "user").length;
1645+
bridge.beginSystemContinuation(occupancy);
1646+
bridge.abortSystemContinuation();
1647+
expect(shell.session.run).toBe("idle");
1648+
1649+
bridge.handle({
1650+
type: "message.received",
1651+
data: { message: { content: operator } },
1652+
});
1653+
expect(shell.streamLog.filter((r) => r.role === "user").length).toBe(userRowsAfterSubmit);
1654+
1655+
bridge.handle({
1656+
type: "message.received",
1657+
data: { message: { content: occupancy } },
1658+
});
1659+
expect(shell.streamLog.filter((r) => r.role === "user").length).toBe(
1660+
userRowsAfterSubmit + 1,
1661+
);
1662+
} finally {
1663+
bridge.dispose();
1664+
shell.dispose();
1665+
}
1666+
},
1667+
{ width: 80, height: 24 },
1668+
);
1669+
});
1670+
16291671
test("occupancy send abort resets the turn without a following reply", async () => {
16301672
await withTestRenderer(
16311673
async (h) => {

src/tui/runtime-bridge.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ export interface SessionBridge {
220220
*/
221221
beginSystemContinuation: (text: string) => void;
222222
/**
223-
* Occupancy send failed after beginSystemContinuation. Re-arm the dry-open
223+
* Occupancy send failed after beginSystemContinuation. Drop the occupancy
224+
* echo so a later matching inbound is not swallowed, re-arm the dry-open
224225
* latch, drop the continuation hold, and idle so follow-ups can drain and a
225226
* later settle can take another occupancy shot.
226227
*/
@@ -1551,6 +1552,18 @@ export function attachSessionBridge(
15511552
},
15521553
abortSystemContinuation: () => {
15531554
if (bag.disposed) return;
1555+
if (bag.awaitingContinuationInference) {
1556+
const occupancy = bag.lastSentMessage;
1557+
if (occupancy.length > 0) {
1558+
const last = bag.pendingEchoes.length - 1;
1559+
if (last >= 0 && bag.pendingEchoes[last] === occupancy) {
1560+
bag.pendingEchoes.pop();
1561+
} else {
1562+
const index = bag.pendingEchoes.lastIndexOf(occupancy);
1563+
if (index !== -1) bag.pendingEchoes.splice(index, 1);
1564+
}
1565+
}
1566+
}
15541567
bag.awaitingContinuationInference = false;
15551568
bag.pendingDryOpenDrive = true;
15561569
bag.lastSentMessage = "";

0 commit comments

Comments
 (0)