fix(desktop): stop a finished reply rendering twice after history catches up
When a turn's reply commits under its own id, the settled local `assistant-stream-*` row shifts one assistant ordinal earlier, so ordinal pairing finds nothing at its slot and re-appends it — the same answer twice. Drop a settled stream row only when the authoritative transcript already carries that exact text. Keying `isPendingAssistant` on the explicit pending flag alone would also have fixed this, but it discards the sibling case in the same report: a reply that finished locally before the gateway committed it, where the local row is the only copy that exists. Co-authored-by: Dolverin <59100064+Dolverin@users.noreply.github.com>
This commit is contained in:
parent
b6f15f546d
commit
34f0427f7f
|
|
@ -945,6 +945,32 @@ describe('preserveLocalPendingTurnMessages', () => {
|
|||
expect(chatMessageText(preserved[1])).toBe('streamed body')
|
||||
})
|
||||
|
||||
// #70209: history committed the reply under its own id, so the settled local
|
||||
// stream row sits at a later ordinal, pairs with nothing, and gets appended —
|
||||
// the same answer twice.
|
||||
it('does not re-append a settled stream row the authoritative history already carries', () => {
|
||||
const next = [msg('1-user-stored', 'user', 'question'), msg('2-assistant-stored', 'assistant', 'answer')]
|
||||
const settledLocalStream = msg('assistant-stream-runtime-1', 'assistant', 'answer', { pending: false })
|
||||
|
||||
expect(preserveLocalPendingTurnMessages(next, [...next, settledLocalStream])).toBe(next)
|
||||
})
|
||||
|
||||
// The reply finished locally but the gateway had not committed it when the
|
||||
// session was reopened — the local row is the only copy and must survive.
|
||||
it('keeps a settled stream row the authoritative history has not committed', () => {
|
||||
const previous = [
|
||||
msg('1-user', 'user', 'question'),
|
||||
msg('assistant-stream-sess', 'assistant', 'the finished reply', { pending: false })
|
||||
]
|
||||
|
||||
const next = [msg('1-user', 'user', 'question')]
|
||||
|
||||
expect(preserveLocalPendingTurnMessages(next, previous).map(message => message.id)).toEqual([
|
||||
'1-user',
|
||||
'assistant-stream-sess'
|
||||
])
|
||||
})
|
||||
|
||||
// The whole point of replacing rather than appending: one reply on screen,
|
||||
// and the committed history around the live turn untouched.
|
||||
it('does not duplicate or rewrite committed history around the live turn', () => {
|
||||
|
|
|
|||
|
|
@ -507,6 +507,24 @@ export function preserveLocalPendingTurnMessages(
|
|||
|
||||
const authoritative = nextByRoleOrdinal.get(`${message.role}:${ordinal}`)
|
||||
|
||||
// A settled stream row (`pending: false` after message.complete) whose reply
|
||||
// the authoritative transcript already carries under its committed id is
|
||||
// stale: ordinal pairing can't see it, because the commit shifted the row
|
||||
// one ordinal earlier, and re-appending it renders the same answer twice
|
||||
// (#70209). Only text-identical rows are dropped — a settled row the backend
|
||||
// has NOT committed yet is the only copy of that reply and must survive.
|
||||
if (
|
||||
isPendingAssistant &&
|
||||
message.pending !== true &&
|
||||
nextMessages.some(
|
||||
candidate =>
|
||||
candidate.role === 'assistant' &&
|
||||
textWithoutReferenceLines(chatMessageText(candidate)) === textWithoutReferenceLines(chatMessageText(message))
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (authoritative) {
|
||||
if (isPendingAssistant) {
|
||||
// Keep the local pending row when it is the same reply further along
|
||||
|
|
@ -522,7 +540,8 @@ export function preserveLocalPendingTurnMessages(
|
|||
}
|
||||
|
||||
if (
|
||||
textWithoutReferenceLines(chatMessageText(authoritative)) === textWithoutReferenceLines(chatMessageText(message))
|
||||
textWithoutReferenceLines(chatMessageText(authoritative)) ===
|
||||
textWithoutReferenceLines(chatMessageText(message))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue