The relay pool had the same class of bugs as the C lib's relay pool
(fixed there in core_relay_pool.c), plus a few unique ones:
1. No reconnection at all. ReconnectConfig was defined and stored but
never read — dead configuration. The run() event loop skipped
non-connected relays forever; once a relay dropped, it never came
back. Now run() attempts reconnection of any non-connected relay
after exponential backoff (initial 1s, x2, max 60s, reset after 60s
stable), honoring max_reconnect_attempts.
2. send_text()/ping() failures left the ws state as Connected (same
bug as the C lib). A relay that dropped the TCP connection kept
reporting Connected and every publish silently failed. Both now
set state to Error on failure.
3. Error-state trap: subscribe() and publish_async() only auto-
connected from Disconnected, but receive_text() sets Error on
transport failure — so a relay that died mid-session was stuck
permanently, even for publishing. Both now reconnect from any
non-Connected state.
4. Stale RelayEntry.status: set to Disconnected in add_relay() and
never updated again, so list_relays() reported Disconnected for
relays connected for days. run() now syncs pool status from the
live ws state each iteration.
5. No health monitoring: PING_INTERVAL_SECS was defined but unused,
ping_latency stats never populated, half-open connections never
probed. run() now sends pings on the configured interval, detects
pong timeouts and ping-send failures, and force-closes dead
transports so the reconnect path picks them up.
Tests: 5 new tests (backoff calculation with default and custom
configs, initial status, connect-failure Error state, entry defaults).
Full workspace suite passes: 210 tests, 0 failures.