{"repo":"NousResearch/hermes-agent","public_id":"349","display_label":"Gateway DM session isolation and scoping issues","summary":"These items concern session scoping, isolation, and key-matching problems in gateway DM contexts, including cross-platform context bleeding between Telegram and Discord, WhatsApp session key mismatches, and efforts to restrict session search and recall to the current chat.","subsystem":"agent","counts":{"members":7,"open_issues":2,"open_issues_distinct":2,"open_prs":2,"closed_issues":2,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":350},"resolution":{"state":"partially_resolved","by":"pr-350"},"freshness":{"state":"stale","last_verified_at":"2026-03-04T11:31:17+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.19428571428571428},"fix_edges":[{"issue_number":349,"pr_number":350,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":349,"title":"WhatsApp DM interrupt silently fails due to session key mismatch in _quick_key","url":null,"state":"closed"},{"kind":"issue","number":1056,"title":"[Bug]: Telegram and Discord DMs share a single global session (Context Bleeding)","url":null,"state":"closed"},{"kind":"issue","number":10554,"title":"session_search should be scoped to the current chat by default in gateway conversations","url":null,"state":"open"},{"kind":"issue","number":30498,"title":"Bug: re-anchor ambiguous gateway continuations before session_search","url":null,"state":"open"},{"kind":"pr","number":350,"title":"fix(gateway): match _quick_key to _generate_session_key for WhatsApp DMs","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","tests/gateway/test_session.py"],"fix_hunks":[{"file":"gateway/run.py","hunks":["@@ -637,11 +637,12 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:\n         # PRIORITY: If an agent is already running for this session, interrupt it\n         # immediately. This is before command parsing to minimize latency -- the\n         # user's \"stop\" message reaches the agent as fast as possible.\n-        _quick_key = (\n-            f\"agent:main:{source.platform.value}:{source.chat_type}:{source.chat_id}\"\n-            if source.chat_type != \"dm\"\n-            else f\"agent:main:{source.platform.value}:dm\"\n-        )\n+        if source.chat_type != \"dm\":\n+            _quick_key = f\"agent:main:{source.platform.value}:{source.chat_type}:{source.chat_id}\"\n+        elif source.platform.value == \"whatsapp\" and source.chat_id:\n+            _quick_key = f\"agent:main:{source.platform.value}:dm:{source.chat_id}\"\n+        else:\n+            _quick_key = f\"agent:main:{source.platform.value}:dm\"\n         if _quick_key in self._running_agents:\n             running_agent = self._running_agents[_quick_key]\n             logger.debug(\"PRIORITY interrupt for session %s\", _quick_key[:20])","@@ -1361,8 +1362,12 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:\n     async def _handle_usage_command(self, event: MessageEvent) -> str:\n         \"\"\"Handle /usage command -- show token usage for the session's last agent run.\"\"\"\n         source = event.source\n-        session_key = f\"agent:main:{source.platform.value}:\" + \\\n-                      (f\"dm\" if source.chat_type == \"dm\" else f\"{source.chat_type}:{source.chat_id}\")\n+        if source.chat_type != \"dm\":\n+            session_key = f\"agent:main:{source.platform.value}:{source.chat_type}:{source.chat_id}\"\n+        elif source.platform.value == \"whatsapp\" and source.chat_id:\n+            session_key = f\"agent:main:{source.platform.value}:dm:{source.chat_id}\"\n+        else:\n+            session_key = f\"agent:main:{source.platform.value}:dm\"\n \n         agent = self._running_agents.get(session_key)\n         if agent and hasattr(agent, \"session_total_tokens\") and agent.session_api_calls > 0:"]},{"file":"tests/gateway/test_session.py","hunks":["@@ -314,6 +314,88 @@ def test_rewrite_with_empty_list(self, store):\n         assert reloaded == []\n \n \n+class TestWhatsAppDMSessionKeyConsistency:\n+    \"\"\"Regression: inline session-key construction in handle_message must match\n+    _generate_session_key for WhatsApp DMs, which include chat_id.\"\"\"\n+\n+    @pytest.fixture()\n+    def store(self, tmp_path):\n+        config = GatewayConfig()\n+        with patch(\"gateway.session.SessionStore._ensure_loaded\"):\n+            s = SessionStore(sessions_dir=tmp_path, config=config)\n+        s._db = None\n+        s._loaded = True\n+        return s\n+\n+    def _build_quick_key(self, source: SessionSource) -> str:\n+        \"\"\"Reproduce the _quick_key logic from gateway/run.py handle_message.\"\"\"\n+        if source.chat_type != \"dm\":\n+            return f\"agent:main:{source.platform.value}:{source.chat_type}:{source.chat_id}\"\n+        elif source.platform.value == \"whatsapp\" and source.chat_id:\n+            return f\"agent:main:{source.platform.value}:dm:{source.chat_id}\"\n+        else:\n+            return f\"agent:main:{source.platform.value}:dm\"\n+\n+    def _build_usage_key(self, source: SessionSource) -> str:\n+        \"\"\"Reproduce the session_key logic from _handle_usage_command.\"\"\"\n+        if source.chat_type != \"dm\":\n+            return f\"agent:main:{source.platform.value}:{source.chat_type}:{source.chat_id}\"\n+        elif source.platform.value == \"whatsapp\" and source.chat_id:\n+            return f\"agent:main:{source.platform.value}:dm:{source.chat_id}\"\n+        else:\n+            return f\"agent:main:{source.platform.value}:dm\"\n+\n+    def test_whatsapp_dm_quick_key_includes_chat_id(self, store):\n+        source = SessionSource(\n+            platform=Platform.WHATSAPP,\n+            chat_id=\"[redacted:email]\",\n+            chat_type=\"dm\",\n+            user_name=\"Phone User\",\n+        )\n+        real_key = store._generate_session_key(source)\n+        quick_key = self._build_quick_key(source)\n+        assert quick_key == real_key\n+        assert \"[redacted:email]\" in quick_key\n+\n+    def test_whatsapp_dm_usage_key_includes_chat_id(self, store):\n+        source = SessionSource(\n+            platform=Platform.WHATSAPP,\n+            chat_id=\"[redacted:email]\",\n+            chat_type=\"dm\",\n+            user_name=\"Phone User\",\n+        )\n+        real_key = store._generate_session_key(source)\n+        usage_key = self._build_usage_key(source)\n+        assert usage_key == real_key\n+        assert \"[redacted:email]\" in usage_key\n+\n+    def test_telegram_dm_key_unchanged(self, store):\n+        \"\"\"Non-WhatsApp DMs should still omit chat_id (single owner DM).\"\"\"\n+        source = SessionSource(\n+            platform=Platform.TELEGRAM,\n+            chat_id=\"99\",\n+            chat_type=\"dm\",\n+        )\n+        real_key = store._generate_session_key(source)\n+        quick_key = self._build_quick_key(source)\n+        usage_key = self._build_usage_key(source)\n+        assert quick_key == real_key == \"agent:main:telegram:dm\"\n+        assert usage_key == real_key\n+\n+    def test_discord_group_key_unchanged(self, store):\n+        \"\"\"Group/channel keys should be unaffected by the fix.\"\"\"\n+        source = SessionSource(\n+            platform=Platform.DISCORD,\n+            chat_id=\"guild-123\",\n+            chat_type=\"group\",\n+        )\n+        real_key = store._generate_session_key(source)\n+        quick_key = self._build_quick_key(source)\n+        usage_key = self._build_usage_key(source)\n+        assert quick_key == real_key == \"agent:main:discord:group:guild-123\"\n+        assert usage_key == real_key\n+\n+\n class TestSessionStoreEntriesAttribute:\n     \"\"\"Regression: /reset must access _entries, not _sessions.\"\"\"\n \n"]}]},{"kind":"pr","number":30502,"title":"fix(agent): re-anchor before session_search for ambiguous continuations","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#70105","number":70105},{"kind":"pr","value":"#70105","number":70105},{"kind":"pr","value":"#70105","number":70105},{"kind":"pr","value":"#70105","number":70105}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":62138,"title":"fix(session-search): scope gateway recall to current chat","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"3ab4977ff","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}