Skip to content

Chat can wait forever for the initial history frame (no deadline, no retry) #956

Description

@clemenshelm

Symptom

Reported on the production PWA, 2026-07-28. On mobile, several agents were opened one after another; each stayed on the loading indicator indefinitely — no error, no timeout, no retry offered. A reload of the PWA on the desktop was followed immediately by the agents appearing on mobile.

The defect that holds regardless of root cause

The client's wait for initial history has no deadline.

hasInitialContent (use-ws-runtime.ts:498) is what takes the chat out of "starting", and it flips only when a history frame arrives — either with messages or with the authoritative sessionKnown: true empty signal added for #197. Nothing in use-ws-runtime.ts arms a timer for that frame; the timers present (delayTimerRef, ack timers, reconnect backoff) all cover sending, not the initial load.

So if the frame is lost for any reason, the UI parks on the loading indicator forever: no error state, no automatic retry, no user-visible way out except a manual reload. That is a real gap independent of which cause dropped the frame, and it is what turned a transient fault into an indefinite dead end here.

Why "the server just didn't answer" is unlikely

ClientRouter's history path (client-router.ts:956-1058) sends a history frame on every branch:

  • messages found → history with messages
  • empty but session known → history with sessionKnown: true
  • waitForConnection() rejects (10 s, CONNECTION_TIMEOUT_MS) → catch → retry once → history either way
  • OpenClaw not connected and session unknown → history with messages: []
  • otherwise → greeting

There is no path that returns without answering. So the likelier reading is that the frame never reached the device, or the request never reached the server — not that the server declined to send one.

Candidate causes — none confirmed

  1. Dead-but-unreported WebSocket after PWA resume. A backgrounded PWA socket can be torn down by the OS without a close event reaching the page. The client believes it is connected, sends the history request into a void, and — with no deadline — waits forever. The extensive lifecycle handling (visibilitychange, pageshow, freeze, resume, use-ws-runtime.ts:1518-1522) exists precisely for this, so if this is it, the gap is in detection, not in intent.

  2. Shared server-side OpenClaw client down. getOpenClawClient() (server/openclaw-client.ts) is a single process-wide instance on globalThis, shared by every user and connection. While it is disconnected, every history request degrades — and when it reconnects, all sessions recover simultaneously. That would explain the "fixed everywhere at once" shape, but not why the desktop reload would be the trigger.

  3. Coincidence. The desktop reload may simply have happened around the time the underlying link recovered on its own. Worth stating explicitly: one observation of "I did X, then Y happened" is not causation, and this one has an obvious confound.

Observation 2 vs 3 is decidable from production logs (Pinchy's OpenClaw client connect/disconnect timestamps vs the recovery moment); observation 1 shows up as a browser-side socket with no matching server-side connection. Production has deliberately not been inspected for this report.

Suggested work

  • Arm a deadline on the initial history wait and surface a real state when it expires: an error with a Retry, reusing the existing retry affordances rather than inventing a new one. This is the part worth doing whatever the root cause turns out to be.
  • Consider an application-level ping/pong or a "history requested but unanswered" watchdog that forces a reconnect, so a silently dead socket self-heals instead of waiting on the user.
  • Add an E2E case for "history frame never arrives" — today nothing covers it, since every test's server answers.

Impact

User-visible and confusing: the app looks broken with no explanation and no path forward except knowing to reload. Not a blocker for 0.9.0 — it needs a transient fault to trigger, and reloading recovers it.

Found during 0.9.0 Flow-5 testing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions