From 8db59d08f45ba6285908be554da14e96ce5b3643 Mon Sep 17 00:00:00 2001 From: Andrew Fiebert Date: Mon, 10 Aug 2026 09:45:10 -0400 Subject: [PATCH] fix(desktop): wait 5m before reconnect warning toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brief transport blips often self-heal in 1–3 minutes. Raise the non-blocking escalate toast from 45s to 5m so those windows stay quiet while chat remains readable/draftable. Confirmed reauth still takes the full-screen recovery overlay immediately. --- .../gateway/hooks/use-gateway-boot.test.tsx | 7 ++++--- .../src/app/gateway/hooks/use-gateway-boot.ts | 19 +++++++++++-------- .../gateway-connecting-overlay.test.tsx | 12 ++++++------ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx index c2ef783e82..cf6e6d618d 100644 --- a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +++ b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx @@ -372,8 +372,9 @@ describe('useGatewayBoot remote reconnect loop (real hook, fake socket)', () => act(() => FakeWebSocket.instances[0].drop()) await flushAsync() - // Walk the backoff past the ~45s escalation threshold. - for (let i = 0; i < 8; i += 1) { + // Walk the backoff well past the historical 45s threshold and into the + // current multi-minute escalate window. Chat must stay unlocked either way. + for (let i = 0; i < 24; i += 1) { await advanceBackoff() } @@ -393,7 +394,7 @@ describe('useGatewayBoot remote reconnect loop (real hook, fake socket)', () => act(() => FakeWebSocket.instances[0].drop()) await flushAsync() - for (let i = 0; i < 8; i += 1) { + for (let i = 0; i < 24; i += 1) { await advanceBackoff() } diff --git a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts index 3550047f11..c52cb4040c 100644 --- a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +++ b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts @@ -64,11 +64,15 @@ import { stashGatewaySurvivor, survivorIsStale, takeGatewaySurvivor } from './ga // After the reconnect loop has been failing for this long, raise a NON-blocking // warning toast. Full-screen BootFailureOverlay used to lock the user out of -// reading/drafting for the whole 1–3 minute blip even though the transcript is -// still on screen underneath. Confirmed reauth still escalates to the overlay -// (Sign in is required). Time-based (not attempt-count) because full-jitter -// backoff makes attempt counts a meaningless clock. -const RECONNECT_ESCALATE_AFTER_MS = 45_000 +// reading/drafting for the whole blip even though the transcript is still on +// screen underneath. Confirmed reauth still escalates to the overlay (Sign in +// is required). Time-based (not attempt-count) because full-jitter backoff +// makes attempt counts a meaningless clock. +// +// 5 minutes (not the historical ~45s) so brief transport weather — ticket mint +// flaps, sleep/wake, Wi‑Fi blips that self-heal in 1–3 minutes — never even +// toast. Chat stays readable/draftable the whole time either way. +const RECONNECT_ESCALATE_AFTER_MS = 300_000 // Bounded self-heal for a failed REMOTE boot (#82679): when the primary boot // fails on a transient remote fault (dropped SSH/HTTP registered connection, @@ -158,9 +162,8 @@ export function useGatewayBoot({ // identical error toasts (and their haptics). Reset on the next clean open. let reauthNotified = false // Raised once the reconnect loop has been failing for - // RECONNECT_ESCALATE_AFTER_MS so the recovery overlay replaces the - // dead-end CONNECTING screen. Reset on a clean open or a manual/ - // wake-driven reconnect. + // RECONNECT_ESCALATE_AFTER_MS so we fire a single non-blocking toast. + // Reset on a clean open or a manual/wake-driven reconnect. let escalated = false // Bounded automatic boot retry for transient REMOTE failures (#82679). let bootRetryAttempt = 0 diff --git a/apps/desktop/src/components/gateway-connecting-overlay.test.tsx b/apps/desktop/src/components/gateway-connecting-overlay.test.tsx index a63fd1c72b..66026b0a55 100644 --- a/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +++ b/apps/desktop/src/components/gateway-connecting-overlay.test.tsx @@ -170,14 +170,14 @@ describe('connecting overlay vs recovery surface', () => { expect(isRecoveryShown()).toBe(false) }) - it('FIX: once the prolonged reconnect raises a recoverable boot error, the recovery overlay takes over', async () => { - // Mirrors what useGatewayBoot.scheduleReconnect() now does after ~45s of - // failed post-boot reconnects: it calls failDesktopBoot(), flipping the UI - // from the dead-end CONNECTING overlay to the recovery surface. + it('FIX: confirmed reauth boot.error still surfaces the recovery overlay (not CONNECTING)', async () => { + // Transport blips no longer set boot.error (toast + background retry). + // Confirmed OAuth reauth still does — and that recovery surface must win + // over any residual connecting state so Sign-in / Gateway settings stay reachable. setGatewayState('error') $desktopBoot.set({ ...$desktopBoot.get(), - error: 'Lost connection to the Hermes gateway and could not reconnect.', + error: 'Your remote gateway session has expired. Sign in again.', running: false, visible: true }) @@ -191,7 +191,7 @@ describe('connecting overlay vs recovery surface', () => { ) }) - // Escape hatch is now reachable; the connecting overlay bows out. + // Escape hatch is reachable; the connecting overlay bows out. expect(isRecoveryShown()).toBe(true) expect(screen.getByRole('button', { name: /gateway settings/i })).toBeTruthy() expect(isConnectingShown()).toBe(false)