Summary
Sometimes clicking a chat in the sidebar shows the loading splash indefinitely — the chat never renders. The only recovery is clicking a different chat and back, which always works. The server actually delivers the snapshot, but the client silently discards it.
Reproduction
- Have a busy instance (many chat contexts, agent tasks running).
- Click a chat in the sidebar. Intermittently (more likely under load), the loading splash appears and never clears.
- Click another chat, then back to the first — it loads instantly.
Intermittent because the trigger is a timing race (see below): the handshake ACK must exceed 2 seconds.
Root cause
Two cooperating defects in webui/components/sync/sync-store.js:
Defect 1 — 2s handshake timeout is too aggressive for busy instances
response = await stateSocket.request("state_request", payload, { timeoutMs: 2000 });
On a busy self-hosted instance (100+ contexts), building/ACKing the state handshake can exceed 2s. The client then transitions to DEGRADED mode (_setMode(SYNC_MODES.DEGRADED, "state_request failed")).
Defect 2 — all state_push events are dropped while DEGRADED
async _handlePush(envelope) {
if (this.mode === SYNC_MODES.DEGRADED) {
debug("[syncStore] ignoring state_push while DEGRADED");
return; // ← the snapshot containing the requested chat is discarded here
}
...
The server sends the state_push containing the chat messages anyway, but the client drops it. The chat-loading splash only clears inside applySnapshot (finishChatLoading(snapshot.context) in webui/index.js), so it spins forever.
Clicking another chat calls sendStateRequest again — a fresh handshake that succeeds within 2s, mode returns to HEALTHY, pushes are processed again. That is why the "click away and back" workaround always fixes it.
Bonus inconsistency: the DEGRADED toast says "WebSocket connection problems - using polling fallback", but there is no polling fallback in the sync layer — /poll was removed in the websocket migration. The promised fallback does not exist, so DEGRADED mode does not degrade gracefully; it starves the UI.
Proposed fixes (both tested locally)
- Raise the handshake timeout so busy instances don't spuriously degrade:
response = await stateSocket.request("state_request", payload, { timeoutMs: 10000 });
- Process pushes while DEGRADED instead of dropping them. Pushes are self-validating: the existing
runtime_epoch mismatch, seq-gap, and log_guid guards in _handlePush detect divergence and trigger a proper resync — processing a stale push is safe and strictly better than starving the UI:
async _handlePush(envelope) {
if (this.mode === SYNC_MODES.DEGRADED) {
debug("[syncStore] state_push received while DEGRADED (processing)");
}
...
Optionally also fix the misleading toast text (there is no polling fallback), or implement an actual poll fallback for DEGRADED mode.
Before/after
| Scenario |
Before |
After |
| Chat click, ACK < 2s |
Loads |
Loads (unchanged) |
| Chat click, ACK > 2s (busy server) |
DEGRADED → snapshot dropped → splash forever |
ACK allowed 10s; even if DEGRADED, snapshot is processed → chat loads |
| Stale push while DEGRADED |
Dropped (UI starves) |
Processed → guards trigger proper resync |
Environment
- Agent Zero: current Docker image (
agent0ai/agent-zero), self-hosted, ~100+ chat contexts
- Reproduces in any Chromium browser; load-dependent
Summary
Sometimes clicking a chat in the sidebar shows the loading splash indefinitely — the chat never renders. The only recovery is clicking a different chat and back, which always works. The server actually delivers the snapshot, but the client silently discards it.
Reproduction
Intermittent because the trigger is a timing race (see below): the handshake ACK must exceed 2 seconds.
Root cause
Two cooperating defects in
webui/components/sync/sync-store.js:Defect 1 — 2s handshake timeout is too aggressive for busy instances
On a busy self-hosted instance (100+ contexts), building/ACKing the state handshake can exceed 2s. The client then transitions to
DEGRADEDmode (_setMode(SYNC_MODES.DEGRADED, "state_request failed")).Defect 2 — all state_push events are dropped while DEGRADED
The server sends the
state_pushcontaining the chat messages anyway, but the client drops it. The chat-loading splash only clears insideapplySnapshot(finishChatLoading(snapshot.context)inwebui/index.js), so it spins forever.Clicking another chat calls
sendStateRequestagain — a fresh handshake that succeeds within 2s, mode returns to HEALTHY, pushes are processed again. That is why the "click away and back" workaround always fixes it.Bonus inconsistency: the DEGRADED toast says "WebSocket connection problems - using polling fallback", but there is no polling fallback in the sync layer —
/pollwas removed in the websocket migration. The promised fallback does not exist, so DEGRADED mode does not degrade gracefully; it starves the UI.Proposed fixes (both tested locally)
runtime_epochmismatch, seq-gap, andlog_guidguards in_handlePushdetect divergence and trigger a proper resync — processing a stale push is safe and strictly better than starving the UI:Optionally also fix the misleading toast text (there is no polling fallback), or implement an actual poll fallback for DEGRADED mode.
Before/after
Environment
agent0ai/agent-zero), self-hosted, ~100+ chat contexts