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.
This commit is contained in:
Leonid Skorobogatyy
2026-07-22 00:32:06 +11:00
committed by Teknium
parent 4b21f970aa
commit 2a4460b58b
2 changed files with 75 additions and 1 deletions
@@ -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<null | string> = { current: "runtime-a" }
const creatingSessionRef = { current: false }
const runtimeIdByStoredSessionIdRef = { current: new Map([["session-a", "runtime-a"]]) }
const selectedStoredSessionIdRef: MutableRefObject<null | string> = { current: "session-a" }
const { rerender } = render(
<RouteResumeHarness
activeSessionId="runtime-a"
activeSessionIdRef={activeSessionIdRef}
creatingSessionRef={creatingSessionRef}
currentView="chat"
freshDraftReady={false}
gatewayState="open"
locationPathname="/session-a"
resumeSession={resumeSession}
routedSessionId="session-a"
runtimeIdByStoredSessionIdRef={runtimeIdByStoredSessionIdRef}
selectedStoredSessionId="session-a"
selectedStoredSessionIdRef={selectedStoredSessionIdRef}
startFreshSessionDraft={startFreshSessionDraft}
/>
)
expect(resumeSession).not.toHaveBeenCalled()
// Profile switch: clear refs, set freshDraftReady, close profile A gateway.
activeSessionIdRef.current = null
selectedStoredSessionIdRef.current = null
rerender(
<RouteResumeHarness
activeSessionId={null}
activeSessionIdRef={activeSessionIdRef}
creatingSessionRef={creatingSessionRef}
currentView="chat"
freshDraftReady
gatewayState="closed"
locationPathname="/session-a"
resumeSession={resumeSession}
routedSessionId="session-a"
runtimeIdByStoredSessionIdRef={runtimeIdByStoredSessionIdRef}
selectedStoredSessionId={null}
selectedStoredSessionIdRef={selectedStoredSessionIdRef}
startFreshSessionDraft={startFreshSessionDraft}
/>
)
// Profile B gateway opens before React Router commits /new.
rerender(
<RouteResumeHarness
activeSessionId={null}
activeSessionIdRef={activeSessionIdRef}
creatingSessionRef={creatingSessionRef}
currentView="chat"
freshDraftReady
gatewayState="open"
locationPathname="/session-a"
resumeSession={resumeSession}
routedSessionId="session-a"
runtimeIdByStoredSessionIdRef={runtimeIdByStoredSessionIdRef}
selectedStoredSessionId={null}
selectedStoredSessionIdRef={selectedStoredSessionIdRef}
startFreshSessionDraft={startFreshSessionDraft}
/>
)
// 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()
})
})
@@ -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