From 2a4460b58be1bda7b0ece896f38e5b2ccffaac8f Mon Sep 17 00:00:00 2001 From: Leonid Skorobogatyy Date: Wed, 22 Jul 2026 00:32:06 +1100 Subject: [PATCH] fix(desktop): guard gateway-open resume against active fresh-draft transition (#68594) When switching profiles, useRouteResume() could treat the target profile gateway opening as a resume command for the old routed session before React Router commits the /new pathname. The gatewayBecameOpen trigger was not guarded by the existing freshDraftReady discriminator, unlike the stuckOnRoutedSession guard on line 137 which already uses it. Fix: add && !freshDraftReady to the gatewayBecameOpen condition in shouldResume (line 143). This is consistent with the existing pattern and preserves reconnect behavior (freshDraftReady=false during normal reconnects). Adds a regression test that simulates the exact race: profile switch clears refs and sets freshDraftReady=true, gateway closes, then profile B gateway opens while pathname is still /session-a. Asserts no resume fires. --- .../session/hooks/use-route-resume.test.tsx | 73 +++++++++++++++++++ .../src/app/session/hooks/use-route-resume.ts | 3 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx b/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx index b7bdead6bb..03e4891d3e 100644 --- a/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx @@ -389,6 +389,78 @@ describe('useRouteResume', () => { expect(resumeSession).toHaveBeenCalledTimes(1) expect(resumeSession).toHaveBeenCalledWith('session-2', true) }) + + it("does not re-resume the old session when the new profile gateway opens before /new commits (#68594)", () => { + const resumeSession = vi.fn(async () => undefined) + const startFreshSessionDraft = vi.fn() + const activeSessionIdRef: MutableRefObject = { current: "runtime-a" } + const creatingSessionRef = { current: false } + const runtimeIdByStoredSessionIdRef = { current: new Map([["session-a", "runtime-a"]]) } + const selectedStoredSessionIdRef: MutableRefObject = { current: "session-a" } + + const { rerender } = render( + + ) + + expect(resumeSession).not.toHaveBeenCalled() + + // Profile switch: clear refs, set freshDraftReady, close profile A gateway. + activeSessionIdRef.current = null + selectedStoredSessionIdRef.current = null + rerender( + + ) + + // Profile B gateway opens before React Router commits /new. + rerender( + + ) + + // Must NOT resume session-a: the fresh draft transition is active. + expect(resumeSession).not.toHaveBeenCalled() + }) }) describe('useRouteResume bounded auto-retry after a failed resume', () => { @@ -602,3 +674,4 @@ describe('useRouteResume bounded auto-retry after a failed resume', () => { expect($resumeExhaustedSessionId.get()).toBeNull() }) }) + diff --git a/apps/desktop/src/app/session/hooks/use-route-resume.ts b/apps/desktop/src/app/session/hooks/use-route-resume.ts index ee135d3303..631f10e007 100644 --- a/apps/desktop/src/app/session/hooks/use-route-resume.ts +++ b/apps/desktop/src/app/session/hooks/use-route-resume.ts @@ -156,7 +156,8 @@ export function useRouteResume({ // we're stranded on a routed session that never loaded. The first two // guard against a transient /:sid re-resume during "new chat" state clears // before the pathname updates from /:sid -> /. - const shouldResume = pathnameChanged || gatewayBecameOpen || stuckOnRoutedSession || explicitlyRequested + const shouldResume = + pathnameChanged || (gatewayBecameOpen && !freshDraftReady) || stuckOnRoutedSession || explicitlyRequested // On a reconnect (gatewayBecameOpen) re-resume even when the route looks // `alreadyActive`: the cached runtime id can be stale once the gateway