diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts index 63d349ad1379..1e1e37405d08 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts @@ -1,5 +1,5 @@ -import type { MutableRefObject } from 'react' -import { useCallback, useRef } from 'react' +import { useStore } from '@nanostores/react' +import { type MutableRefObject, useCallback, useEffect, useRef } from 'react' import type { NavigateFunction } from 'react-router-dom' import { revealTreePane } from '@/components/pane-shell/tree/store' @@ -13,6 +13,7 @@ import { clearNotifications, notify, notifyError } from '@/store/notifications' import { $activeGatewayProfile, $newChatProfile, ensureGatewayProfile, normalizeProfileKey } from '@/store/profile' import { resolveNewSessionCwd, tombstoneSessions, untombstoneSessions } from '@/store/projects' import { + $activeSessionStoredId, $currentCwd, $currentFastMode, $currentModel, @@ -177,6 +178,33 @@ export function useSessionActions({ const copy = t.desktop const resumeRequestRef = useRef(0) + // Follow auto-compression's stored-id rotation. When the active session's + // stored id changes (compression ends the SessionDB session and forks a + // continuation), re-anchor the URL route + selection to the new id so the + // next send doesn't hit a stale stored→runtime mapping and trigger a full + // thread reload. replace: true — it's the same conversation, not a new + // history entry. + const rotatedStoredId = useStore($activeSessionStoredId) + + useEffect(() => { + if (!rotatedStoredId || rotatedStoredId === selectedStoredSessionIdRef.current) { + return + } + + const oldStoredId = selectedStoredSessionIdRef.current + + setSelectedStoredSessionId(rotatedStoredId) + selectedStoredSessionIdRef.current = rotatedStoredId + navigate(sessionRoute(rotatedStoredId), { replace: true }) + + // Clean up the stale stored→runtime mapping so getRuntimeIdForStoredSession + // can't resolve the old id to this runtime (it would fail the storedSessionId + // check and return null, but leaving the stale key is sloppy). + if (oldStoredId) { + runtimeIdByStoredSessionIdRef.current.delete(oldStoredId) + } + }, [rotatedStoredId, navigate, runtimeIdByStoredSessionIdRef, selectedStoredSessionIdRef]) + const startFreshSessionDraft = useCallback( (options: boolean | FreshSessionDraftOptions = false) => { const draftOptions = typeof options === 'boolean' ? { replaceRoute: options } : options diff --git a/apps/desktop/src/app/session/hooks/use-session-state-cache.ts b/apps/desktop/src/app/session/hooks/use-session-state-cache.ts index 3e59dd972709..75b535835acc 100644 --- a/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +++ b/apps/desktop/src/app/session/hooks/use-session-state-cache.ts @@ -6,10 +6,12 @@ import { preserveLocalAssistantErrors } from '@/lib/chat-messages' import { createClientSessionState } from '@/lib/chat-runtime' import { setMutableRef } from '@/lib/mutable-ref' import { + $activeSessionId, $busy, $messages, noteSessionActivity, onSessionWatchdogClear, + setActiveSessionStoredId, setCurrentFastMode, setCurrentModel, setCurrentPersonality, @@ -115,6 +117,15 @@ export function useSessionStateCache({ if (previousStoredSessionId && previousStoredSessionId !== storedSessionId) { setSessionWorking(previousStoredSessionId, false) + + // Auto-compression rotated the stored id on the active session. Signal + // the route-following effect in use-session-actions so the URL + selection + // re-anchor to the continuation id — otherwise the next send hits a stale + // stored→runtime mapping (getRuntimeIdForStoredSession returns null) and + // triggers a full thread reload via resumeStoredSession. + if (sessionId === $activeSessionId.get()) { + setActiveSessionStoredId(storedSessionId) + } } } diff --git a/apps/desktop/src/store/session.ts b/apps/desktop/src/store/session.ts index 78c66618e58b..b735b454d7bf 100644 --- a/apps/desktop/src/store/session.ts +++ b/apps/desktop/src/store/session.ts @@ -248,6 +248,13 @@ export const $sessionsLoading = atom(true) export const $workingSessionIds = atom([]) export const $activeSessionId = atom(null) export const $selectedStoredSessionId = atom(null) +// Reactive signal for when the active session's stored id rotates (auto- +// compression ends the SessionDB session and forks a continuation). The +// route + selection must follow the rotation so the next send doesn't +// trigger a full thread reload (getRuntimeIdForStoredSession would return +// null for the old stored id, forcing resumeStoredSession). Set in +// ensureSessionState when the cache entry's storedSessionId changes. +export const $activeSessionStoredId = atom(null) export const $messages = atom([]) // Streaming-stable derivations of $messages. During a token stream the array @@ -321,6 +328,8 @@ export const setSessionProfileTotals = (next: Updater>) = export const setSessionsLoading = (next: Updater) => updateAtom($sessionsLoading, next) export const setWorkingSessionIds = (next: Updater) => updateAtom($workingSessionIds, next) export const setActiveSessionId = (next: Updater) => updateAtom($activeSessionId, next) +export const setActiveSessionStoredId = (next: Updater) => + updateAtom($activeSessionStoredId, next) export const setSelectedStoredSessionId = (next: Updater) => { updateAtom($selectedStoredSessionId, next)