the finished message wins over the buffer that was streaming it

A reply rendered as "ipped." — first two characters gone — while the durable log
held it complete. Nothing was lost; the client discarded it.

The streaming buffer is an accumulation of deltas and is only as good as the
socket. The `assistant:text` event that ends the stream carries the whole message
and arrives right after. Committing the buffer and ignoring that text means a
single dropped frame is permanent, even though a correct copy came in immediately
behind it.

Losing a frame is not exotic: restarting the server mid-turn does it, which is how
this was found — three times tonight, by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 00:54:32 +00:00
co-authored by Claude Opus 5
parent 2651c45733
commit 730aeab6c5
+14 -4
View File
@@ -301,9 +301,19 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
});
}
function commitStreaming() {
if (!streamingRef.current) return;
const text = streamingRef.current;
/**
* Turn the in-flight streaming buffer into a committed message.
*
* `authoritative` is the complete text from the `assistant:text` event that ends the stream, and it
* wins whenever it is present. The buffer is an accumulation of deltas and is only ever as good as the
* socket: lose one frame — a restart mid-turn will do it — and it is short by whatever that frame
* carried, while the event carries the whole message. Committing the buffer threw away a correct copy
* in favour of a damaged one, which is how a reply rendered as "ipped." with its first two characters
* missing while the durable log held it intact.
*/
function commitStreaming(authoritative?: string) {
if (!streamingRef.current && !authoritative) return;
const text = authoritative ?? streamingRef.current;
streamingRef.current = '';
setStreamingText('');
setMessages((prev) => [...prev, { role: 'assistant', id: randomId(), text }]);
@@ -427,7 +437,7 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
break;
}
if (streamingRef.current) {
commitStreaming();
commitStreaming(msg.text);
} else {
setMessages((prev) => [...prev, { role: 'assistant', id: randomId(), text: msg.text }]);
}