f529986abf
Follow-up to the cherry-picked watchdog from #112052 (@KoNit-K), finishing the class the reporter of #112049 laid out: - `_websocket_loop` runs the read loop and the discovery sweep as sibling tasks and ends the connection when EITHER finishes. The discovery sweep re-raises `ConnectionClosed` instead of logging it and retrying next tick: a send that sees the socket closed is proof the read the loop is parked on will never return. That is exactly the traceback the reporter watched for 22-86 h while inbound stayed silent. - Health is invalidated while reconnecting: the first disconnect publishes `retrying` (`_mark_degraded`) and a successful re-subscribe publishes `connected` again. Before, `connect()` wrote "connected" once and nothing ever changed it, so `/health/detailed` claimed delivery during the silence. - The teardown awaits both tasks with `gather(return_exceptions=True)` instead of a bare `except (CancelledError, Exception): pass`, which could swallow a `disconnect()` cancellation landing mid-teardown. - Slims the salvaged read-loop hunk: the extra "receive task remained parked" warning and the in-loop `_mark_degraded()` are dropped; the reconnect log line and the loop-level health flip cover both. Docs: the Buzz page still described inbound as poll-only and the WebSocket transport as a future optimization; it now describes the watchdog and the `retrying` health state.