Skip to content

Commit 2ba8e7f

Browse files
committed
fix(tui): flush mailbox mail before ask wake on settle and gate close
settleRunToIdle (idle-with-fleet) and gateClosed flushed the ask wake before mailbox mail, letting a re-surface wake steal the turn occupancy should take. Match the #1080 abort-path rule: mail first, wake only if mail did not start a turn (isProcessing guard, deliveredAskWake dedupe — no duplicate wake). Tests: 4 new CL-8061 order/suppression cases in agent-ask-wake.test.ts (failed pre-fix, pass post-fix). Fixes CL-8061
1 parent 810fd77 commit 2ba8e7f

2 files changed

Lines changed: 86 additions & 2 deletions

File tree

‎src/tui/agent-ask-wake.test.ts‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,86 @@ describe("agent ask wake delivery", () => {
405405
});
406406
});
407407

408+
test("gate close flushes mailbox mail before the ask wake (CL-8061)", async () => {
409+
await withWakeBridge((bridge, sends) => {
410+
const order: string[] = [];
411+
bridge.setOnAskWakeSent(() => {
412+
order.push("wake");
413+
});
414+
bridge.setMailboxMailDriver(() => {
415+
order.push("mail");
416+
return false;
417+
});
418+
bridge.gateOpened();
419+
bridge.handle({ type: "agent-ask", asks: [wake("a1", "q1")] });
420+
expect(sends).toEqual([]);
421+
bridge.gateClosed();
422+
expect(order).toEqual(["mail", "wake"]);
423+
expect(sends).toHaveLength(1);
424+
});
425+
});
426+
427+
test("gate close suppresses the wake when mail takes the turn (CL-8061)", async () => {
428+
await withWakeBridge((bridge, sends) => {
429+
const order: string[] = [];
430+
bridge.setOnAskWakeSent(() => {
431+
order.push("wake");
432+
});
433+
bridge.setMailboxMailDriver(() => {
434+
order.push("mail");
435+
bridge.beginSystemContinuation("mailbox occupancy");
436+
return true;
437+
});
438+
bridge.gateOpened();
439+
bridge.handle({ type: "agent-ask", asks: [wake("a1", "q1")] });
440+
expect(sends).toEqual([]);
441+
bridge.gateClosed();
442+
expect(order).toEqual(["mail"]);
443+
expect(sends).toEqual([]);
444+
});
445+
});
446+
447+
test("idle-with-fleet settle flushes mailbox mail before the ask wake (CL-8061)", async () => {
448+
await withWakeBridge((bridge, sends) => {
449+
const order: string[] = [];
450+
bridge.setOnAskWakeSent(() => {
451+
order.push("wake");
452+
});
453+
bridge.setMailboxMailDriver(() => {
454+
order.push("mail");
455+
return false;
456+
});
457+
bridge.handle({ type: "inference.start", data: {} });
458+
bridge.handle({ type: "fleet", running: 1 });
459+
bridge.handle({ type: "agent-ask", asks: [wake("a1", "q1")] });
460+
expect(sends).toEqual([]);
461+
bridge.handle({ type: "inference.done", data: {} });
462+
expect(order).toEqual(["mail", "wake"]);
463+
expect(sends).toHaveLength(1);
464+
});
465+
});
466+
467+
test("idle-with-fleet settle suppresses the wake when mail takes the turn (CL-8061)", async () => {
468+
await withWakeBridge((bridge, sends) => {
469+
const order: string[] = [];
470+
bridge.setOnAskWakeSent(() => {
471+
order.push("wake");
472+
});
473+
bridge.setMailboxMailDriver(() => {
474+
order.push("mail");
475+
bridge.beginSystemContinuation("mailbox occupancy");
476+
return true;
477+
});
478+
bridge.handle({ type: "inference.start", data: {} });
479+
bridge.handle({ type: "fleet", running: 1 });
480+
bridge.handle({ type: "agent-ask", asks: [wake("a1", "q1")] });
481+
expect(sends).toEqual([]);
482+
bridge.handle({ type: "inference.done", data: {} });
483+
expect(order).toEqual(["mail"]);
484+
expect(sends).toEqual([]);
485+
});
486+
});
487+
408488
test("clearQueuedDelivery drops stashed wakes and resets delivered identities", async () => {
409489
await withWakeBridge((bridge, sends) => {
410490
bridge.handle({ type: "inference.start", data: {} });

‎src/tui/runtime-bridge.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,9 +1308,11 @@ function settleRunToIdle(shell: AppShell, bag: BridgeBag): void {
13081308
// Hold: the fleet is still live, so the run stays busy. Steers left
13091309
// pending send now — the parent they were steering has stopped, so
13101310
// each one starts its own turn — while follow-ups keep waiting.
1311+
// Occupancy/mailbox first so a re-surface wake cannot steal that turn;
1312+
// the trailing wake flush restates only if mail did not start one.
13111313
drainSteersAtBoundary(shell, bag);
1312-
bag.flushPendingAskWake?.();
13131314
bag.flushMailboxMail?.();
1315+
if (!bag.turn.isProcessing) bag.flushPendingAskWake?.();
13141316
return;
13151317
}
13161318
if (!bag.droveOpenTasksThisDry) {
@@ -1993,8 +1995,10 @@ export function attachSessionBridge(
19931995
if (bag.disposed) return;
19941996
bag.turn = turnStateGateClosed(bag.turn, now());
19951997
paintPhase();
1996-
flushPendingAskWake();
1998+
// Occupancy/mailbox first so a re-surface wake cannot steal that turn;
1999+
// the trailing wake flush restates only if mail did not start one.
19972000
flushMailboxMail();
2001+
if (!bag.turn.isProcessing) flushPendingAskWake();
19982002
};
19992003

20002004
const tick = (): void => {

0 commit comments

Comments
 (0)