From d15ed4445207dda418b984e8bda0f68f48b8c6f3 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:16:11 +0530 Subject: [PATCH] fix(desktop): forget in-memory paging state on a connection/mode re-home wipeSessionListsForGatewaySwitch cleared the persisted transcript-tail cache but never the in-memory $transcriptTailBySessionId atom. Before this stack both an ambient local read and an ambient remote read of a recycled stored id landed under the bare-id key, so the newer backend simply overwrote the older. Owner-keyed entries coexist instead, and the unique-match lookup that gates "Show earlier" sees two candidates and fails closed for that session until eviction. Wipe the atom alongside the cache. --- apps/desktop/src/store/gateway-switch.test.ts | 23 +++++++++++++++++++ apps/desktop/src/store/gateway-switch.ts | 7 +++++- apps/desktop/src/store/transcript-tail.ts | 7 ++++++ 3 files changed, 36 insertions(+), 1 deletion(-) 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()