# NousResearch/hermes-agent · WebSocket busy-loop and reconnect failures across adapters

## Open now

- 8 issues, 8 PRs open on GitHub.
- #14539: QQ Bot adapter silently stops reconnecting without notifying gateway
- #15490: qqbot adapter silently dies on network outage during reconnect; gateway has no task watchdog
- #21851: \[Bug\] QQBot: _listen_loop exits after MAX_RECONNECT_ATTEMPTS and never restarts; send() waits 15s for reconnection that cannot succeed
- #25505: QQBot adapter silently dies after max reconnect attempts, never recovers
- #41872: QQ Bot adapter spins at 100% CPU when aiohttp returns WSMsgType.CLOSING
- #49918: fix(wecom): WeCom adapter still spins 100% CPU on silent ws close (related to #28293)
- #55487: WeCom adapter busy-loop consumes 100% CPU on websocket disconnect, freezes all platforms
- #55492: QQBot _read_events has same silent-return pattern as WeCom busy-loop (mitigated but inconsistent)
- PR #15510: fix: notify gateway when QQ bot adapter exhausts reconnect attempts
- PR #17814: fix(qqbot): trigger fatal error + reconnect when max reconnect attempts reached
- PR #21878: fix(qqbot): prevent _listen_loop from exiting silently after MAX_RECONNECT_ATTEMPTS
- PR #41883: fix(qqbot): handle WebSocket CLOSING in _read_events
- PR #49941: fix(wecom): prevent 100% CPU spin on silent websocket close
- PR #55524: fix(qqbot): add post-loop guard in _read_events to prevent backoff reset
- PR #56436: fix(gateway): guard closed websocket in WeCom _read_events to prevent busy-loop (#55487)
- PR #83778: fix(qqbot): handle WSMsgType.CLOSING to stop CPU spin

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.

## TL;DR

- 38 member(s): 8 open issue(s), 7 closed issue(s), 8 open PR(s), 1 merged PR(s), 14 PR(s) closed without merging.
- A fix was merged to main in PR #40574. Verify it addresses your case.
- **Recommended:** read PR #40574 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-40574`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.05`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #14539: QQ Bot adapter silently stops reconnecting without notifying gateway
- #15490: qqbot adapter silently dies on network outage during reconnect; gateway has no task watchdog
- #21851: \[Bug\] QQBot: _listen_loop exits after MAX_RECONNECT_ATTEMPTS and never restarts; send() waits 15s for reconnection that cannot succeed
- #25505: QQBot adapter silently dies after max reconnect attempts, never recovers
- #41872: QQ Bot adapter spins at 100% CPU when aiohttp returns WSMsgType.CLOSING
- #49918: fix(wecom): WeCom adapter still spins 100% CPU on silent ws close (related to #28293)
- #55487: WeCom adapter busy-loop consumes 100% CPU on websocket disconnect, freezes all platforms
- #55492: QQBot _read_events has same silent-return pattern as WeCom busy-loop (mitigated but inconsistent)

## Closed / duplicate issues

- #17703: QQBot stops reconnecting after failed reconnect leaves websocket closed
- #27810: \[Bug\]: QQBot closed WebSocket 导致无限空循环，单线程 CPU 100%
- #31101: QQ Bot adapter: _read_events() silent loop after reconnect failure prevents further retries
- #31193: QQ Bot Reconnect Busy Loop Causes 100% CPU Spin
- #31771: \[Bug\]: QQBot adapter busy-loops after WebSocket reconnect failure, causing 100% CPU
- #42271: fix(qqbot): listen_loop enters permanent dead state after network outage — _read_events() bypasses reconnect logic
- #67470: \[Bug\]: Home Assistant gateway adapter goes silently deaf after transient network failure (same class as Telegram #66377)

## Prior & competing PRs

