Skip to content

fix(webui): conflate queued chat renders — chat body no longer lags behind finished agents - #1804

Open
Zenetusken wants to merge 1 commit into
agent0ai:mainfrom
Zenetusken:fix/webui-render-conflation
Open

fix(webui): conflate queued chat renders — chat body no longer lags behind finished agents#1804
Zenetusken wants to merge 1 commit into
agent0ai:mainfrom
Zenetusken:fix/webui-render-conflation

Conversation

@Zenetusken

Copy link
Copy Markdown

Problem

During an active turn the Web UI chat body progressively falls behind the backend, and after the agent finishes it keeps "catching up" for many minutes — a completed workflow looks like it is still running. Other clients (CLI, fresh tabs) show the true state immediately; a page refresh "fixes" the Web UI by discarding the backlog. Reported recurring across sessions; #1803 fixed the stale status indicator (sequencing), this PR fixes the chat body backlog itself.

Root cause (measured)

  • Server pushes are debounced at 25ms (helpers/state_monitor.py), so during reasoning streaming, snapshots arrive at up to ~40/s.
  • setMessages chained one full render per snapshot onto _messageRenderQueue. Every queued render executed even when superseded — no conflation. With renders slower than pushes, the queue grows unboundedly (O(pushes) renders for one turn).
  • Backend verified healthy throughout: finished/idle threads (py-spy), complete persisted log, empty queue — the lag was purely client-side render backlog.

Behavioral proof (shipped as a test): the real setMessages extracted from source, run in Node with a slow renderer — pre-fix code renders 60/60 pushes; fixed code conflates to a handful of renders with zero messages lost.

Fix

Snapshot logs are deltas, but merging is cumulative-safe (message-window.merge dedups by id+type, last write wins). So setMessages now accumulates pending deltas and drains them in as few renders as possible:

  • While a drain is in flight, new pushes just extend the pending buffer (early return).
  • The drain loop renders accumulated batches until empty — no intermediate state is ever rendered.
  • Awaiting callers still resolve only after a render that includes their messages; stale generations are dropped on render-state reset.

Validation

  • New tests/test_webui_render_conflation.py: behavioral Node harness (2 tests) + structural guard (1 test). Discriminating: conflation test fails on pre-fix v2.8 ("got 60 renders for 60 pushes"), all pass with the fix. Completeness test passes on both (old code was complete, just unboundedly slow).
  • Full suite: 1331 passed, 0 failed, 1 skipped on the v2.8 runtime.
  • node --check webui/js/messages.js clean.

Note

Related but separate: the browser-panel screencast (ws_browser._stream_frames) streams every frame unconditionally while the panel is open (~30% CPU, shares the WS worker with chat pushes). Backpressure for that is a candidate follow-up arc.

setMessages chained one full render per incoming snapshot. Server pushes
are debounced at 25ms, so during reasoning streaming snapshots arrive far
faster than renders complete; the render queue grew unboundedly and the
chat body fell behind in real time. A finished workflow then looked like
it was still running until the backlog drained (CLI vs Web UI split-brain;
a refresh 'fixed' it by discarding the backlog).

Snapshot logs are deltas but merging is cumulative-safe (message-window
dedups by id+type, last write wins), so accumulate pending deltas and
drain them in as few renders as possible. Awaiting callers still resolve
only after a render that includes their messages; stale generations are
dropped on render-state reset.

Regression coverage runs the real setMessages in Node with a slow
renderer: pre-fix code renders 60/60 pushes (demonstrated), fixed code
conflates heavily while rendering every message exactly once, in order.
Zenetusken added a commit to Zenetusken/agent-zero that referenced this pull request Aug 2, 2026
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.

1 participant