Skip to content

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

Description

@nico7master

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

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)

  1. Raise the handshake timeout so busy instances don't spuriously degrade:
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:
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

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