b89fb608b6
When any of the read/write/heartbeat loops ends, we were cancelling remaining tasks but not explicitly closing the WebSocket. This caused the connection to be dropped with 1006 abnormal closure instead of a clean 1000 close. The frontend then reconnected, creating a loop.