Skip to content

fix(assistant): trailing-edge flush for throttled message updates - #221

Merged
juacker merged 2 commits into
mainfrom
fix/assistant-update-trailing-flush
Sep 27, 2026
Merged

juacker merged 2 commits into
mainfrom
fix/assistant-update-trailing-flush

Conversation

@juacker

@juacker juacker commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

When an agent makes parallel tool calls, both run, but the chat shows only the first one. The second appears only when the next batch of tool calls or the end of the turn arrives.

Root cause: the 200 ms throttle on assistant_message_updated (Claude, Codex exec, Codex app-server and OpenCode paths in local_agent.rs) was leading-edge only. An update suppressed inside the window was never re-sent. The database row always had every tool call; only the live UI event was lost. Codex emits parallel tool calls 3–53 ms apart, and Claude's adopt_complete_assistant_message persists several tool calls in a loop, so both paths hit it.

Fix

  • One throttle helper. UpdateThrottle<T> replaces three copy-pasted throttles. It emits immediately, or holds the latest update and releases it when the window ends.
  • Trailing emit. Each read loop adds a timer branch to its select!, so a held update reaches the UI within about 200 ms even while a tool runs for minutes. The rate stays capped at 5 updates per second.
  • Delta ordering. A held update is always sent before any AssistantDelta (emit_assistant_delta / delta_events). Otherwise a late snapshot would wipe text that had already streamed, because updateMessageContent resets the delta accumulator.
  • Error exits. Read, parse and handler errors, child.wait(), and the finalize failures in cancel, loop-end, rotate and split all send the held update first.
  • Completion, rotate and split. The held update is dropped, because AssistantMessageCompleted already carries the full content. Nothing is emitted after completion or under a stale message id.

Tests

  • update_throttle*: covers holding, latest-wins, window restart, and two tool calls 3 ms apart where the last emit contains both.
  • sleep_until_due: checked with paused tokio time.
  • delta_events_emit_held_update_before_delta: checks the ordering.
  • Changing offer back to the old drop-on-hold behaviour fails 4 of the 5 throttle tests.

No loop-level test exists, because the repo has no Tauri mock app.

Review

  • Round 1 returned needs_work: a stale snapshot could erase streamed text, and two minor findings. All three were fixed in f37f05981.
  • Round 2 returned production_quality.

Manual check

With a Codex- or Claude-backed agent, ask for two parallel tool calls, such as two workspace_assignTask calls. Both should appear in the chat immediately.

The 200 ms AssistantMessageUpdated throttle was leading-edge only: an
update suppressed inside the window was never re-emitted, so a second
parallel tool call persisted a few ms after the first stayed invisible
until some later update, possibly minutes away.

Replace the three copied throttle blocks (Claude, Codex, OpenCode) with
one UpdateThrottle that holds the latest suppressed update and reports
when it is due. Each read loop (Claude, Codex exec, Codex app-server,
OpenCode) races its next read against that deadline and emits the held
message on expiry. Error exits from a loop emit the held update; message
finalization/rotation/steer-split drop it, since AssistantMessageCompleted
carries the full content and the held message belongs to the old id.
… error exits

A held AssistantMessageUpdated emitted after an AssistantDelta for the same
message made the frontend drop its delta accumulator, hiding streamed text.
Route every delta through emit_assistant_delta, which flushes the held update
first. Also emit the held update on cancel-arm, child.wait and finalize
failures, but not after a message is finalized.
@juacker
juacker marked this pull request as ready for review September 27, 2026 00:34
@juacker
juacker merged commit 260b342 into main Sep 27, 2026
2 checks passed
@juacker
juacker deleted the fix/assistant-update-trailing-flush branch September 27, 2026 00:34
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