From ca8c78e588b700e865227de2d2cc5ba1d437f40c Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Fri, 5 Jun 2026 07:52:44 -0500 Subject: [PATCH] fix(desktop): heal stale runtime-id cache + model on profile switch (#39819) Two switch-time regressions from the multi-profile rail work: - "Session not found" (4007): pruneSecondaryGateways idle-reaps a non-active profile's backend; switching back respawns a *fresh* backend that mints new runtime ids, but runtimeIdByStoredSessionId is never pruned. resumeSession's cache fast-path then makes a dead runtime id active and returns, so session.usage + the next prompt 404. Probe the cached id; on rejection drop the stale mapping and fall through to a full resume that rebinds a live id. - "Forgets the LLM setting": $currentModel is a nanostore set only by refreshCurrentModel (gatewayState->open, etc). A swap fires invalidateQueries() (react-query only) and keeps the socket 'open', so the model/pill kept showing the previous profile. Re-pull both when $activeGatewayProfile changes. --- apps/desktop/src/app/desktop-controller.tsx | 21 +++++++++++- .../app/session/hooks/use-session-actions.ts | 32 ++++++++++++++----- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 00c1d8fa59a..e5c193ba630 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -29,7 +29,7 @@ import { unpinSession } from '../store/layout' import { $filePreviewTarget, $previewTarget, closeActiveRightRailTab } from '../store/preview' -import { $freshSessionRequest, normalizeProfileKey, refreshActiveProfile } from '../store/profile' +import { $activeGatewayProfile, $freshSessionRequest, normalizeProfileKey, refreshActiveProfile } from '../store/profile' import { $activeSessionId, $currentCwd, @@ -506,6 +506,25 @@ export function DesktopController() { startFreshSessionDraft() }, [freshSessionRequest, startFreshSessionDraft]) + // Swapping the live gateway to another profile must re-pull that profile's + // global model + active-profile pill. Both are nanostores, so the blanket + // invalidateQueries() the profile store fires on swap doesn't touch them — + // without this the statusbar keeps showing the previous profile's model + // (the "forgets the LLM setting" report). gatewayState stays 'open' across a + // swap (background sockets persist), so the open→open effect won't re-run. + const activeGatewayProfile = useStore($activeGatewayProfile) + const lastGatewayProfileRef = useRef(activeGatewayProfile) + + useEffect(() => { + if (activeGatewayProfile === lastGatewayProfileRef.current) { + return + } + + lastGatewayProfileRef.current = activeGatewayProfile + void refreshCurrentModel() + void refreshActiveProfile() + }, [activeGatewayProfile, refreshCurrentModel]) + const composer = useComposerActions({ activeSessionId, currentCwd, diff --git a/apps/desktop/src/app/session/hooks/use-session-actions.ts b/apps/desktop/src/app/session/hooks/use-session-actions.ts index 3a6251b75f9..bdd9dd15b87 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions.ts @@ -453,15 +453,31 @@ export function useSessionActions({ clearComposerDraft() clearComposerAttachments() - void requestGateway('session.usage', { session_id: cachedRuntimeId }) - .then(usage => { - if (isCurrentResume() && usage) { - setCurrentUsage(current => ({ ...current, ...usage })) - } - }) - .catch(() => undefined) + try { + const usage = await requestGateway('session.usage', { session_id: cachedRuntimeId }) - return + if (!isCurrentResume()) { + return + } + + if (usage) { + setCurrentUsage(current => ({ ...current, ...usage })) + } + + return + } catch { + // The cached runtime id was minted by a prior backend instance. A + // pooled profile backend that gets idle-reaped (pruneSecondaryGateways) + // and respawned across a profile swap mints fresh ids, so this mapping + // now 404s ("session not found"). Drop it and fall through to a full + // resume that rebinds a live runtime id. + if (!isCurrentResume()) { + return + } + + runtimeIdByStoredSessionIdRef.current.delete(storedSessionId) + sessionStateByRuntimeIdRef.current.delete(cachedRuntimeId) + } } setFreshDraftReady(false)