- **#14565: fix(qqbot): notify gateway via _set_fatal_error when reconnect loop exhausts**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#19414`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#25537: fix(qqbot): notify gateway on max reconnect exhaustion instead of silent death**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#19414`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#32885: fix(qqbot): prevent reconnect stall after closed websocket**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3eeca4613d618618093db416b564a2b9ef8dbe6a`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#39430: fix(qqbot): prevent CPU-spinning tight loop after WebSocket reconnect failure (#17703)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#41773`, `#41773`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#40198: fix(gateway): keep QQBot reconnect loop alive after stale websocket**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3eeca4613d618618093db416b564a2b9ef8dbe6a`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#40497: fix(qqbot): prevent busy-loop when WebSocket is closed but not None**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `8dca28775`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#40574: fix(qqbot): stop 100% CPU spin when WebSocket is closed but not None (#31193, #31771)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/qqbot/adapter.py`, `tests/gateway/test_qqbot.py`
  - fix hunks:
  - `gateway/platforms/qqbot/adapter.py`

    ```diff
    @@ -681,6 +681,12 @@ async def _read_events(self) -> None:
             """Read WebSocket frames until connection closes."""
             if not self._ws:
                 raise RuntimeError("WebSocket not connected")
    +        if self._ws.closed:
    +            # A closed-but-non-None ws makes the while-condition false on entry,
    +            # so this would return normally — which _listen_loop treats as a
    +            # clean read and immediately retries with backoff reset to 0,
    +            # producing a 100% CPU spin. Raise so the reconnect/backoff path runs.
    +            raise RuntimeError("WebSocket closed")
     
             while self._running and self._ws and not self._ws.closed:
                 msg = await self._ws.receive()
    ```
  - `tests/gateway/test_qqbot.py`

    ```diff
    @@ -2196,3 +2196,27 @@ def test_fatal_codes_include_intent_errors(self):
             assert 4014 in fatal_codes
             assert 4001 in fatal_codes
             assert 4915 in fatal_codes
    +
    +
    +class TestReadEventsClosedWsGuard:
    +    """Regression: a closed-but-non-None ws must raise on entry, not return
    +    normally, so _listen_loop goes through reconnect/backoff instead of
    +    busy-looping at 100% CPU (issues #31193 / #31771)."""
    +
    +    def _make_adapter(self, **extra):
    +        from gateway.platforms.qqbot import QQAdapter
    +        return QQAdapter(_make_config(app_id="a", client_secret="b", **extra))
    +
    +    def test_read_events_raises_when_ws_closed_on_entry(self):
    +        adapter = self._make_adapter()
    +        adapter._running = True
    +        adapter._ws = SimpleNamespace(closed=True)
    +        with pytest.raises(RuntimeError):
    +            asyncio.run(adapter._read_events())
    +
    +    def test_read_events_raises_when_ws_none(self):
    +        adapter = self._make_adapter()
    +        adapter._running = True
    +        adapter._ws = None
    +        with pytest.raises(RuntimeError):
    +            asyncio.run(adapter._read_events())
    ```
- **#15510: fix: notify gateway when QQ bot adapter exhausts reconnect attempts**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#14565`, `#14539`, `#15051`
  - files: `gateway/platforms/qqbot/adapter.py`, `plugins/memory/hindsight/__init__.py`
- **#17704: fix(qqbot): keep reconnecting after closed websocket**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3eeca4613d618618093db416b564a2b9ef8dbe6a`, `#17704`, `#31193`, `#31771`, `#40574`
- **#17814: fix(qqbot): trigger fatal error + reconnect when max reconnect attempts reached**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#14565`, `#14539`
  - files: `gateway/platforms/qqbot/adapter.py`
- **#21878: fix(qqbot): prevent _listen_loop from exiting silently after MAX_RECONNECT_ATTEMPTS**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `ec7e92082d15074339c8e7449eebf7b8ede3bd4d`
  - files: `gateway/platforms/qqbot/adapter.py`
- **#27821: fix(qqbot): prevent busy-loop when WebSocket closes without exception**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3eeca4613d618618093db416b564a2b9ef8dbe6a`, `#31193`, `#31771`, `#40574`
- **#31151: fix(qqbot): raise RuntimeError in _read_events when WebSocket is closed**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#17704`, `#17703`, `#25070`, `#20994`
- **#31774: fix(qqbot): prevent listener busy-loop after closed-WebSocket reconnect failure (#31771)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `3eeca4613d618618093db416b564a2b9ef8dbe6a`, `#31771`, `#40574`
- **#41883: fix(qqbot): handle WebSocket CLOSING in _read_events**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `20293ce129247da43973a7200cc36e42afe50c74`
  - files: `gateway/platforms/qqbot/adapter.py`, `hermes_cli/main.py`, `tests/cli/test_cli_provider_resolution.py`, `tests/gateway/test_qqbot.py`
- **#41937: fix(qqbot): handle WSMsgType.CLOSING to prevent CPU spin (#41872)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#41883`, `#41872`
- **#49941: fix(wecom): prevent 100% CPU spin on silent websocket close**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `plugins/platforms/wecom/adapter.py`
- **#55524: fix(qqbot): add post-loop guard in _read_events to prevent backoff reset**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `gateway/platforms/qqbot/adapter.py`, `tests/gateway/test_qqbot.py`
- **#55601: fix(qqbot): raise on silent loop exit to keep _listen_loop consistent**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#55524`, `#55492`, `#55486`, `#55524`
- **#56436: fix(gateway): guard closed websocket in WeCom _read_events to prevent busy-loop (#55487)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `gateway/platforms/wecom.py`
- **#83778: fix(qqbot): handle WSMsgType.CLOSING to stop CPU spin**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `gateway/platforms/qqbot/adapter.py`, `tests/gateway/test_qqbot.py`
- **#99156: fix(qqbot): notify gateway when max reconnect attempts exhausted instead of silent return**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#99156`
- **#124422: fix(qqbot): keep _listen_loop alive past MAX_RECONNECT_ATTEMPTS closes (#21851)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#21851`

## Fix status

- Merged to main in PR #40574 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-07`.
- Subsystem: `gateway`
