From 928f1ac0e187a5d2ef4f397dbbc1f2edfc98201e Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 4 Jun 2026 02:28:43 -0700 Subject: [PATCH] fix(desktop): re-mint OAuth WS ticket on gateway reconnect (#38886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit attemptReconnect() connected with the stale cached conn.wsUrl. OAuth WS tickets are single-use with a ~30s TTL, so the first sign-in (which goes through boot() and re-mints via resolveGatewayWsUrl) succeeds, but every reconnect (sleep/wake, network online, window refocus, socket drop, app restart) reused a dead ticket and failed the WS upgrade with an opaque "Could not connect to Hermes gateway" — even though backend resolution (cookie + REST) reported ready. attemptReconnect now mints a fresh ticket before connecting, mirroring use-gateway-request.ts, and surfaces the reauth "sign in again" message once on OAuth expiry instead of silently looping backoff against a dead ticket. Local/token gateways are unaffected (re-mint is a no-op). --- .../src/app/gateway/hooks/use-gateway-boot.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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 6875c445f..14b229c2b 100644 --- a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +++ b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts @@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react' import type { HermesConnection } from '@/global' import { HermesGateway } from '@/hermes' -import { resolveGatewayWsUrl } from '@/lib/gateway-ws-url' +import { isGatewayReauthRequired, resolveGatewayWsUrl } from '@/lib/gateway-ws-url' import { $desktopBoot, applyDesktopBootProgress, @@ -104,7 +104,15 @@ export function useGatewayBoot({ } publish(conn) - await gateway.connect(conn.wsUrl) + // Re-mint the WS URL before reconnecting. OAuth tickets are single-use + // with a short TTL, so the ticket baked into the cached conn.wsUrl is + // dead on every reconnect after the initial boot — reusing it surfaces + // as an opaque "Could not connect to Hermes gateway". resolveGatewayWsUrl + // mints a fresh ticket (or throws a reauth error in OAuth mode rather + // than connecting with a stale one). For local/token gateways the URL + // carries a long-lived token and the re-mint is a cheap no-op. + const wsUrl = await resolveGatewayWsUrl(desktop, conn) + await gateway.connect(wsUrl) if (cancelled) { return @@ -114,8 +122,14 @@ export function useGatewayBoot({ // Resync state that may have moved on the backend while we were asleep. await callbacksRef.current.refreshHermesConfig().catch(() => undefined) await callbacksRef.current.refreshSessions().catch(() => undefined) - } catch { - // Fall through to scheduleReconnect's backoff below. + } catch (err) { + // OAuth session expired mid-reconnect: surface the actionable "sign in + // again" message once instead of silently looping the backoff against a + // ticket that can never succeed. Transport failures fall through to the + // backoff in the finally block below. + if (!cancelled && isGatewayReauthRequired(err)) { + notifyError(err, 'Gateway sign-in required') + } } finally { reconnecting = false