When a relay closes the TCP connection without a clean WebSocket CLOSE
frame, the ws client's cached state remains NOSTR_WS_CONNECTED forever
(it only transitions on a clean close). Previously:
- nostr_ws_ping() send failures were silently ignored (return code
unchecked), so the pool never learned the transport was dead
- the pong-timeout branch marked the relay DISCONNECTED but left the
stale ws_client alive, so ensure_relay_connection() short-circuited
on the cached CONNECTED state and never reconnected
Result: a single relay-side disconnect left the pool with zero live
sockets indefinitely while still reporting all relays connected —
all publishes dropped and all inbound events silently missed.
Fix: on ping-send failure or pong timeout, close and destroy the stale
ws client (ws_client = NULL) and mark the relay DISCONNECTED so the
reconnect logic performs a fresh connect. Recovery now takes one ping
interval + pong timeout + backoff (~1-2 min) instead of forever.
Observed in production on a didactyl agent: relays dropped the
connection at 00:08 UTC; the agent ignored all inbound DMs for 13+
hours while /api/status reported 3/3 connected.