diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 646eefc6eb2d..e0dd2c4fb549 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -5212,6 +5212,50 @@ function getOauthSession() { return oauthSession } +// Cold-start cookie-jar warm-up. A `persist:` partition materialized via +// session.fromPartition() loads its on-disk cookie store LAZILY: the very first +// cookies.get() on a fresh cold start can resolve BEFORE the jar has finished +// hydrating from disk and return an empty array — even though the user is +// signed in. That false-negative used to make hasLiveOauthSession() report +// "not signed in", which on the initial boot path (startHermes → the renderer's +// single-shot boot() with no retry) surfaced as the "Hermes couldn't start" +// OAuth overlay that vanishes the instant the user clicks Retry. +// +// We force the store to hydrate once, up front: flushStorageData() then a +// throwaway cookies.get(). The promise is memoized so every caller awaits the +// same single warm-up. Best-effort — any error resolves so we fall back to the +// live read (which then does its own bounded re-check). +let oauthCookieWarmup: Promise | null = null + +function warmOauthCookieStore() { + if (oauthCookieWarmup) { + return oauthCookieWarmup + } + + oauthCookieWarmup = (async () => { + const sess = getOauthSession() + + if (!sess) { + // App not ready yet — don't memoize a no-op; let a later call retry. + oauthCookieWarmup = null + + return + } + + try { + // flushStorageData() forces Chromium to reconcile the in-memory cookie + // monster with the on-disk SQLite store; the subsequent get() then reads + // a populated jar rather than racing the lazy first-access load. + sess.flushStorageData?.() + await sess.cookies.get({}) + } catch { + // Best effort; the real read below re-checks with bounded retries. + } + })() + + return oauthCookieWarmup +} + // Bare + prefixed variants of the session cookies live in // connection-config.ts (cookiesHaveSession / cookiesHaveLiveSession). See // that module for details. @@ -5258,19 +5302,45 @@ async function hasLiveOauthSession(baseUrl) { const parsed = new URL(baseUrl) - try { - const cookies = await sess.cookies.get({ url: baseUrl }) - - return cookiesHaveLiveSession(cookies) - } catch { + const readLive = async () => { try { - const cookies = await sess.cookies.get({ domain: parsed.hostname }) + const cookies = await sess.cookies.get({ url: baseUrl }) return cookiesHaveLiveSession(cookies) } catch { - return false + try { + const cookies = await sess.cookies.get({ domain: parsed.hostname }) + + return cookiesHaveLiveSession(cookies) + } catch { + return false + } } } + + // First read against the (possibly still-hydrating) jar. + if (await readLive()) { + return true + } + + // Cold-start false-negative guard. A `persist:` partition's cookie store + // loads lazily, so the FIRST read on a fresh boot can come back empty even + // for a signed-in user — the exact race that produced the transient "Hermes + // couldn't start / not signed in" overlay that Retry always cleared. Before + // trusting a negative, force the store to hydrate and re-read a couple of + // times with a short backoff. A genuinely signed-out user still resolves + // false quickly (≤ ~180ms); a signed-in user racing the load now wins. + await warmOauthCookieStore() + + for (const delayMs of [30, 60, 90]) { + if (await readLive()) { + return true + } + + await new Promise(resolve => setTimeout(resolve, delayMs)) + } + + return readLive() } async function clearOauthSession(baseUrl) {