Skip to content

Commit 4f06ddb

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 2bc969c commit 4f06ddb

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
@@ -221,7 +221,8 @@ export interface SessionBridge {
221221
*/
222222
beginSystemContinuation: (text: string) => void;
223223
/**
224-
* Occupancy send failed after beginSystemContinuation. Re-arm the dry-open
224+
* Occupancy send failed after beginSystemContinuation. Drop the occupancy
225+
* echo so a later matching inbound is not swallowed, re-arm the dry-open
225226
* latch, drop the continuation hold, and idle so follow-ups can drain and a
226227
* later settle can take another occupancy shot.
227228
*/
@@ -1617,6 +1618,18 @@ export function attachSessionBridge(
16171618
},
16181619
abortSystemContinuation: () => {
16191620
if (bag.disposed) return;
1621+
if (bag.awaitingContinuationInference) {
1622+
const occupancy = bag.lastSentMessage;
1623+
if (occupancy.length > 0) {
1624+
const last = bag.pendingEchoes.length - 1;
1625+
if (last >= 0 && bag.pendingEchoes[last] === occupancy) {
1626+
bag.pendingEchoes.pop();
1627+
} else {
1628+
const index = bag.pendingEchoes.lastIndexOf(occupancy);
1629+
if (index !== -1) bag.pendingEchoes.splice(index, 1);
1630+
}
1631+
}
1632+
}
16201633
bag.awaitingContinuationInference = false;
16211634
bag.pendingDryOpenDrive = true;
16221635
bag.lastSentMessage = "";

0 commit comments

Comments
 (0)