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
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 theresponseSSE event, separated by two DB writes and a function return boundary.The
responseSSE 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 (BroadcastStreamsilently drops onErr(_) => Noneatsse.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.rstoagent_loop.rssrc/agent/thread_ops.rs: Remove "Done" sends at lines 578-585 and 1612-1619src/agent/agent_loop.rs: SendStatusUpdate::Status("Done")AFTERchannels.respond()succeeds in therun()loop (~lines 959-982)New ordering:
persist → respond → DoneFrontend: Safety net for lost events
src/channels/web/static/app.js: Track whetherresponsewas received for the current turn. When "Done" arrives without a precedingresponse, triggerloadHistory()after a 1500ms delay as fallback.Files
src/agent/thread_ops.rssrc/agent/agent_loop.rschannels.respond()in run loopsrc/channels/web/static/app.jsloadHistory()timerVerification
cargo clippy/cargo testpassresponsearriving beforeDone