{"repo":"NousResearch/hermes-agent","public_id":"50656","display_label":"Feishu group chat WebSocket event delivery fixes","summary":"Multiple bug reports and fix attempts addressing Feishu/Lark group chat messages and @mentions not being received or routed through the WebSocket gateway. A minor unrelated gateway fix for file path rejection is also present.","subsystem":"gateway","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":61770,"merged_at":"2026-07-10T03:31:50+00:00"},"resolution":{"state":"resolved","by":"pr-61770"},"freshness":{"state":"verified","last_verified_at":"2026-07-10T03:31:50+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":50656,"pr_number":61770,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-10T03:31:50+00:00"}],"members":[{"kind":"issue","number":50656,"title":"[Bug]: Feishu/Lark group @mention messages not delivered — WebSocket never receives group events","url":null,"state":"closed"},{"kind":"pr","number":52908,"title":"fix(feishu): add extra_ua_tags=[\"channel\"] to FeishuWSClient for group @mention delivery","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61770","number":61770}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":61770,"title":"fix(feishu): restore group events over WebSocket","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["infographic/feishu-group-events/infographic.png","plugins/platforms/feishu/adapter.py","pyproject.toml","scripts/release.py","tests/gateway/test_feishu.py","tools/lazy_deps.py"],"fix_hunks":[{"file":"plugins/platforms/feishu/adapter.py","hunks":["@@ -4730,6 +4730,12 @@ async def _connect_websocket(self) -> None:\n             log_level=lark.LogLevel.INFO,\n             event_handler=self._event_handler,\n             domain=domain,\n+            # Channel SDK signaling tag: without this UA tag the Feishu\n+            # server does not push group @mention events over the WebSocket\n+            # transport.  The tag tells the server to use the Channel protocol\n+            # which enables group-message routing in addition to P2P DM.\n+            # See https://github.com/NousResearch/hermes-agent/issues/50656\n+            extra_ua_tags=[\"channel\"],\n         )\n         self._ws_future = loop.run_in_executor(\n             None,"]},{"file":"pyproject.toml","hunks":["@@ -249,7 +249,7 @@ termux-all = [\n   \"hermes-agent[web]\",\n ]\n dingtalk = [\"dingtalk-stream==0.24.3\", \"alibabacloud-dingtalk==2.2.42\", \"qrcode==7.4.2\"]\n-feishu = [\"lark-oapi==1.5.3\", \"qrcode==7.4.2\"]\n+feishu = [\"lark-oapi==1.6.8\", \"qrcode==7.4.2\"]\n google = [\n   # Required by the google-workspace skill (Gmail, Calendar, Drive, Contacts,\n   # Sheets, Docs).  Declared here so packagers (Nix, Homebrew) ship them with"]},{"file":"scripts/release.py","hunks":["@@ -46,7 +46,7 @@\n # Auto-extracted from noreply emails + manual overrides\n AUTHOR_MAP = {\n     \"lemonwan@users.noreply.github.com\": \"lemonwan\",  # PR #59430 sibling salvage (adapter reconnect contract guard)\n-    \"[redacted:email]\": \"luxuguang-leo\",  # PR #52966 salvage (QQBot reconnect contract)\n+    \"[redacted:email]\": \"luxuguang-leo\",  # PR #52966 + #52908 salvage (QQBot reconnect + Feishu Channel signaling)\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     \"164521089+rainbowgits@users.noreply.github.com\": \"rainbowgore\",  # PR #59405 salvage (mcp: bound stdio initialize handshake to stop subprocess/FD leak; #59349)"]},{"file":"tests/gateway/test_feishu.py","hunks":["@@ -173,6 +173,16 @@ def test_normalize_interactive_card_preserves_title_body_and_actions(self):\n \n \n class TestFeishuAdapterMessaging(unittest.TestCase):\n+    @unittest.skipUnless(_HAS_LARK_OAPI, \"lark-oapi not installed\")\n+    def test_websocket_sdk_accepts_channel_ua_tag(self):\n+        \"\"\"The shipped SDK must support the Channel signaling argument.\"\"\"\n+        import inspect\n+\n+        from lark_oapi.ws import Client as FeishuWSClient\n+\n+        signature = inspect.signature(FeishuWSClient)\n+        self.assertIn(\"extra_ua_tags\", signature.parameters)\n+\n     @patch.dict(os.environ, {\n         \"FEISHU_APP_ID\": \"cli_app\",\n         \"FEISHU_APP_SECRET\": \"[redacted:secret-assignment]\",","@@ -414,6 +424,65 @@ def is_closed(self):\n         self.assertEqual(sleeps, [1])\n         self.assertEqual(fake_loop.calls, 2)\n \n+    @patch.dict(os.environ, {\n+        \"FEISHU_APP_ID\": \"cli_app\",\n+        \"FEISHU_APP_SECRET\": \"[redacted:secret-assignment]\",\n+    }, clear=True)\n+    def test_connect_websocket_sets_channel_ua_tag(self):\n+        \"\"\"Verify that FeishuWSClient receives extra_ua_tags=[\"channel\"].\n+\n+        Without this UA tag the Feishu server does not push group @mention\n+        events over the WebSocket transport.  See\n+        https://github.com/NousResearch/hermes-agent/issues/50656\n+        \"\"\"\n+        from gateway.config import PlatformConfig\n+        from plugins.platforms.feishu.adapter import FeishuAdapter\n+\n+        adapter = FeishuAdapter(PlatformConfig())\n+        ws_client = SimpleNamespace()\n+\n+        with (\n+            patch(\"plugins.platforms.feishu.adapter.FEISHU_AVAILABLE\", True),\n+            patch(\"plugins.platforms.feishu.adapter.FEISHU_WEBSOCKET_AVAILABLE\", True),\n+            patch(\"plugins.platforms.feishu.adapter.lark\",\n+                  SimpleNamespace(LogLevel=SimpleNamespace(INFO=\"INFO\", WARNING=\"WARNING\"))),\n+            patch(\"plugins.platforms.feishu.adapter.EventDispatcherHandler\") as mock_handler_class,\n+            patch(\"plugins.platforms.feishu.adapter.FeishuWSClient\") as mock_ws_client,\n+            patch(\"plugins.platforms.feishu.adapter._run_official_feishu_ws_client\"),\n+            patch(\"plugins.platforms.feishu.adapter.acquire_scoped_lock\", return_value=(True, None)),\n+            patch(\"plugins.platforms.feishu.adapter.release_scoped_lock\"),\n+            patch.object(adapter, \"_hydrate_bot_identity\", new=AsyncMock()),\n+            patch.object(adapter, \"_build_lark_client\", return_value=SimpleNamespace()),\n+        ):\n+            _mock_event_dispatcher_builder(mock_handler_class)\n+\n+            loop = asyncio.new_event_loop()\n+            future = loop.create_future()\n+            future.set_result(None)\n+\n+            class _Loop:\n+                def run_in_executor(self, *_args, **_kwargs):\n+                    return future\n+                def is_closed(self):\n+                    return False\n+\n+            try:\n+                with patch(\"plugins.platforms.feishu.adapter.asyncio.get_running_loop\",\n+                           return_value=_Loop()):\n+                    connected = asyncio.run(adapter.connect())\n+            finally:\n+                loop.close()\n+\n+        self.assertTrue(connected)\n+        # Verify the Channel SDK UA tag is present — this is the fix for\n+        # group @mention message delivery over WebSocket.\n+        mock_ws_client.assert_called_once()\n+        call_kwargs = mock_ws_client.call_args.kwargs\n+        self.assertIn(\"extra_ua_tags\", call_kwargs,\n+                      \"FeishuWSClient must receive extra_ua_tags for group @mention delivery\")\n+        self.assertEqual(call_kwargs[\"extra_ua_tags\"], [\"channel\"],\n+                         \"extra_ua_tags must be ['channel'] to enable group event routing\")\n+\n     @patch.dict(os.environ, {}, clear=True)\n     def test_edit_message_updates_existing_feishu_message(self):\n         from gateway.config import PlatformConfig"]},{"file":"tools/lazy_deps.py","hunks":["@@ -188,7 +188,7 @@\n         \"qrcode==7.4.2\",\n     ),\n     \"platform.feishu\": (\n-        \"lark-oapi==1.5.3\",\n+        \"lark-oapi==1.6.8\",\n         \"qrcode==7.4.2\",\n     ),\n     # WeCom callback-mode adapter — parses untrusted XML POST bodies. Pulls"]},{"file":"uv.lock","hunks":["@@ -1794,7 +1794,7 @@ requires-dist = [\n     { name = \"honcho-ai\", marker = \"extra == 'honcho'\", specifier = \"==2.0.1\" },\n     { name = \"httpx\", extras = [\"socks\"], specifier = \"==0.28.1\" },\n     { name = \"jinja2\", specifier = \"==3.1.6\" },\n-    { name = \"lark-oapi\", marker = \"extra == 'feishu'\", specifier = \"==1.5.3\" },\n+    { name = \"lark-oapi\", marker = \"extra == 'feishu'\", specifier = \"==1.6.8\" },\n     { name = \"markdown\", specifier = \"==3.10.2\" },\n     { name = \"mautrix\", extras = [\"encryption\"], marker = \"extra == 'matrix'\", specifier = \"==0.21.0\" },\n     { name = \"mcp\", marker = \"extra == 'computer-use'\", specifier = \"==1.26.0\" },","@@ -2184,7 +2184,7 @@ wheels = [\n \n [[package]]\n name = \"lark-oapi\"\n-version = \"1.5.3\"\n+version = \"1.6.8\"\n source = { registry = \"https://pypi.org/simple\" }\n dependencies = [\n     { name = \"httpx\" },","@@ -2194,7 +2194,7 @@ dependencies = [\n     { name = \"websockets\" },\n ]\n wheels = [\n-    { url = \"https://files.pythonhosted.org/packages/bf/ff/2ece5d735ebfa2af600a53176f2636ae47af2bf934e08effab64f0d1e047/lark_oapi-1.5.3-py3-none-any.whl\", hash = \"sha256:fda6b32bb38d21b6bdaae94979c600b94c7c521e985adade63a54e4b3e20cc36\", size = 6993016, upload-time = \"2026-01-27T08:21:49.307Z\" },\n+    { url = \"https://files.pythonhosted.org/packages/4a/ad/1ab04db5d549ad1a7a2cd33682b1c38dee1d65019cb24fd7a23270e6337d/lark_oapi-1.6.8-py3-none-any.whl\", hash = \"sha256:9b443a5d47a7d204dd42dc40896c8b75087cc35788e45c48c140806d7df7e5e8\", size = 7798300, upload-time = \"2026-06-02T07:40:05.492Z\" },\n ]\n \n [[package]]\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}