Skip to content

fix(tui): coalesce part stream deltas per frame to prevent stalls - #41472

Draft
warrenbhw wants to merge 1 commit into
anomalyco:devfrom
warrenbhw:thinking-delta-coalescing
Draft

fix(tui): coalesce part stream deltas per frame to prevent stalls#41472
warrenbhw wants to merge 1 commit into
anomalyco:devfrom
warrenbhw:thinking-delta-coalescing

Conversation

@warrenbhw

Copy link
Copy Markdown

Problem

Clicking to expand a streaming <thinking> block in the TUI can stall the UI until reasoning finishes.

Root cause chain:

  1. The LLM stream emits session.next.reasoning.delta (and text.delta / tool.input.delta) events per chunk, unbuffered (packages/core/src/session/runner/publish-llm-event.ts:278).
  2. The TUI store applied every delta synchronously: match.text += event.data.delta on each event (packages/tui/src/context/data.tsx).
  3. While a reasoning block is expanded, every store write re-renders its markdown block and OpenTUI re-parses + re-highlights the entire accumulated text on each token — O(N) work per delta, O(N²) over the stream. Long thinking blocks (thousands of tokens) pin the render loop and the TUI appears frozen.

Note the web app already avoids this exact problem with per-part delta accumulation (part_text_accum_delta, #26822); the TUI had no equivalent.

Change

Adds a per-frame delta coalescer (packages/tui/src/context/delta-buffer.ts):

  • Deltas accumulate per (kind, sessionID, messageID, partID); the store is rewritten at most once per animation frame instead of once per event.
  • drain() runs before any non-delta event (e.g. *.ended) so terminating events that replace the full part text still observe the final accumulated deltas; also drains on provider unmount.
  • Covers text.delta, reasoning.delta, and tool.input.delta (identical pattern, same fix).

Testing

  • New unit tests for createDeltaBuffer (coalescing, per-part isolation, drain semantics, stale flush no-op, single-schedule). bun test passes 5/5.
  • Could not run tsgo --noEmit in this environment (tool not installed); changes are type-annotated to match the module's existing usage.
  • Not yet manually verified in a live TUI session.

Follow-up (not included)

  • The v2 session durable-store path (packages/core/src/session/message-updater.ts:358) performs the same per-delta full-text rewrite; worth batching there separately.

@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Aug 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@warrenbhw

Copy link
Copy Markdown
Author

This is obviously not ready for review, just had an agent do a first slop pass trying to fix a bug I hit in opencode2.

I'll figure out a repro case, validate the fix, and clean up the code sometime in the next couple of days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs:compliance This means the issue will auto-close after 2 hours. needs:issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant