fix(desktop): qualify lineage absorption members by profile (#92454)
`_lineage_ids` members were matched bare, so a kept row in another profile whose stored id merely coincided with an incoming lineage was evicted. Qualify members with the owning row's profile, matching the identity and lineage keys in the same predicate.
This commit is contained in:
@@ -649,6 +649,50 @@ describe('mergeSessionPage', () => {
|
||||
expect(merged.map(s => s.id)).toEqual(['pinned', 'recent'])
|
||||
})
|
||||
|
||||
it('keeps a pinned twin in another profile when an incoming lineage carries the same stored id (#92454)', () => {
|
||||
// Bare-id lineage matching evicted a kept row whose id merely coincided
|
||||
// with another profile's `_lineage_ids`: in that other profile the id
|
||||
// really was absorbed into the projected tip, but the pinned row is a
|
||||
// different session. Lineage members must be profile-qualified like
|
||||
// every other key in the survivor predicate.
|
||||
const previous = [
|
||||
session({ id: 'sess-42', profile: 'quietbot', title: 'Pinned quiet work' })
|
||||
] as SessionInfo[]
|
||||
|
||||
const incoming = [
|
||||
session({
|
||||
id: 'tip',
|
||||
profile: 'testbot',
|
||||
_lineage_ids: ['sess-42', 'fresh-root', 'tip'],
|
||||
_lineage_root_id: 'fresh-root'
|
||||
})
|
||||
] as SessionInfo[]
|
||||
|
||||
const merged = mergeSessionPage(previous, incoming, ['sess-42'])
|
||||
|
||||
expect(merged.map(s => `${s.profile}:${s.id}`).sort()).toEqual(['quietbot:sess-42', 'testbot:tip'])
|
||||
})
|
||||
|
||||
it('drops a same-profile segment in the keep set when the incoming lineage absorbed it', () => {
|
||||
// Counterpart to the twin guard: qualification must not weaken the actual
|
||||
// absorption. seg2 was absorbed into testbot's projected tip and must be
|
||||
// evicted even though it sits in the keep set.
|
||||
const previous = [session({ id: 'seg2', profile: 'testbot' })] as SessionInfo[]
|
||||
|
||||
const incoming = [
|
||||
session({
|
||||
id: 'tip',
|
||||
profile: 'testbot',
|
||||
_lineage_ids: ['seg1', 'seg2', 'fresh-root', 'tip'],
|
||||
_lineage_root_id: 'fresh-root'
|
||||
})
|
||||
] as SessionInfo[]
|
||||
|
||||
const merged = mergeSessionPage(previous, incoming, ['seg2'])
|
||||
|
||||
expect(merged.map(s => `${s.profile}:${s.id}`)).toEqual(['testbot:tip'])
|
||||
})
|
||||
|
||||
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 })]
|
||||
|
||||
@@ -696,9 +696,12 @@ export function mergeSessionPage(
|
||||
// `_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.
|
||||
// inside another session's lineage. Like the identity and lineage keys
|
||||
// above, members are qualified by the owning row's profile — stored ids
|
||||
// are only unique per-profile (#92454), so a bare-id match would evict a
|
||||
// kept twin in another profile whose id merely coincides with a lineage.
|
||||
const incomingLineageIdMembers = new Set(
|
||||
merged.flatMap(session => session._lineage_ids ?? [])
|
||||
merged.flatMap(session => (session._lineage_ids ?? []).map(id => `${profileKeyOf(session)}::${id}`))
|
||||
)
|
||||
|
||||
const survivors = previous.filter(
|
||||
@@ -709,7 +712,7 @@ export function mergeSessionPage(
|
||||
!session.hidden &&
|
||||
!incomingIds.has(identity(session)) &&
|
||||
!incomingLineageKeys.has(lineageIdentity(session)) &&
|
||||
!incomingLineageIdMembers.has(session.id) &&
|
||||
!incomingLineageIdMembers.has(identity(session)) &&
|
||||
(keep.has(session.id) || (session._lineage_root_id != null && keep.has(session._lineage_root_id)))
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user