Problem
At fork audit ref 1bdf088e, public/app.js::connectWS() reconnects only from onclose. A proxy/NAT drop, sleep/resume, or stuck handshake can leave the browser-side socket apparently open but silent, so all views sharing it stop receiving live updates indefinitely. The server sends protocol-level WebSocket pings every 30 seconds in cmd/server/websocket.go, but browser JavaScript cannot observe those frames.
Upstream reference: Kpa-clawbot/CoreScope#2020. This needs an adapted port, not a blind cherry-pick.
Acceptance criteria
- The server emits a small application-level heartbeat from its existing single WebSocket writer loop, without adding a competing writer or changing dead-client handling.
- The client tracks liveness from socket creation; any received frame refreshes it. A socket or handshake silent beyond a documented threshold is replaced exactly once.
onclose, the watchdog, online, visible-tab resume, and pull-to-reconnect cannot stack reconnect timers or leave multiple live sockets.
- Old handlers are detached before intentional replacement.
- Heartbeats are consumed before logo pulses, cache invalidation, pause buffers, or
onWS listeners. Normal packet messages remain unchanged.
- Preserve the configured reconnect delay for ordinary close events and document old-tab behavior during a rolling static-asset update.
Tests
- Deterministic fake-clock/fake-WebSocket tests for a silent OPEN socket, never-open handshake, traffic/heartbeat refresh, clock steps, online/visibility resume, ordinary close, and repeated/pull reconnect races.
- Go tests for the heartbeat cadence and the existing protocol ping behavior.
- Register tests locally and in CI without changing workflow triggers, permissions, or fork guards.
Non-goals
No deploy, publish, release, or staging/production contact. Do not broaden this into unrelated WebSocket UI work.
Problem
At fork audit ref
1bdf088e,public/app.js::connectWS()reconnects only fromonclose. A proxy/NAT drop, sleep/resume, or stuck handshake can leave the browser-side socket apparently open but silent, so all views sharing it stop receiving live updates indefinitely. The server sends protocol-level WebSocket pings every 30 seconds incmd/server/websocket.go, but browser JavaScript cannot observe those frames.Upstream reference: Kpa-clawbot/CoreScope#2020. This needs an adapted port, not a blind cherry-pick.
Acceptance criteria
onclose, the watchdog,online, visible-tab resume, and pull-to-reconnect cannot stack reconnect timers or leave multiple live sockets.onWSlisteners. Normal packet messages remain unchanged.Tests
Non-goals
No deploy, publish, release, or staging/production contact. Do not broaden this into unrelated WebSocket UI work.