* fix(dashboard): add events-feed reconnect policy helpers
Extract the reconnect arithmetic and close-code classification for the
ChatSidebar /api/events socket into a pure module so both can be tested
without a fake WebSocket or a mounted component.
Two decisions live here rather than inline in the effect:
- `shouldRetryEventsClose` — 1000 (normal) and 4401/4403 (auth) are
terminal; everything else, including 1005/1006 from a killed gateway
or a dropped network, is retryable.
- `isEventsFeedMessage` — the sidebar's banner is shared with
`info.credential_warning` and the JSON-RPC sidecar, so a reconnect may
only clear a message the events feed wrote itself.
Co-authored-by: Ishan Parihar <ishan@supreme-god.dev>
Co-authored-by: Vyre <vyre@ishanparihar.com>
Co-authored-by: eric-senyao <178080753+eric-senyao@users.noreply.github.com>
* fix(dashboard): auto-reconnect the events WebSocket with backoff
The chat sidebar's /api/events subscriber surfaced a static "disconnected"
banner on a transient drop and never retried, so a gateway restart or a
network blip left the feed dead until the user reloaded the page. The feed
drives the live chat title (session.info) and dashboard.new_session_requested,
both of which silently stopped working.
Reconnect with exponential backoff (1s → 2s → 4s → … → 30s cap, 15 attempts
then a terminal banner). Specifically:
- One scheduling path. `close` always follows `error` for a failed socket,
so scheduling from both — as the superseded PRs did — queues two timers
and leaks the one that is no longer tracked for cleanup. `scheduleReconnect`
returns early when a retry is already pending.
- The auth ticket is re-minted per attempt via `buildWsUrl`; tickets are
single-use with a short TTL, so replaying the first URL would 4401 on the
second attempt.
- A superseded socket's late close cannot schedule a retry on top of its
replacement (`isCurrent` generation check).
- A successful open resets the backoff and clears only the events feed's own
banner, leaving a credential warning or sidecar error visible.
- The pending timer is cleared on unmount, not merely neutered by the
`unmounting` flag.
Also retires two strings the tools box left behind when it was removed in
47fccc073 (#51737): the banner no longer promises "tool calls may not
appear" and the button reads "reconnect events feed".
Co-authored-by: Ishan Parihar <ishan@supreme-god.dev>
Co-authored-by: Vyre <vyre@ishanparihar.com>
Co-authored-by: eric-senyao <178080753+eric-senyao@users.noreply.github.com>
* test(dashboard): cover the events-feed reconnect bug class
Fake-timer coverage for the behaviors the superseded PRs changed without
tests. Each case was mutation-checked — reverting the corresponding guard
in ChatSidebar.tsx makes exactly that test fail:
- transient close reconnects, and backoff grows 1s → 2s → 4s
- error + close on one socket schedules ONE retry, not two
- a successful open resets the backoff to 1s
- 4401/4403 and a normal 1000 close never retry
- the attempt cap stops the loop instead of retrying forever
- reconnect clears the feed's own banner but not a credential warning
- unmount clears the pending timer (asserted via `vi.getTimerCount()`,
since the `unmounting` flag alone hides a leaked timer)
Co-authored-by: Ishan Parihar <ishan@supreme-god.dev>
Co-authored-by: Vyre <vyre@ishanparihar.com>
Co-authored-by: eric-senyao <178080753+eric-senyao@users.noreply.github.com>
* fix(dashboard): stop the events feed overwriting a foreign banner
Review catch: `clearEventsBanner` guarded the shared banner but `surface`
did not, so the guard was only half applied. A sidecar error or
`credential_warning` already on screen when the feed dropped was replaced
by "events feed disconnected" — and lost for good, since `error` is that
message's only home and the sidecar does not re-emit.
`surface` now writes only over an empty banner or one of the feed's own
messages. Declining to write does not affect the retry itself; the
reconnect still runs on schedule, it just stays silent while a more
important message holds the banner.
Both directions are covered: a foreign banner survives a drop, and the
reconnect still fires while suppressed.
---------
Co-authored-by: Ishan Parihar <ishan@supreme-god.dev>
Co-authored-by: Vyre <vyre@ishanparihar.com>
Co-authored-by: eric-senyao <178080753+eric-senyao@users.noreply.github.com>