Skip to content

fix: web UI messages stuck until refresh — SSE event ordering bug #2079

Description

@henrypark133

Bug

Web UI chat messages intermittently appear "stuck" — they don't render in real-time, but show up after page refresh. Only affects web UI; other channels work fine.

Root Cause

Event ordering bug in thread_ops.rs: The "Done" status SSE event is sent BEFORE the response SSE event, separated by two DB writes and a function return boundary.

thread_ops.rs:578  →  send_status("Done")           ← SSE "Done" (arrives first)
thread_ops.rs:588  →  persist_tool_calls()           ← DB write
thread_ops.rs:597  →  persist_assistant_response()   ← DB write
thread_ops.rs:620  →  return response string
agent_loop.rs:972  →  channels.respond(response)     ← SSE "response" (renders the message)

The response SSE event is the sole mechanism for rendering the assistant's message in the browser (no streaming chunks emitted in the v1 engine path). If it's lost — due to broadcast channel lag (BroadcastStream silently drops on Err(_) => None at sse.rs:163), Railway proxy buffering, or brief disconnection during the gap — the message is invisible until manual refresh.

The same ordering issue exists at thread_ops.rs:1612 (approval continuation path).

Fix

Two-pronged: fix the ordering (eliminate the bug) + frontend safety net (handle residual edge cases).

Backend: Move "Done" from thread_ops.rs to agent_loop.rs

  • src/agent/thread_ops.rs: Remove "Done" sends at lines 578-585 and 1612-1619
  • src/agent/agent_loop.rs: Send StatusUpdate::Status("Done") AFTER channels.respond() succeeds in the run() loop (~lines 959-982)

New ordering: persist → respond → Done

Frontend: Safety net for lost events

src/channels/web/static/app.js: Track whether response was received for the current turn. When "Done" arrives without a preceding response, trigger loadHistory() after a 1500ms delay as fallback.

Files

File Change
src/agent/thread_ops.rs Remove "Done" sends (lines 578-585, 1612-1619)
src/agent/agent_loop.rs Add "Done" after channels.respond() in run loop
src/channels/web/static/app.js Turn-tracking state + fallback loadHistory() timer

Verification

  • cargo clippy / cargo test pass
  • Messages render in real-time in web UI (no refresh needed)
  • Browser devtools EventStream shows response arriving before Done

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions