# NousResearch/hermes-agent · QQBot closed WebSocket causes busy-loop, stalled reconnects

## Open now

- 1 issue, 1 PR open on GitHub.
- #55492: QQBot _read_events has same silent-return pattern as WeCom busy-loop (mitigated but inconsistent)
- PR #55524: fix(qqbot): add post-loop guard in _read_events to prevent backoff reset

Members consistently concern the QQBot adapter's \`_read_events\`/\`_listen_loop\` misbehaving when the WebSocket is closed but not None after a network outage or an unsuccessful reconnect, reported as a 100% CPU busy-loop or reconnection stopping. Many are proposed fixes, such as raising an error or adding guards, and some issues mention a similar WeCom pattern.

## TL;DR

- 18 member(s): 1 open issue(s), 6 closed issue(s), 1 open PR(s), 1 merged PR(s), 9 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.14`: 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

- #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

## Prior & competing PRs

- **#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`
  - _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`
  - 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())
    ```
- **#17704: fix(qqbot): keep reconnecting after closed websocket**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3eeca4613d618618093db416b564a2b9ef8dbe6a`, `#17704`, `#31193`, `#31771`, `#40574`
- **#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`
- **#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`

## 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`
