#1840·agent-zero

Chat click sometimes shows loading splash forever: state_push silently dropped in DEGRADED mode + phantom polling fallback

Author: nico7masterCreated Aug 19, 2026Updated Sep 4, 2026

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

  1. Have a busy instance (many chat contexts, agent tasks running).
  2. Click a chat in the sidebar. Intermittently (more likely under load), the loading splash appears and never clears.
  3. 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

javascript
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

javascript
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)

  1. Raise the handshake timeout so busy instances don't spuriously degrade:
javascript
response = await stateSocket.request("state_request", payload, { timeoutMs: 10000 });
  1. 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:
javascript
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