fix(desktop): prevent stale optimistic tails after compression (#69682)

This commit is contained in:
ethernet 2026-07-22 19:32:20 -04:00 committed by GitHub
parent 6283a33a50
commit 93c97073d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 54 additions and 3 deletions

View file

@ -327,6 +327,42 @@ describe('preserveLocalPendingTurnMessages', () => {
expect(preserveLocalPendingTurnMessages(next, previous)).toBe(next)
})
it('drops stale optimistic history after compression and keeps only the live tail', () => {
const compressedAuthority = [
msg('stored-user', 'user', 'first turn that survived compression'),
msg('stored-assistant', 'assistant', 'latest authoritative reply')
]
const pollutedWarmCache = [
msg('user-old-1', 'user', 'compressed-away prompt one'),
msg('assistant-old-1', 'assistant', 'compressed-away reply one'),
msg('user-old-2', 'user', 'compressed-away prompt two'),
msg('assistant-old-2', 'assistant', 'compressed-away reply two'),
msg('user-inflight', 'user', 'the one genuinely in-flight prompt')
]
expect(preserveLocalPendingTurnMessages(compressedAuthority, pollutedWarmCache).map(message => message.id)).toEqual([
'stored-user',
'stored-assistant',
'user-inflight'
])
})
it('drops the live tail once the latest authoritative user has persisted it after compression', () => {
const compressedAuthority = [
msg('stored-user', 'user', 'the one genuinely in-flight prompt'),
msg('stored-assistant', 'assistant', 'its authoritative reply')
]
const pollutedWarmCache = [
msg('user-old-1', 'user', 'compressed-away prompt one'),
msg('assistant-old-1', 'assistant', 'compressed-away reply one'),
msg('user-inflight', 'user', 'the one genuinely in-flight prompt')
]
expect(preserveLocalPendingTurnMessages(compressedAuthority, pollutedWarmCache)).toBe(compressedAuthority)
})
})
describe('appendLiveSessionProjection', () => {

View file

@ -234,9 +234,12 @@ export function reconcileResumeMessages(nextMessages: ChatMessage[], previousMes
* dropping either makes an accepted turn appear to vanish during transport
* churn.
*
* Authoritative rows use different ids, so match by role ordinal. A matching
* user row is considered committed only when its visible text also matches;
* any authoritative assistant at the same ordinal supersedes the local stream.
* A lagging projection can be behind by one live turn, never a whole local
* history window. Preserve only the newest optimistic user row: compression
* rewrites past context, so older `user-*` rows in a warm cache are stale
* history, not in-flight work. The latest authoritative user confirms whether
* that tail has persisted; any authoritative assistant at the same ordinal
* supersedes the local stream.
*/
export function preserveLocalPendingTurnMessages(
nextMessages: ChatMessage[],
@ -257,6 +260,10 @@ export function preserveLocalPendingTurnMessages(
const nextIds = new Set(nextMessages.map(message => message.id))
const previousRoleCounts = new Map<ChatMessage['role'], number>()
const newestOptimisticUser = [...previousMessages]
.reverse()
.find(message => message.role === 'user' && message.id.startsWith('user-'))
const latestAuthoritativeUser = [...nextMessages].reverse().find(message => message.role === 'user')
const preserved: ChatMessage[] = []
for (const message of previousMessages) {
@ -272,6 +279,14 @@ export function preserveLocalPendingTurnMessages(
continue
}
if (isOptimisticUser && message !== newestOptimisticUser) {
continue
}
if (isOptimisticUser && latestAuthoritativeUser && chatMessageText(latestAuthoritativeUser).trim() === chatMessageText(message).trim()) {
continue
}
const authoritative = nextByRoleOrdinal.get(`${message.role}:${ordinal}`)
if (authoritative) {