Skip to content

fix(tui): coalesce message.part.delta store writes - #48431

Open
dcerisano wants to merge 5 commits into
anomalyco:devfrom
dcerisano:coalesce-deltas
Open

dcerisano wants to merge 5 commits into
anomalyco:devfrom
dcerisano:coalesce-deltas

Conversation

@dcerisano

Copy link
Copy Markdown

Issue for this PR

Closes #36043 — jointly with the markdown-live-relex PR. Each applies
independently (different layer, no shared code, merge in either order);
only together do they remove O(n²) from both client-side stages of the
stream path and fully close the freeze. Supersedes #41472, which was
closed unmerged — this reworks that approach for the current sync.tsx
wiring (32 ms timer instead of per-frame, plus dropping deltas superseded
by authoritative events).

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Every message.part.delta was applied to the TUI store immediately,
re-rendering the whole growing part per token — O(n²) over the stream,
which freezes the TUI on fast reasoning streams. Deltas now accumulate per
part in a small buffer (delta-buffer.ts) and flush to the store on a
32 ms timer, capping renders around 30fps regardless of token rate.
Authoritative events (part.updated / part.removed / message.removed)
discard buffered deltas for the part they replace, and the buffer drains on
cleanup so *.ended handlers still see the final text. The
markdown-live-relex PR covers the other stage: growing the open markdown
tail in place instead of re-lexing per delta.

How did you verify your code works?

From packages/tui:

bun test src/context/delta-buffer.test.ts

New delta-buffer.test.ts (7 tests: same-part coalescing, part/kind
isolation, immediate drain, stale-flush no-op, single schedule across 100
pushes, drop by part and by message) — all passing. Also streamed a
reasoning-heavy session in the live TUI: no freeze, trailing text intact
once the stream ends.

Screenshots / recordings

N/A — no visual change, streaming-perf fix.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Tracking: #48430

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Related PR Found (Not Duplicate)

PR #48432: fix(session-ui): grow markdown live tail in place
#48432

This is a related but separate PR, not a duplicate. According to the description:

No duplicate PRs found for PR #48431 itself.

@kvnloo

kvnloo commented Sep 15, 2026

Copy link
Copy Markdown

Why this matters

Per-delta TUI store writes on long streams freeze the UI (#36043). Coalescing before apply is the leaf.

Evidence

Tip 685fa17 (base dev):

  • packages/tui/src/context/delta-buffer.tscreateDeltaBuffer
  • packages/tui/src/context/sync.tsxDELTA_COALESCE_MS = 32
  • live: bun test src/context/delta-buffer.test.ts7/7 pass; file absent on dev
  • CI: standards / compliance success

Confirms scope. Happy to help land as-is.

Ask (design, light) — prospective

  1. Is stream-delta store thrash TUI-only, or should all sync clients coalesce part deltas?
  2. For never-again: shared delta-buffer util across TUI/app, or per-surface timers?

@dcerisano

dcerisano commented Sep 16, 2026

Copy link
Copy Markdown
Author
  1. Is stream-delta store thrash TUI-only, or should all sync clients coalesce part deltas?

TUI-only in symptom. The web path already collapses same-tick bursts in server-sdk.tsx (consecutive same-part deltas merge pre-store), and Solid's fine-grained updates are cheaper than opentui's whole-part re-render — so the identical per-event writes in server-session.ts / event-reducer.ts don't freeze there. Headless surfaces need write-elimination, not timers. No blanket change planned; web gets a timer only if its timeline perf harness flags it.

  1. For never-again: shared delta-buffer util across TUI/app, or per-surface timers?

Per-surface timers — flush cadence is renderer-specific (opentui ~30fps vs browser vs headless-none), and sharing one couples three release cadences. What's shareable is the pure key/concat logic (now in three places: the buffer, currentDeltaKey/Fragment); I'd extract that to @opencode-ai/ui when a second consumer needs it. Until then, single-use-helper rule says keep the buffer TUI-local.

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.

TUI scroll freezes for seconds during fast-streaming responses (message.part.delta applied per-chunk with no batching)

2 participants