{"repo":"NousResearch/hermes-agent","public_id":"52914","display_label":"QQAdapter.connect() missing is_reconnect parameter bug","summary":"Nearly all members concern a single bug where QQAdapter.connect() lacks the is_reconnect keyword argument required by the base class, causing reconnect failures. One unrelated member addresses filesystem preview confinement in the dashboard.","subsystem":"dashboard","counts":{"members":28,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":11,"merged_prs":2,"attempt_prs":15},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":61767},"resolution":{"state":"partially_resolved","by":"pr-61767"},"freshness":{"state":"verified","last_verified_at":"2026-07-10T02:09:39+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.09746031746031743},"fix_edges":[{"issue_number":46621,"pr_number":61767,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":52914,"pr_number":61767,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":53443,"pr_number":61767,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":54679,"pr_number":61767,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":52914,"title":"[Bug]: fix(qqbot): QQBot adapter.connect() missing is_reconnect parameter causes infinite retry loop","url":null,"state":"closed"},{"kind":"issue","number":53443,"title":"fix(qqbot): QQAdapter.connect() missing is_reconnect kwarg causes TypeError on every gateway startup","url":null,"state":"closed"},{"kind":"issue","number":54410,"title":"[Bug]: hermes agent have help me: QQAdapter.connect() got an unexpected keyword argument 'is_reconnect'","url":null,"state":"closed"},{"kind":"issue","number":54679,"title":"[Bug]: fix(gateway): QQAdapter.connect() does not accept is_reconnect parameter on reconnect","url":null,"state":"closed"},{"kind":"issue","number":57252,"title":"QQBot adapter: connect() receives unexpected 'is_reconnect' keyword from gateway","url":null,"state":"closed"},{"kind":"issue","number":57663,"title":"QQ Bot adapter connect() missing is_reconnect parameter","url":null,"state":"closed"},{"kind":"issue","number":57671,"title":"[Bug]: QQ Bot adapter connect() missing is_reconnect parameter","url":null,"state":"closed"},{"kind":"issue","number":58049,"title":"v0.18.0: QQBot adapter fails to connect - QQAdapter.connect() missing is_reconnect parameter","url":null,"state":"closed"},{"kind":"issue","number":58646,"title":"QQ bot adapter startup failure: QQAdapter.connect() got an unexpected keyword argument 'is_reconnect'","url":null,"state":"closed"},{"kind":"issue","number":59272,"title":"fix(qqbot): QQAdapter.connect() missing is_reconnect parameter — TypeError on reconnect","url":null,"state":"closed"},{"kind":"issue","number":60635,"title":"fix(qqbot): QQAdapter.connect() missing is_reconnect parameter, crashes on reconnect","url":null,"state":"closed"},{"kind":"pr","number":52911,"title":"fix(relay): accept is_reconnect kwarg in RelayAdapter.connect","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true},{"kind":"pr","number":52922,"title":"fix(qqbot): add missing is_reconnect parameter to QQAdapter.connect()","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":52966,"title":"fix(qqbot): add is_reconnect param to QQAdapter.connect for gateway reconnect compat","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61767","number":61767}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":53540,"title":"fix(qqbot): add missing is_reconnect kwarg to QQAdapter.connect()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"276542c729c10ff9d093760897f4c2d1256a79ce","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":54719,"title":"fix(qqbot): accept is_reconnect kwarg in QQAdapter.connect (closes #54679)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52922","number":52922},{"kind":"pr","value":"#52922","number":52922}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":55419,"title":"fix(qqbot): accept is_reconnect kwarg in QQAdapter.connect()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#52914","number":52914},{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#53540","number":53540},{"kind":"pr","value":"#53546","number":53546},{"kind":"pr","value":"#53948","number":53948},{"kind":"pr","value":"#54029","number":54029},{"kind":"pr","value":"#54037","number":54037},{"kind":"pr","value":"#54165","number":54165},{"kind":"pr","value":"#52922","number":52922},{"kind":"pr","value":"#52966","number":52966}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":55494,"title":"fix(qqbot): accept is_reconnect kwarg in connect() to fix reconnect","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#52966","number":52966}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":57266,"title":"fix(qqbot): accept is_reconnect kwarg in connect() to match base class contract","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52966","number":52966}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":57537,"title":"fix(qqbot): accept is_reconnect kwarg in connect() to match base adapter interface","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"276542c729","number":0},{"kind":"pr","value":"#61767","number":61767},{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#52914","number":52914}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":58341,"title":"fix(qqbot): add is_reconnect kwarg to QQAdapter.connect()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"276542c729c10ff9d093760897f4c2d1256a79ce","number":0},{"kind":"commit","value":"8416545c423a1f0f88f1236fd999c03922318547","number":0},{"kind":"pr","value":"#61767","number":61767}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":58726,"title":"fix(dashboard): confine fs preview and git diff reads to the managed root","url":null,"state":"closed","close_reason":{"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":59317,"title":"fix(qqbot): accept is_reconnect kwarg in connect() matching base-class contract (#59272)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"276542c729c10ff9d093760897f4c2d1256a79ce","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":59436,"title":"fix(qqbot): accept is_reconnect kwarg in connect() to match base class signature","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#52914","number":52914},{"kind":"pr","value":"#52922","number":52922},{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#53546","number":53546},{"kind":"pr","value":"#54029","number":54029},{"kind":"pr","value":"#54547","number":54547},{"kind":"pr","value":"#57105","number":57105}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":59462,"title":"fix(qqbot): accept base-class is_reconnect kwarg in connect() (#57671)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#52966","number":52966}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":60641,"title":"fix(qqbot): add is_reconnect parameter to QQAdapter.connect()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#60635","number":60635},{"kind":"pr","value":"#52914","number":52914}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":61298,"title":"fix(qqbot): add missing is_reconnect parameter to connect()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#52966","number":52966},{"kind":"pr","value":"#52922","number":52922},{"kind":"pr","value":"#54977","number":54977},{"kind":"pr","value":"#53948","number":53948},{"kind":"pr","value":"#54037","number":54037},{"kind":"pr","value":"#60161","number":60161}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":61767,"title":"fix(gateway): enforce reconnect contract across adapters","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","infographic/gateway-reconnect-contract/infographic.png","plugins/platforms/wecom/callback_adapter.py","scripts/release.py","tests/gateway/test_adapter_connect_is_reconnect_contract.py","tests/gateway/test_qqbot.py"],"fix_hunks":[{"file":"gateway/platforms/qqbot/adapter.py","hunks":["@@ -278,8 +278,16 @@ def enforces_own_access_policy(self) -> bool:\n     # Connection lifecycle\n     # ------------------------------------------------------------------\n \n-    async def connect(self) -> bool:\n-        \"\"\"Authenticate, obtain gateway URL, and open the WebSocket.\"\"\"\n+    async def connect(self, *, is_reconnect: bool = False) -> bool:\n+        \"\"\"\n+        Authenticate, obtain gateway URL, and open the WebSocket.\n+\n+        Args:\n+            is_reconnect: False on a cold first boot; True when the\n+                reconnect watcher is re-establishing this platform after\n+                an outage. QQBot has no server-side update queue so this\n+                flag is accepted for interface conformance only.\n+        \"\"\"\n         if not AIOHTTP_AVAILABLE:\n             message = \"QQ startup failed: aiohttp not installed\"\n             self._set_fatal_error(\"qq_missing_dependency\", message, retryable=True)"]},{"file":"plugins/platforms/wecom/callback_adapter.py","hunks":["@@ -115,7 +115,13 @@ def _normalize_apps(extra: Dict[str, Any]) -> List[Dict[str, Any]]:\n     # Lifecycle\n     # ------------------------------------------------------------------\n \n-    async def connect(self) -> bool:\n+    async def connect(self, *, is_reconnect: bool = False) -> bool:\n+        # ``is_reconnect`` is forwarded by GatewayRunner on every retry per\n+        # the BasePlatformAdapter.connect contract. Callback adapters have\n+        # no server-side queue to preserve, so the flag is accepted-and-\n+        # ignored — but the kwarg MUST be present or the reconnect watcher\n+        # dies with TypeError and the platform silently stays offline.\n+        del is_reconnect\n         if not self._apps:\n             logger.warning(\"[WecomCallback] No callback apps configured\")\n             return False"]},{"file":"scripts/release.py","hunks":["@@ -45,6 +45,8 @@\n \n # Auto-extracted from noreply emails + manual overrides\n AUTHOR_MAP = {\n+    \"[redacted:email]\": \"lemonwan\",  # PR #59430 sibling salvage (adapter reconnect contract guard)\n+    \"[redacted:email]\": \"luxuguang-leo\",  # PR #52966 salvage (QQBot reconnect contract)\n     \"[redacted:email]\": \"evelynburger\",  # PR #57544 salvage (gateway: webhook payload filters + route scripts; commit under unlinked identity)\n     \"[redacted:email]\": \"SiteupAgencia\",  # PR #57435 salvage (tui_gateway: back off notification poller when session is busy; #55578)\n     \"[redacted:email]\": \"rainbowgore\",  # PR #59405 salvage (mcp: bound stdio initialize handshake to stop subprocess/FD leak; #59349)"]},{"file":"tests/gateway/test_adapter_connect_is_reconnect_contract.py","hunks":["@@ -0,0 +1,157 @@\n+\"\"\"Regression: every platform adapter's ``connect()`` must accept the\n+``is_reconnect`` keyword-only argument.\n+\n+The gateway reconnect watcher forwards ``is_reconnect=True`` to every\n+adapter on every retry (see ``GatewayRunner._call_adapter_connect`` in\n+``gateway/run.py``). An adapter whose ``connect()`` signature omits\n+``is_reconnect`` blows up on the first reconnect attempt with::\n+\n+    TypeError: <Foo>Adapter.connect() got an unexpected\n+               keyword argument 'is_reconnect'\n+\n+…and never recovers, leaving that platform silently disconnected until\n+the operator manually restarts the gateway. This exact bug shipped for\n+``QQAdapter`` and was only discovered after messages stopped flowing on\n+the QQ channel for hours.\n+\n+To prevent this class of bug from regressing, we statically parse every\n+``adapter.py`` under ``gateway/platforms/`` and ``plugins/platforms/``\n+and assert that its ``connect()`` method accepts an ``is_reconnect``\n+keyword. Doing this via AST (rather than importing) avoids pulling every\n+platform's optional third-party SDK (aiohttp, slack_sdk, telegram,\n+matrix-nio, etc.) into the test environment.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import ast\n+from pathlib import Path\n+\n+import pytest\n+\n+\n+REPO_ROOT = Path(__file__).resolve().parents[2]\n+\n+# Directories that hold platform adapters. Each entry is a directory\n+# whose immediate children are either ``adapter.py`` files or\n+# sub-packages that expose one.\n+ADAPTER_ROOTS = [\n+    REPO_ROOT / \"gateway\" / \"platforms\",\n+    REPO_ROOT / \"plugins\" / \"platforms\",\n+]\n+\n+\n+def _iter_adapter_files() -> list[Path]:\n+    \"\"\"Every ``*adapter*.py`` under the two adapter roots.\n+\n+    We intentionally cast a wide net (any ``adapter.py`` / ``*_adapter.py``\n+    inside these trees) so a new platform can't sneak in without the\n+    contract check firing.\n+    \"\"\"\n+    files: list[Path] = []\n+    for root in ADAPTER_ROOTS:\n+        if not root.is_dir():\n+            continue\n+        for path in root.rglob(\"*.py\"):\n+            if path.name == \"adapter.py\" or path.stem.endswith(\"_adapter\"):\n+                files.append(path)\n+    return sorted(files)\n+\n+\n+def _find_adapter_classes(module: ast.Module) -> list[ast.ClassDef]:\n+    \"\"\"Classes that look like a platform adapter.\n+\n+    Heuristic: any class whose name ends in ``Adapter`` and that defines\n+    an ``async def connect`` method. This catches every subclass of\n+    ``BasePlatformAdapter`` in the tree today (QQAdapter, TelegramAdapter,\n+    SlackAdapter, …) without importing the base class.\n+    \"\"\"\n+    hits: list[ast.ClassDef] = []\n+    for node in ast.walk(module):\n+        if not isinstance(node, ast.ClassDef):\n+            continue\n+        if not node.name.endswith(\"Adapter\"):\n+            continue\n+        has_connect = any(\n+            isinstance(item, ast.AsyncFunctionDef) and item.name == \"connect\"\n+            for item in node.body\n+        )\n+        if has_connect:\n+            hits.append(node)\n+    return hits\n+\n+\n+def _connect_accepts_is_reconnect(cls: ast.ClassDef) -> bool:\n+    \"\"\"True iff the class's own ``connect()`` accepts ``is_reconnect``.\n+\n+    Accepts the kwarg via:\n+    - keyword-only argument named ``is_reconnect`` (the canonical form\n+      used by ``BasePlatformAdapter``), OR\n+    - ``**kwargs`` catch-all (also safe — the kwarg is absorbed).\n+    \"\"\"\n+    for item in cls.body:\n+        if not (isinstance(item, ast.AsyncFunctionDef) and item.name == \"connect\"):\n+            continue\n+        args = item.args\n+        if any(a.arg == \"is_reconnect\" for a in args.kwonlyargs):\n+            return True\n+        if any(a.arg == \"is_reconnect\" for a in args.args):\n+            return True\n+        if args.kwarg is not None:  # **kwargs\n+            return True\n+        return False\n+    return False\n+\n+\n+ADAPTER_FILES = _iter_adapter_files()\n+\n+\n+def test_adapter_discovery_finds_platforms():\n+    \"\"\"Sanity: the discovery walker actually found a meaningful set of\n+    adapters. If this drops to a trivial number, the glob broke and the\n+    contract test below is silently passing on nothing.\n+    \"\"\"\n+    assert len(ADAPTER_FILES) >= 20, (\n+        f\"Expected to discover >=20 platform adapter files under \"\n+        f\"{[str(p) for p in ADAPTER_ROOTS]}, found {len(ADAPTER_FILES)}. \"\n+        f\"The discovery glob is likely broken.\"\n+    )\n+\n+\n[redacted:email](\n+    \"adapter_file\",\n+    ADAPTER_FILES,\n+    ids=lambda p: str(p.relative_to(REPO_ROOT)),\n+)\n+def test_adapter_connect_accepts_is_reconnect(adapter_file: Path):\n+    \"\"\"Every ``*Adapter.connect()`` must accept ``is_reconnect``.\n+\n+    This is the contract enforced by ``BasePlatformAdapter.connect`` and\n+    relied on by ``GatewayRunner._call_adapter_connect``. Violating it\n+    silently disables the affected platform after its first reconnect.\n+    \"\"\"\n+    source = adapter_file.read_text(encoding=\"utf-8\")\n+    try:\n+        tree = ast.parse(source, filename=str(adapter_file))\n+    except SyntaxError as exc:\n+        pytest.fail(f\"Could not parse {adapter_file}: {exc}\")\n+\n+    classes = _find_adapter_classes(tree)\n+    if not classes:\n+        pytest.skip(\n+            f\"{adapter_file.relative_to(REPO_ROOT)} has no *Adapter class \"\n+            f\"with an async connect() — nothing to check.\"\n+        )\n+\n+    offenders = [cls.name for cls in classes if not _connect_accepts_is_reconnect(cls)]\n+\n+    assert not offenders, (\n+        f\"{adapter_file.relative_to(REPO_ROOT)}: the following adapter \"\n+        f\"class(es) define `async def connect()` WITHOUT accepting the \"\n+        f\"`is_reconnect` kwarg: {offenders}. \"\n+        f\"Add `*, is_reconnect: bool = False` to the signature \"\n+        f\"(matching BasePlatformAdapter.connect). \"\n+        f\"The gateway reconnect watcher forwards this kwarg on every \"\n+        f\"retry — an adapter that rejects it silently disconnects after \"\n+        f\"the first outage.\"\n+    )"]},{"file":"tests/gateway/test_qqbot.py","hunks":["@@ -190,6 +190,21 @@ def test_connect_uses_redirect_guard_hook(self):\n         assert kwargs.get(\"follow_redirects\") is True\n         assert kwargs.get(\"event_hooks\", {}).get(\"response\") == [_ssrf_redirect_guard]\n \n+    def test_connect_accepts_is_reconnect_param(self):\n+        \"\"\"connect() must accept is_reconnect for interface conformance with\n+        the base adapter, which the reconnect watcher calls with\n+        is_reconnect=True.\"\"\"\n+        from gateway.platforms.qqbot import QQAdapter\n+\n+        adapter = QQAdapter(_make_config(app_id=\"a\", client_secret=\"b\"))\n+        adapter._ensure_token = [redacted:secret-assignment]\"stop after client init\"))\n+\n+        # Both forms must not raise TypeError.\n+        connected_default = asyncio.run(adapter.connect())\n+        connected_explicit = asyncio.run(adapter.connect(is_reconnect=True))\n+        assert connected_default is False\n+        assert connected_explicit is False\n+\n \n # ---------------------------------------------------------------------------\n # WebSocket proxy handling\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}