You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A session view can get stuck on "Loading history…" forever: no error is shown, and nothing recovers it except a full page reload. The session state machine leaves openState === "loading" permanently when the connection generation is invalidated while events.open() is still in flight.
Observed on the web surface viewed from a mobile browser over a remote connection; the triggering condition is a WebSocket drop landing inside an in-flight open.
Version: 0.1.5-rc.2 (@deepseek-ai/dsh, web profile)
openState === "error" renders chat.loadError with message + code
Because the state never leaves "loading", the error branch is never reached and the user sees an endless spinner with no diagnostic. The only recovery is reloading the page.
Root cause
In @deepseek-ai/dsh-api-session-controller (repo: packages/api/session-controller). Line numbers below are from the published lib/client.js of 0.1.5-rc.2:
The two stale-generation guards (A) and (B)return without writing any terminal state. openState was set to "loading" on entry and is now never advanced:
If the open succeeded but the generation moved on → guard (A) fires → stuck at "loading".
If the open failed and the generation moved on → guard (B) fires → stuck at "loading" (not even "error").
Note the final markDirty() in finally is also skipped when the generation changed, so the stale state is not even re-published.
Nothing recovers it
There is no timeout around events.open(). A grep for setTimeout in lib/client.js only finds debounce/catalog/schedule uses — no watchdog on the open path.
Nothing re-invokes open() when the generation moves on. resync() (lib/client.js:1858) is only called from configureSubagent() when a subagent address changes, not on a connection-generation change.
Session.open() (lib/client.js:1791) is itself idempotent and would recover the session — it returns early only when openState === "open", and the in-flight openPromise has already been cleared by its own .finally. But nothing calls it again, so the stuck state persists.
Why it is easy to hit on mobile
Any WebSocket drop produces a new connection generation, so any drop that lands inside an in-flight open strands the view. A desktop browser on loopback stays connected and rarely hits this; a mobile browser is suspended whenever it is backgrounded or the screen locks, so the drop frequently lands inside the open — especially for long sessions, where events.open({ maxMessages: 50 }) takes longer and widens the window.
Suggested fixes
Any one of these closes the hole; ideally (1) plus (3):
Never leave "loading" on a stale generation. In guards (A)/(B), transition to a terminal or retryable state (e.g. "error", or a new "stalled" state) instead of returning silently.
Hand off instead of dropping. When the generation changed mid-open, re-drive the open for the current generation rather than returning.
Re-open sessions in "loading" on a connection-generation change — the session controller currently does not listen for that transition at all.
Optionally add a UI-side watchdog: if openState === "loading" has not changed for N seconds, surface a retry affordance instead of an endless spinner.
Workaround for users
Reloading the page re-drives open(), which clears the stuck state. The existing "Disconnected, reconnect now" / connection.reconnect() affordance does not help here, because it only appears when the connection itself is unhealthy — in this case the connection is fine and only the session open is stale.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
A session view can get stuck on "Loading history…" forever: no error is shown, and nothing recovers it except a full page reload. The session state machine leaves
openState === "loading"permanently when the connection generation is invalidated whileevents.open()is still in flight.Observed on the web surface viewed from a mobile browser over a remote connection; the triggering condition is a WebSocket drop landing inside an in-flight open.
0.1.5-rc.2(@deepseek-ai/dsh, web profile)tailscale serve)Symptom
The chat view shows the loading hint indefinitely:
openState === "loading"renderschat.loadingHistory("Loading history…"/"载入历史…")openState === "error"renderschat.loadErrorwith message + codeBecause the state never leaves
"loading", the error branch is never reached and the user sees an endless spinner with no diagnostic. The only recovery is reloading the page.Root cause
In
@deepseek-ai/dsh-api-session-controller(repo:packages/api/session-controller). Line numbers below are from the publishedlib/client.jsof0.1.5-rc.2:The two stale-generation guards (A) and (B)
returnwithout writing any terminal state.openStatewas set to"loading"on entry and is now never advanced:"loading"."loading"(not even"error").Note the final
markDirty()infinallyis also skipped when the generation changed, so the stale state is not even re-published.Nothing recovers it
events.open(). A grep forsetTimeoutinlib/client.jsonly finds debounce/catalog/schedule uses — no watchdog on the open path.open()when the generation moves on.resync()(lib/client.js:1858) is only called fromconfigureSubagent()when a subagent address changes, not on a connection-generation change.Session.open()(lib/client.js:1791) is itself idempotent and would recover the session — it returns early only whenopenState === "open", and the in-flightopenPromisehas already been cleared by its own.finally. But nothing calls it again, so the stuck state persists.Why it is easy to hit on mobile
Any WebSocket drop produces a new connection generation, so any drop that lands inside an in-flight open strands the view. A desktop browser on loopback stays connected and rarely hits this; a mobile browser is suspended whenever it is backgrounded or the screen locks, so the drop frequently lands inside the open — especially for long sessions, where
events.open({ maxMessages: 50 })takes longer and widens the window.Suggested fixes
Any one of these closes the hole; ideally (1) plus (3):
"loading"on a stale generation. In guards (A)/(B), transition to a terminal or retryable state (e.g."error", or a new"stalled"state) instead of returning silently."loading"on a connection-generation change — the session controller currently does not listen for that transition at all.openState === "loading"has not changed for N seconds, surface a retry affordance instead of an endless spinner.Workaround for users
Reloading the page re-drives
open(), which clears the stuck state. The existing "Disconnected, reconnect now" /connection.reconnect()affordance does not help here, because it only appears when the connection itself is unhealthy — in this case the connection is fine and only the session open is stale.All reactions