fix(desktop): compare pinned/archived in the session list signature
`refreshSessions` swaps the session page into `$sessions` only when
`sameCronSignature` reports a change, and that signature compared row
content — id, lineage root, title, source, profile, preview,
message_count, last_active, ended_at — but not row state. A page whose
only delta was `pinned` was judged identical and discarded, so the row
cached in the atom kept its old flag indefinitely. An idle conversation
never moves any of the compared fields again, which is exactly the kind
a user goes and unpins.
`session-pin-sync` treats that row as authoritative. Its write guard
(daeedf67c) is released by a page that CONFIRMS the value it wrote, and
falls back to letting the server win once WRITE_GUARD_MS elapses with no
confirmation. Because the confirming page was filtered out one layer up,
the fallback was the only branch that ever ran: ~10s after an unpin the
next reconcile read the frozen `pinned: true` row and called
pinSession() again. Adoption marks the id `mirrored`, so the push pass
never corrected the backend either — the local pin set and
sessions.pinned drifted apart permanently, which is why four of five
pins rendered in the sidebar read pinned=0 in state.db.
Compare both flags so a pin-only page reaches the atom. That restores
the guard's confirm path and makes WRITE_GUARD_MS a backstop again
rather than the load-bearing branch. `archived` is included for the same
reason: it is row state a consumer reads. Neither flag moves outside a
deliberate user action, so the churn the gate exists to prevent is
unaffected.
The existing `releases the guard once a page confirms the written value`
test passes on main because it hands `$sessions` the confirming page
directly — the gap was in the pipeline that decides whether such a page
is ever delivered.
Fixes #76919
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,8 @@ import type { SessionInfo } from '@/hermes'
|
||||
|
||||
import { sameCronSignature, sessionMessagesSignature } from './session-signatures'
|
||||
|
||||
const session = (id: string, title: string | null): SessionInfo => ({ id, title }) as SessionInfo
|
||||
const session = (id: string, title: string | null, extra: Partial<SessionInfo> = {}): SessionInfo =>
|
||||
({ id, title, ...extra }) as SessionInfo
|
||||
|
||||
describe('sameCronSignature', () => {
|
||||
it('is false when the lengths differ', () => {
|
||||
@@ -28,6 +29,28 @@ describe('sameCronSignature', () => {
|
||||
const b = [session('b', 't'), session('a', 't')]
|
||||
expect(sameCronSignature(a, b)).toBe(false)
|
||||
})
|
||||
|
||||
// A pin-only page must reach $sessions: session-pin-sync treats the row as
|
||||
// authoritative and releases its write guard when a page confirms the value
|
||||
// it wrote. Gating that page out froze the row and re-pinned what the user
|
||||
// had just unpinned (#76919).
|
||||
it('is false when only the pinned flag changed', () => {
|
||||
const a = [session('a', 't', { pinned: true })]
|
||||
const b = [session('a', 't', { pinned: false })]
|
||||
expect(sameCronSignature(a, b)).toBe(false)
|
||||
})
|
||||
|
||||
it('is false when only the archived flag changed', () => {
|
||||
const a = [session('a', 't', { archived: false })]
|
||||
const b = [session('a', 't', { archived: true })]
|
||||
expect(sameCronSignature(a, b)).toBe(false)
|
||||
})
|
||||
|
||||
it('is true when both flags match', () => {
|
||||
const a = [session('a', 't', { archived: false, pinned: true })]
|
||||
const b = [session('a', 't', { archived: false, pinned: true })]
|
||||
expect(sameCronSignature(a, b)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sessionMessagesSignature', () => {
|
||||
|
||||
@@ -23,7 +23,14 @@ export function sameCronSignature(a: SessionInfo[], b: SessionInfo[]): boolean {
|
||||
session.preview === other.preview &&
|
||||
session.message_count === other.message_count &&
|
||||
session.last_active === other.last_active &&
|
||||
session.ended_at === other.ended_at
|
||||
session.ended_at === other.ended_at &&
|
||||
// Row STATE, not just row content: session-pin-sync reconciles the
|
||||
// sidebar's pins against `pinned` on the rows in this atom, so a page
|
||||
// whose only delta is a flag has to swap in or the reconciler reads a
|
||||
// frozen copy forever. An idle conversation never moves any of the
|
||||
// fields above again, which is exactly when a pin gets toggled (#76919).
|
||||
session.pinned === other.pinned &&
|
||||
session.archived === other.archived
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user