fix(desktop): keep the attachment-tolerance dedupe inside the identity gate (#122079)
The tolerant sameAttachmentTurn arm was an unparenthesised || operand, so a rowId-bearing optimistic row could be swallowed by a committed row it provably is not — pasting the same captioned screenshot twice dropped the second, genuine turn. Gate both arms on !conflictingTranscriptIdentity, matching the comment's invariant; the rowId-less paste keeps matching tolerantly.
This commit is contained in:
@@ -2205,4 +2205,35 @@ describe('preserveLocalPendingTurnMessages attachment rewrites (#120978)', () =>
|
||||
'user-plain-repeat'
|
||||
])
|
||||
})
|
||||
|
||||
it('never tolerance-matches a rowId-bearing optimistic row it provably is not (#122079)', () => {
|
||||
// The submit receipt binds user_row_id onto the optimistic row while the
|
||||
// stored page still ends at the earlier paste, so the row reaches the
|
||||
// dedupe compare carrying a rowId none of the committed candidates hold.
|
||||
// The tolerant arm must stay inside the identity gate: pasting the same
|
||||
// captioned screenshot twice is a genuine new turn, not a duplicate.
|
||||
const previous = [
|
||||
msg('1-user', 'user', 'first'),
|
||||
msg('2-assistant', 'assistant', 'first answer'),
|
||||
msg('user-1790168309-ab12cd', 'user', 'unable to publish', {
|
||||
rowId: 901,
|
||||
attachmentRefs: ['data:image/png;base64,AAAA']
|
||||
})
|
||||
]
|
||||
|
||||
const next = [
|
||||
msg('1-user-stored', 'user', 'first', { rowId: 1 }),
|
||||
msg('2-assistant-stored', 'assistant', 'first answer', { rowId: 2 }),
|
||||
msg('3-user-stored', 'user', 'unable to publish\n\n[Image attached at: C:\\img\\shot.png]\n[screenshot]', {
|
||||
rowId: 3
|
||||
})
|
||||
]
|
||||
|
||||
expect(preserveLocalPendingTurnMessages(next, previous).map(message => message.id)).toEqual([
|
||||
'1-user-stored',
|
||||
'2-assistant-stored',
|
||||
'3-user-stored',
|
||||
'user-1790168309-ab12cd'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -795,12 +795,14 @@ export function preserveLocalPendingTurnMessages(
|
||||
// unacknowledged repeat whose committed twin predates the acknowledged
|
||||
// boundary (and never enters this window) still survives.
|
||||
const newestAuthoritativeUser = [...remainingNext].reverse().find(message => message.role === 'user')
|
||||
|
||||
const acknowledgedUserCandidates = remainingNext.filter(
|
||||
message =>
|
||||
message.role === 'user' &&
|
||||
!isGatewaySystemMarker(message) &&
|
||||
(message.rowId !== undefined || message === newestAuthoritativeUser)
|
||||
)
|
||||
|
||||
const preserved: ChatMessage[] = []
|
||||
// Authoritative id → richer local pending row. Replacing (not appending)
|
||||
// avoids painting both the empty inflight shell and the full stream bubble.
|
||||
@@ -886,9 +888,14 @@ export function preserveLocalPendingTurnMessages(
|
||||
isOptimisticUser &&
|
||||
acknowledgedUserCandidates.some(
|
||||
candidate =>
|
||||
(!conflictingTranscriptIdentity(message, candidate) &&
|
||||
textWithoutReferenceLines(chatMessageText(candidate)) === textWithoutReferenceLines(chatMessageText(message))) ||
|
||||
sameAttachmentTurn(candidate, message)
|
||||
// #122079: the tolerant arm widens the TEXT compare only — it stays
|
||||
// inside the identity gate, so a rowId-bearing optimistic row is
|
||||
// never swallowed by a committed row it provably is not (a genuine
|
||||
// repeat of the same captioned paste). The rowId-less paste from
|
||||
// #120978 carries no identity and keeps matching tolerantly.
|
||||
!conflictingTranscriptIdentity(message, candidate) &&
|
||||
(textWithoutReferenceLines(chatMessageText(candidate)) === textWithoutReferenceLines(chatMessageText(message)) ||
|
||||
sameAttachmentTurn(candidate, message))
|
||||
)
|
||||
) {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user