Skip to content

Commit 37c3c1f

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 dfeac6d commit 37c3c1f

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
@@ -1322,9 +1322,11 @@ function settleRunToIdle(shell: AppShell, bag: BridgeBag): void {
13221322
// Hold: the fleet is still live, so the run stays busy. Steers left
13231323
// pending send now — the parent they were steering has stopped, so
13241324
// each one starts its own turn — while follow-ups keep waiting.
1325+
// Occupancy/mailbox first so a re-surface wake cannot steal that turn;
1326+
// the trailing wake flush restates only if mail did not start one.
13251327
drainSteersAtBoundary(shell, bag);
1326-
bag.flushPendingAskWake?.();
13271328
bag.flushMailboxMail?.();
1329+
if (!bag.turn.isProcessing) bag.flushPendingAskWake?.();
13281330
return;
13291331
}
13301332
if (!bag.droveOpenTasksThisDry) {
@@ -2032,8 +2034,10 @@ export function attachSessionBridge(
20322034
if (bag.disposed) return;
20332035
bag.turn = turnStateGateClosed(bag.turn, now());
20342036
paintPhase();
2035-
flushPendingAskWake();
2037+
// Occupancy/mailbox first so a re-surface wake cannot steal that turn;
2038+
// the trailing wake flush restates only if mail did not start one.
20362039
flushMailboxMail();
2040+
if (!bag.turn.isProcessing) flushPendingAskWake();
20372041
};
20382042

20392043
const tick = (): void => {

0 commit comments

Comments
 (0)