[Mirror] fix(ui): throttle SSE part deltas and prevent stale delta text duplication#17
Conversation
Accumulate text deltas in a buffer and flush every 50ms, so rapid streaming chunks don't trigger a Solid store mutation + re-render on every single delta event. On mobile this cuts the main-thread blocking caused by KaTeX / highlight.js re-renders during streaming responses.
When message.part.updated arrives with a complete part, applyPartUpdate replaces the part entirely with the server's state. But if deltas were enqueued before the part update arrived and haven't flushed yet, those stale deltas would be applied AFTER the replacement, causing text duplication at the end of assistant messages. Fix: clear pending deltas for a part before applying the full part update, since the update already contains the complete state and any accumulated deltas are now stale. This prevents the race where: 1. Deltas accumulate in the 50ms throttle window 2. message.part.updated arrives and replaces the part with complete text 3. Delta timer expires and concatenates stale deltas → duplicate text
…t ordering When deltas are buffered for up to 50ms and message.updated arrives before the flush timer fires, the message could be marked complete/error before pending text mutations are applied. This causes the UI to observe a terminal-status message with stale content. Fix: flush any pending deltas for the message before applying the message.updated event. This preserves the server's event ordering: all delta content is applied first, then the message status/metadata update runs on the complete content. Addresses gatekeeper review blocking finding #1 on PR NeuralNomadsAI#536.
|
| Filename | Overview |
|---|---|
| packages/ui/src/stores/session-events.ts | Adds 50 ms delta-throttle buffer, two eager-flush helpers, and a "sent"-status fix for synthetic message matching; logic is sound but flushPendingDeltasForMessage mutates the Map while iterating its keys (already flagged in prior review), and the file now sits at 851 lines above the project's ~800-line target. |
Sequence Diagram
sequenceDiagram
participant SSE as SSE Stream
participant H as handleMessagePartDelta
participant Q as pendingDeltas Map
participant T as Timer 50ms
participant B as applyPartDeltaV2
SSE->>H: message.part.delta
H->>Q: accumulate delta by instanceId:messageId:partId:field
Q-->>T: arm timer if not running
alt message.part.updated arrives first
SSE->>Q: clearPendingDeltasForPart
Note over Q: stale deltas discarded, full server state applied
else message.updated arrives first
SSE->>Q: flushPendingDeltasForMessage
Q->>B: applyPartDeltaV2 for each pending key
Note over B: all content applied before message marked complete
else timer fires normally
T->>Q: flushDeltas
Q->>B: applyPartDeltaV2 for each batched entry
Note over Q: Map cleared via Array.from snapshot
end
Reviews (2): Last reviewed commit: "fix(ui): flush pending deltas before mes..." | Re-trigger Greptile
Apply two P2 (style-level) improvements from Greptile gatekeeper review: 1. Consistent snapshot pattern in flushPendingDeltasForMessage - Collect keys first, then iterate to flush (matches clearPendingDeltasForPart) - Prevents mutation-during-iteration and ensures delete() runs before apply - If applyPartDeltaV2 throws, entry is already removed (no re-apply on timer) 2. Extract delta-buffer to dedicated module - session-events.ts: 854 → 808 lines (under 850, closer to 800 target) - delta-buffer.ts: 80 lines (focused, single-responsibility) - Improves maintainability per AGENTS.md file-length guidelines No functional changes. Build verified. Addresses: Pagecran#17 (comment) Pagecran#17 (comment)
Mirror of NeuralNomadsAI#536 for Greptile review.\n\nSource PR: https://github.com/NeuralNomadsAI/CodeNomad/pull/536\nSource head: 105838b\nSource base: 4a1d53b\n\nThis mirror uses a dedicated base branch matching the upstream PR base so the diff stays limited to the original PR changes.