{"repo":"NousResearch/hermes-agent","public_id":"14539","display_label":"WebSocket busy-loop and reconnect failures across adapters","summary":"These issues and fixes address CPU-spinning busy-loops and silent dead states caused by closed or closing WebSocket connections in QQBot, WeCom, and Home Assistant adapters. Common themes include improper handling of WebSocket CLOSING states, reconnect logic that silently exits or never restarts, and missing watchdogs or error propagation.","subsystem":"gateway","counts":{"members":38,"open_issues":8,"open_issues_distinct":8,"open_prs":8,"closed_issues":7,"merged_prs":1,"attempt_prs":22},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":40574},"resolution":{"state":"partially_resolved","by":"pr-40574"},"freshness":{"state":"verified","last_verified_at":"2026-06-07T01:44:45+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.05467994310099573},"fix_edges":[{"issue_number":17703,"pr_number":40574,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":27810,"pr_number":40574,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":31193,"pr_number":40574,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":31771,"pr_number":40574,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":14539,"title":"QQ Bot adapter silently stops reconnecting without notifying gateway","url":null,"state":"open"},{"kind":"issue","number":15490,"title":"qqbot adapter silently dies on network outage during reconnect; gateway has no task watchdog","url":null,"state":"open"},{"kind":"issue","number":17703,"title":"QQBot stops reconnecting after failed reconnect leaves websocket closed","url":null,"state":"closed"},{"kind":"issue","number":21851,"title":"[Bug] QQBot: _listen_loop exits after MAX_RECONNECT_ATTEMPTS and never restarts; send() waits 15s for reconnection that cannot succeed","url":null,"state":"open"},{"kind":"issue","number":25505,"title":"QQBot adapter silently dies after max reconnect attempts, never recovers","url":null,"state":"open"},{"kind":"issue","number":27810,"title":"[Bug]: QQBot closed WebSocket 导致无限空循环，单线程 CPU 100%","url":null,"state":"closed"},{"kind":"issue","number":31101,"title":"QQ Bot adapter: _read_events() silent loop after reconnect failure prevents further retries","url":null,"state":"closed"},{"kind":"issue","number":31193,"title":"QQ Bot Reconnect Busy Loop Causes 100% CPU Spin","url":null,"state":"closed"},{"kind":"issue","number":31771,"title":"[Bug]: QQBot adapter busy-loops after WebSocket reconnect failure, causing 100% CPU","url":null,"state":"closed"},{"kind":"issue","number":41872,"title":"QQ Bot adapter spins at 100% CPU when aiohttp returns WSMsgType.CLOSING","url":null,"state":"open"},{"kind":"issue","number":42271,"title":"fix(qqbot): listen_loop enters permanent dead state after network outage — _read_events() bypasses reconnect logic","url":null,"state":"closed"},{"kind":"issue","number":49918,"title":"fix(wecom): WeCom adapter still spins 100% CPU on silent ws close (related to #28293)","url":null,"state":"open"},{"kind":"issue","number":55487,"title":"WeCom adapter busy-loop consumes 100% CPU on websocket disconnect, freezes all platforms","url":null,"state":"open"},{"kind":"issue","number":55492,"title":"QQBot _read_events has same silent-return pattern as WeCom busy-loop (mitigated but inconsistent)","url":null,"state":"open"},{"kind":"issue","number":67470,"title":"[Bug]: Home Assistant gateway adapter goes silently deaf after transient network failure (same class as Telegram #66377)","url":null,"state":"closed"},{"kind":"pr","number":14565,"title":"fix(qqbot): notify gateway via _set_fatal_error when reconnect loop exhausts","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19414","number":19414}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":15510,"title":"fix: notify gateway when QQ bot adapter exhausts reconnect attempts","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#14565","number":14565},{"kind":"pr","value":"#14539","number":14539},{"kind":"pr","value":"#15051","number":15051}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":17704,"title":"fix(qqbot): keep reconnecting after closed websocket","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3eeca4613d618618093db416b564a2b9ef8dbe6a","number":0},{"kind":"pr","value":"#17704","number":17704},{"kind":"pr","value":"#31193","number":31193},{"kind":"pr","value":"#31771","number":31771},{"kind":"pr","value":"#40574","number":40574}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":17814,"title":"fix(qqbot): trigger fatal error + reconnect when max reconnect attempts reached","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#14565","number":14565},{"kind":"pr","value":"#14539","number":14539}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":21878,"title":"fix(qqbot): prevent _listen_loop from exiting silently after MAX_RECONNECT_ATTEMPTS","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"ec7e92082d15074339c8e7449eebf7b8ede3bd4d","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":25537,"title":"fix(qqbot): notify gateway on max reconnect exhaustion instead of silent death","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19414","number":19414}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":27821,"title":"fix(qqbot): prevent busy-loop when WebSocket closes without exception","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3eeca4613d618618093db416b564a2b9ef8dbe6a","number":0},{"kind":"pr","value":"#31193","number":31193},{"kind":"pr","value":"#31771","number":31771},{"kind":"pr","value":"#40574","number":40574}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":31151,"title":"fix(qqbot): raise RuntimeError in _read_events when WebSocket is closed","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17704","number":17704},{"kind":"pr","value":"#17703","number":17703},{"kind":"pr","value":"#25070","number":25070},{"kind":"pr","value":"#20994","number":20994}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":31774,"title":"fix(qqbot): prevent listener busy-loop after closed-WebSocket reconnect failure (#31771)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3eeca4613d618618093db416b564a2b9ef8dbe6a","number":0},{"kind":"pr","value":"#31771","number":31771},{"kind":"pr","value":"#40574","number":40574}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":32885,"title":"fix(qqbot): prevent reconnect stall after closed websocket","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3eeca4613d618618093db416b564a2b9ef8dbe6a","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":39430,"title":"fix(qqbot): prevent CPU-spinning tight loop after WebSocket reconnect failure (#17703)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#41773","number":41773},{"kind":"pr","value":"#41773","number":41773}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":40198,"title":"fix(gateway): keep QQBot reconnect loop alive after stale websocket","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3eeca4613d618618093db416b564a2b9ef8dbe6a","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":40497,"title":"fix(qqbot): prevent busy-loop when WebSocket is closed but not None","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"8dca28775","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":40574,"title":"fix(qqbot): stop 100% CPU spin when WebSocket is closed but not None (#31193, #31771)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/qqbot/adapter.py","tests/gateway/test_qqbot.py"],"fix_hunks":[{"file":"gateway/platforms/qqbot/adapter.py","hunks":["@@ -681,6 +681,12 @@ async def _read_events(self) -> None:\n         \"\"\"Read WebSocket frames until connection closes.\"\"\"\n         if not self._ws:\n             raise RuntimeError(\"WebSocket not connected\")\n+        if self._ws.closed:\n+            # A closed-but-non-None ws makes the while-condition false on entry,\n+            # so this would return normally — which _listen_loop treats as a\n+            # clean read and immediately retries with backoff reset to 0,\n+            # producing a 100% CPU spin. Raise so the reconnect/backoff path runs.\n+            raise RuntimeError(\"WebSocket closed\")\n \n         while self._running and self._ws and not self._ws.closed:\n             msg = await self._ws.receive()"]},{"file":"tests/gateway/test_qqbot.py","hunks":["@@ -2196,3 +2196,27 @@ def test_fatal_codes_include_intent_errors(self):\n         assert 4014 in fatal_codes\n         assert 4001 in fatal_codes\n         assert 4915 in fatal_codes\n+\n+\n+class TestReadEventsClosedWsGuard:\n+    \"\"\"Regression: a closed-but-non-None ws must raise on entry, not return\n+    normally, so _listen_loop goes through reconnect/backoff instead of\n+    busy-looping at 100% CPU (issues #31193 / #31771).\"\"\"\n+\n+    def _make_adapter(self, **extra):\n+        from gateway.platforms.qqbot import QQAdapter\n+        return QQAdapter(_make_config(app_id=\"a\", client_secret=\"b\", **extra))\n+\n+    def test_read_events_raises_when_ws_closed_on_entry(self):\n+        adapter = self._make_adapter()\n+        adapter._running = True\n+        adapter._ws = SimpleNamespace(closed=True)\n+        with pytest.raises(RuntimeError):\n+            asyncio.run(adapter._read_events())\n+\n+    def test_read_events_raises_when_ws_none(self):\n+        adapter = self._make_adapter()\n+        adapter._running = True\n+        adapter._ws = None\n+        with pytest.raises(RuntimeError):\n+            asyncio.run(adapter._read_events())\n"]}]},{"kind":"pr","number":41883,"title":"fix(qqbot): handle WebSocket CLOSING in _read_events","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"20293ce129247da43973a7200cc36e42afe50c74","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":41937,"title":"fix(qqbot): handle WSMsgType.CLOSING to prevent CPU spin (#41872)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#41883","number":41883},{"kind":"pr","value":"#41872","number":41872}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":49941,"title":"fix(wecom): prevent 100% CPU spin on silent websocket close","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":55524,"title":"fix(qqbot): add post-loop guard in _read_events to prevent backoff reset","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":55601,"title":"fix(qqbot): raise on silent loop exit to keep _listen_loop consistent","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#55524","number":55524},{"kind":"pr","value":"#55492","number":55492},{"kind":"pr","value":"#55486","number":55486},{"kind":"pr","value":"#55524","number":55524}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":56436,"title":"fix(gateway): guard closed websocket in WeCom _read_events to prevent busy-loop (#55487)","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":83778,"title":"fix(qqbot): handle WSMsgType.CLOSING to stop CPU spin","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":99156,"title":"fix(qqbot): notify gateway when max reconnect attempts exhausted instead of silent return","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#99156","number":99156}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":124422,"title":"fix(qqbot): keep _listen_loop alive past MAX_RECONNECT_ATTEMPTS closes (#21851)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#21851","number":21851}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}