Skip to content

[Mirror] fix(ui): throttle SSE part deltas and prevent stale delta text duplication#17

Open
pascalandr wants to merge 3 commits into
mirror/pr-536-base-dev-4a1d53bfrom
mirror/pr-536-sse-delta-throttle
Open

[Mirror] fix(ui): throttle SSE part deltas and prevent stale delta text duplication#17
pascalandr wants to merge 3 commits into
mirror/pr-536-base-dev-4a1d53bfrom
mirror/pr-536-sse-delta-throttle

Conversation

@pascalandr

Copy link
Copy Markdown

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.

JDis03 added 3 commits June 8, 2026 21:47
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.
@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR throttles SSE message.part.delta events through a 50 ms accumulation buffer to reduce redundant re-renders, and adds two synchronous flush paths (clearPendingDeltasForPart, flushPendingDeltasForMessage) to prevent stale buffered deltas from duplicating content when a full message.part.updated or message.updated event arrives first.

  • Introduces enqueueDelta/flushDeltas with a module-level Map + setTimeout to coalesce part-delta SSE events into single applyPartDeltaV2 calls every 50 ms.
  • Adds eager-flush hooks in both message.part.updated and message.updated handlers so server-authoritative full-state updates always land on a clean buffer.
  • Extends findPendingSyntheticMessageId to also match messages in "sent" status, closing a race where a synthetic placeholder persisted past the server-acknowledged-but-not-yet-replaced window.

Confidence Score: 5/5

Safe to merge; the buffer/flush logic is sound and the eager-flush paths correctly prevent stale deltas from duplicating content.

The three main code paths — timer flush, part-update discard, and message-update eager flush — all use consistent patterns. The findPendingSyntheticMessageId fix to include "sent" is small and targeted. No data loss or duplication scenarios are introduced by the new buffering mechanism.

packages/ui/src/stores/session-events.ts — now at 851 lines, above the project's ~800-line target, and flushPendingDeltasForMessage mutates the Map while iterating its keys (prior review thread).

Important Files Changed

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
Loading

Reviews (2): Last reviewed commit: "fix(ui): flush pending deltas before mes..." | Re-trigger Greptile

Comment thread packages/ui/src/stores/session-events.ts
Comment thread packages/ui/src/stores/session-events.ts
JDis03 added a commit to JDis03/CodeNomad that referenced this pull request Jun 10, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants