fix(desktop): drop ghost sidebar rows absorbed by a reorganized compression chain (#85331)
mergeSessionPage's survivor filter kept any previous row whose id sat in the keep set and that the incoming page did not return. The tip-rotation lineage dedup (#43483) only matched through the lineage ROOT key, so when a compression-chain reorganization minted a FRESH root id (manual storage-format repair), an old SEGMENT row that was in the keep set (the working/selected session at refresh time) produced the exact signal of a legitimately-kept row: absent from the page, unmatched by lineage key. It survived as a title-less ghost the backend never sent — the sidebar rendered N+2 rows for a group the endpoints served as N. Fix: the survivor filter now also drops a row whose id appears anywhere inside an incoming row's `_lineage_ids` (the full chain the backend projects, already served on list rows). That is absorption, not staleness: a genuinely-kept row — pinned aged off the page, an in-flight first turn — never has its id inside ANOTHER session's lineage, so the protected survivor behaviors all hold (guards included). No API change, no extra traffic; the data needed was already on the wire. Regression tests: the reporter's exact unit repro (old segment in keep after reorg mints a fresh root) and the pinned-aging-off guard with a deep lineage on the incoming page. The official repair-chains maintenance command from the report is a separate feature (open PR #86743) and stays out of this fix.
This commit is contained in:
@@ -606,6 +606,49 @@ describe('mergeSessionPage', () => {
|
||||
expect(merged.map(s => s.id)).toEqual(['b', 'a-new'])
|
||||
})
|
||||
|
||||
it('drops an old segment kept in the keep set after the chain reorg minted a fresh root (#85331)', () => {
|
||||
// The reporter's unit repro: a manual compression-chain reorganization
|
||||
// relinked old segments under a NEW root id. The backend now returns the
|
||||
// tip (carrying _lineage_ids with every chain segment); the previous
|
||||
// list still holds an old SEGMENT row that is in the keep set (it was
|
||||
// the working/selected session at refresh time). Its id is absent from
|
||||
// the incoming page and unmatched by the root-only lineage key, so it
|
||||
// used to survive as a title-less ghost row the backend never sent.
|
||||
const previous = [
|
||||
session({ id: 'seg2' }), // old segment: no lineage of its own
|
||||
session({ id: 'tip', _lineage_root_id: 'fresh-root' }),
|
||||
session({ id: 'other' })
|
||||
] as SessionInfo[]
|
||||
|
||||
const incoming = [
|
||||
// The reorganized chain served as its tip, with every chain id.
|
||||
session({ id: 'tip', _lineage_ids: ['seg1', 'seg2', 'fresh-root', 'tip'], _lineage_root_id: 'fresh-root' }),
|
||||
session({ id: 'other' })
|
||||
] as SessionInfo[]
|
||||
|
||||
// seg2 was the working session at refresh time — in the keep set.
|
||||
const merged = mergeSessionPage(previous, incoming, ['seg2'])
|
||||
|
||||
expect(merged.map(s => s.id)).toEqual(['tip', 'other'])
|
||||
})
|
||||
|
||||
it('keeps a pinned session aged off the page even when another row carries a deep lineage', () => {
|
||||
// The absorption filter must NOT evict legitimately-kept rows: a pinned
|
||||
// row's own id never appears inside ANOTHER session's lineage. Guard for
|
||||
// the #85331 fix — this is the pre-existing pinned-aging-off behavior
|
||||
// ('keeps a pinned session that has aged off the recent page') plus a
|
||||
// deep _lineage_ids on the incoming page.
|
||||
const previous = [session({ id: 'recent' }), session({ id: 'pinned' })]
|
||||
|
||||
const incoming = [
|
||||
session({ id: 'recent', _lineage_ids: ['recent', 'recent-root'], _lineage_root_id: 'recent-root' })
|
||||
]
|
||||
|
||||
const merged = mergeSessionPage(previous, incoming, ['pinned'])
|
||||
|
||||
expect(merged.map(s => s.id)).toEqual(['pinned', 'recent'])
|
||||
})
|
||||
|
||||
it('never regresses last_active behind an optimistic user-send bump', () => {
|
||||
const previous = [session({ id: 'old', last_active: 9_000 })]
|
||||
const incoming = [session({ id: 'old', last_active: 100, message_count: 4 })]
|
||||
|
||||
@@ -683,6 +683,24 @@ export function mergeSessionPage(
|
||||
// another profile is a DIFFERENT session and must survive the dedupe.
|
||||
const incomingLineageKeys = new Set(merged.map(lineageIdentity))
|
||||
|
||||
// Absorption filter: a survivor whose id appears ANYWHERE inside an
|
||||
// incoming row's compression lineage is not a separate session anymore —
|
||||
// the backend now serves that conversation as the chain's projected row.
|
||||
// `mergeSessionPage`'s own survivors come from the tip-rotation dedup
|
||||
// (#43483), but that only catches a lineage match through the root key.
|
||||
// When a reorganized chain mints a FRESH root id (manual compression-chain
|
||||
// repair, #85331), an old segment row in the keep set (it was the
|
||||
// working/selected session at refresh time) produced the exact signal of a
|
||||
// legitimately-kept row: absent from the incoming page, unmatched by
|
||||
// lineage key. It survived as a title-less ghost. The incoming rows carry
|
||||
// `_lineage_ids` — every id the chain has answered to — so matching a
|
||||
// survivor id against that list identifies absorption WITHOUT evicting a
|
||||
// genuinely-pinned row aged off the page: a pinned row's id never appears
|
||||
// inside another session's lineage.
|
||||
const incomingLineageIdMembers = new Set(
|
||||
merged.flatMap(session => session._lineage_ids ?? [])
|
||||
)
|
||||
|
||||
const survivors = previous.filter(
|
||||
session =>
|
||||
// The keep-list answers "live, not listed yet" — a hidden row (canonical
|
||||
@@ -691,6 +709,7 @@ export function mergeSessionPage(
|
||||
!session.hidden &&
|
||||
!incomingIds.has(identity(session)) &&
|
||||
!incomingLineageKeys.has(lineageIdentity(session)) &&
|
||||
!incomingLineageIdMembers.has(session.id) &&
|
||||
(keep.has(session.id) || (session._lineage_root_id != null && keep.has(session._lineage_root_id)))
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user