diff --git a/apps/desktop/src/store/gateway-switch.test.ts b/apps/desktop/src/store/gateway-switch.test.ts index b3632a9193..5d82e62a5a 100644 --- a/apps/desktop/src/store/gateway-switch.test.ts +++ b/apps/desktop/src/store/gateway-switch.test.ts @@ -18,6 +18,7 @@ import { setSessionsLoading } from '@/store/session' import { $stalledSessionIds } from '@/store/session-states' +import { $transcriptTailBySessionId, recordTranscriptTail, transcriptTailState } from '@/store/transcript-tail' import { $gatewaySwitching, @@ -79,6 +80,28 @@ describe('wipeSessionListsForGatewaySwitch', () => { expect($freshDraftReady.get()).toBe(true) }) + it("forgets the previous backend's in-memory paging state", () => { + const page = { + messages: Array.from({ length: 120 }, (_, index) => ({ + id: index, + role: 'user' as const, + content: '', + timestamp: 1 + })), + pagination: { limit: 120, offset: 0, order: 'latest' as const, returned: 120 } + } + + recordTranscriptTail('recycled-id', page, { connectionId: 'local', profile: 'default' }) + + wipeSessionListsForGatewaySwitch() + + // A same-id session on the next backend must resolve its own tail alone. + recordTranscriptTail('recycled-id', page, { connectionId: 'remote-1', profile: 'default' }) + expect(Object.keys($transcriptTailBySessionId.get())).toHaveLength(1) + expect(transcriptTailState('recycled-id')?.possiblyTruncated).toBe(true) + $transcriptTailBySessionId.set({}) + }) + it('strands in-flight profile-list fetches so the old backend cannot repaint the rail (#85731)', () => { // The soft re-home moves /api/profiles routing to the NEW backend; a // response still in flight from the previous one must be invalidated diff --git a/apps/desktop/src/store/gateway-switch.ts b/apps/desktop/src/store/gateway-switch.ts index 620b77ecff..b73c067f64 100644 --- a/apps/desktop/src/store/gateway-switch.ts +++ b/apps/desktop/src/store/gateway-switch.ts @@ -26,6 +26,7 @@ import { import { clearAllSessionControl } from '@/store/session-control' import { resetSessionPinMirror } from '@/store/session-pin-sync' import { clearAllSessionStates } from '@/store/session-states' +import { clearAllTranscriptTails } from '@/store/transcript-tail' import { clearTranscriptTails } from '@/store/transcript-tail-cache' // True while a connection switch is mid-flight — a Settings → Gateway apply @@ -227,8 +228,12 @@ export function wipeSessionListsForGatewaySwitch(): void { // Cached transcript tails belong to the PREVIOUS backend's sessions; a // different backend can recycle stored ids, and painting another machine's - // conversation under a same-named id is worse than a loader. Wipe them. + // conversation under a same-named id is worse than a loader. Wipe both the + // persisted cache and the in-memory paging entries — the latter are keyed by + // owner, so a survivor from the old backend would sit beside the new one and + // fail the unique-match lookup that shows "Show earlier". clearTranscriptTails() + clearAllTranscriptTails() // Narrowed: account/marketplace/onboarding caches are global, not gateway- // scoped, so a mode swap must not refetch them. diff --git a/apps/desktop/src/store/transcript-tail.ts b/apps/desktop/src/store/transcript-tail.ts index 9e2443d2f9..565a1e78af 100644 --- a/apps/desktop/src/store/transcript-tail.ts +++ b/apps/desktop/src/store/transcript-tail.ts @@ -178,6 +178,13 @@ export function transcriptTailState( return matches.length === 1 ? matches[0][1] : undefined } +/** Forget every tail: the backend behind the window changed and a recycled + * stored id must not carry the previous backend's paging state. */ +export function clearAllTranscriptTails(): void { + transcriptTailOrder = [] + $transcriptTailBySessionId.set({}) +} + export function clearTranscriptTail(storedSessionId: string, profile?: TranscriptProfileScope): void { const current = $transcriptTailBySessionId.get()