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.
This commit is contained in:
kshitijk4poor
2026-09-11 12:16:11 +05:30
committed by kshitij
parent c2d01f1012
commit d15ed44452
3 changed files with 36 additions and 1 deletions
@@ -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
+6 -1
View File
@@ -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.
@@ -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()