diff --git a/src/tui/runner/reconnect-recovery.test.ts b/src/tui/runner/reconnect-recovery.test.ts index 74fe02e2e..9cb5b38fc 100644 --- a/src/tui/runner/reconnect-recovery.test.ts +++ b/src/tui/runner/reconnect-recovery.test.ts @@ -197,6 +197,34 @@ describe("reconnect recovery selection", () => { expect(delivered).toHaveLength(1); }); + test("does not replay when the accepted generation becomes stale", () => { + const state = createReconnectRecoveryState(); + const attempt = state.begin(operatorMessage(), "xai"); + state.observe(attempt, credentialRetry()); + state.observe(attempt, credentialFailure("xai/default-2")); + const pending = required(state.settle(attempt), "pending recovery"); + + let completeReconnect: ((connected: boolean) => void) | undefined; + applyReconnectRecoverySelection({ + state, + generation: pending.generation, + selectedId: reconnectRecoveryItemId(pending.scope), + reconnect: (_scope, onComplete) => { + completeReconnect = onComplete; + }, + armContinuation: () => { + throw new Error("should not arm"); + }, + cancelContinuation: () => undefined, + deliverContinuation: () => { + throw new Error("should not deliver"); + }, + }); + + state.begin(operatorMessage(), "anthropic"); + completeReconnect?.(true); + }); + test("cancel, failure, and committed attempts never replay", () => { for (const connected of [false, true]) { const state = createReconnectRecoveryState(); diff --git a/src/tui/runner/reconnect-recovery.ts b/src/tui/runner/reconnect-recovery.ts index 6c5b5c28e..76c24925c 100644 --- a/src/tui/runner/reconnect-recovery.ts +++ b/src/tui/runner/reconnect-recovery.ts @@ -183,6 +183,9 @@ export function createReconnectRecoveryState() { replay: !claimed.committed, }; }, + isCurrent(generation: number): boolean { + return generation === nextGeneration; + }, clear(): void { pending = null; nextGeneration++; @@ -208,7 +211,13 @@ export function applyReconnectRecoverySelection(args: { const onComplete = (connected: boolean): void => { if (completed) return; completed = true; - if (!connected || !acceptance.replay) return; + if ( + !connected || + !acceptance.replay || + !args.state.isCurrent(acceptance.generation) + ) { + return; + } args.armContinuation(acceptance.generation); try { args.deliverContinuation(