{"repo":"NousResearch/hermes-agent","public_id":"7574","display_label":"Broken import cleanup after BuiltinMemoryProvider removal","summary":"A cluster of issues and PRs centered on a broken test import referencing a non-existent BuiltinMemoryProvider module following dead code cleanup. Members address the import error, stale docstring references, and related CI test failures.","subsystem":null,"counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":9483},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-04-14T08:43:45+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.29066666666666663},"fix_edges":[{"issue_number":7574,"pr_number":9483,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":7574,"title":"[Bug]: test_memory_user_id broken import after dead code cleanup","url":null,"state":"closed"},{"kind":"issue","number":14402,"title":"Broken memory user_id regression test imports nonexistent agent.builtin_memory_provider module","url":null,"state":"closed"},{"kind":"pr","number":7329,"title":"fix(tests): repair broken import in test_memory_user_id after dead code cleanup","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#9483","number":9483},{"kind":"pr","value":"#7574","number":7574}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":9483,"title":"fix: resolve CI test failures — add missing functions, fix stale tests","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/matrix.py","hermes_cli/runtime_provider.py","hermes_logging.py","tests/agent/test_auxiliary_client.py","tests/agent/test_compress_focus.py","tests/agent/test_memory_user_id.py","tests/cli/test_cli_interrupt_subagent.py","tests/gateway/test_display_config.py","tests/gateway/test_email.py","tests/gateway/test_feishu.py","tests/gateway/test_restart_drain.py","tests/gateway/test_session_env.py","tests/gateway/test_session_hygiene.py","tests/gateway/test_ws_auth_retry.py","tests/hermes_cli/test_auth_provider_gate.py","tests/hermes_cli/test_opencode_go_in_model_list.py","tools/approval.py"],"fix_hunks":[{"file":"gateway/platforms/matrix.py","hunks":["@@ -958,6 +958,16 @@ async def _sync_loop(self) -> None:\n                 sync_data = await client.sync(\n                     since=next_batch, timeout=30000,\n                 )\n+\n+                # nio returns SyncError objects (not exceptions) for auth\n+                # failures like M_UNKNOWN_TOKEN.  Detect and stop immediately.\n+                _sync_msg = getattr(sync_data, \"message\", None)\n+                if _sync_msg and isinstance(_sync_msg, str):\n+                    _lower = _sync_msg.lower()\n+                    if \"m_unknown_token\" in _lower or \"unknown_token\" in _lower:\n+                        logger.error(\"Matrix: permanent auth error from sync: %s — stopping\", _sync_msg)\n+                        return\n+\n                 if isinstance(sync_data, dict):\n                     # Update joined rooms from sync response.\n                     rooms_join = sync_data.get(\"rooms\", {}).get(\"join\", {})"]},{"file":"hermes_cli/runtime_provider.py","hunks":["@@ -287,6 +287,9 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An\n             # Resolve the API key from the env var name stored in key_env\n             key_env = str(entry.get(\"key_env\", \"\") or \"\").strip()\n             resolved_api_key = [redacted:secret-assignment] \"\").strip() if key_env else \"\"\n+            # Fall back to inline api_key when key_env is absent or unresolvable\n+            if not resolved_api_key:\n+                resolved_api_key = [redacted:secret-assignment]\"api_key\", \"\") or \"\").strip()\n \n             if requested_norm in {ep_name, name_norm, f\"custom:{name_norm}\"}:\n                 # Found match by provider key"]},{"file":"hermes_logging.py","hunks":["@@ -78,6 +78,10 @@ def set_session_context(session_id: str) -> None:\n     _session_context.session_id = session_id\n \n \n+def clear_session_context() -> None:\n+    \"\"\"Clear the session ID for the current thread.\"\"\"\n+    _session_context.session_id = None\n+\n \n # ---------------------------------------------------------------------------\n # Record factory — injects session_tag into every LogRecord at creation"]},{"file":"tests/agent/test_auxiliary_client.py","hunks":["@@ -365,7 +365,7 @@ def test_expired_codex_custom_endpoint_wins(self, tmp_path, monkeypatch):\n     def test_hermes_oauth_file_sets_oauth_flag(self, monkeypatch):\n         \"\"\"OAuth-style tokens should get is_oauth=*** (token is not sk-ant-api-*).\"\"\"\n         # Mock resolve_anthropic_token to return an OAuth-style token\n-        with patch(\"agent.anthropic_adapter.resolve_anthropic_token\", return_value=\"hermes-oauth-jwt-token\"), \\\n+        with patch(\"agent.anthropic_adapter.resolve_anthropic_token\", return_value=\"[redacted:openai-key]\"), \\\n              patch(\"agent.anthropic_adapter.build_anthropic_client\") as mock_build, \\\n              patch(\"agent.auxiliary_client._select_pool_entry\", return_value=(False, None)):\n             mock_build.return_value = MagicMock()","@@ -420,7 +420,7 @@ def test_jwt_invalid_json_payload_passes_through(self, tmp_path, monkeypatch):\n \n     def test_claude_code_oauth_env_sets_flag(self, monkeypatch):\n         \"\"\"CLAUDE_CODE_OAUTH_TOKEN env var should get is_oauth=True.\"\"\"\n-        monkeypatch.setenv(\"CLAUDE_CODE_OAUTH_TOKEN\", \"cc-oauth-token-test\")\n+        monkeypatch.setenv(\"CLAUDE_CODE_OAUTH_TOKEN\", \"[redacted:openai-key]\")\n         monkeypatch.delenv(\"ANTHROPIC_TOKEN\", raising=False)\n         with patch(\"agent.anthropic_adapter.build_anthropic_client\") as mock_build:\n             mock_build.return_value = MagicMock()","@@ -786,7 +786,7 @@ def test_vision_auto_uses_active_provider_as_fallback(self, monkeypatch):\n             patch(\"agent.anthropic_adapter.build_anthropic_client\", return_value=MagicMock()),\n             patch(\"agent.anthropic_adapter.resolve_anthropic_token\", return_value=\"***\"),\n         ):\n-            client, model = get_vision_auxiliary_client()\n+            provider, client, model = resolve_vision_provider_client()\n \n         assert client is not None\n         assert client.__class__.__name__ == \"AnthropicAuxiliaryClient\""]},{"file":"tests/agent/test_compress_focus.py","hunks":["@@ -25,6 +25,11 @@ def _make_compressor():\n     compressor._previous_summary = None\n     compressor._summary_failure_cooldown_until = 0.0\n     compressor.summary_model = None\n+    compressor.model = \"test-model\"\n+    compressor.provider = \"test\"\n+    compressor.base_url = \"http://localhost\"\n+    compressor.api_key = \"[redacted:secret-assignment]\"\n+    compressor.api_mode = \"chat_completions\"\n     return compressor\n \n "]},{"file":"tests/agent/test_memory_user_id.py","hunks":["@@ -109,14 +109,12 @@ def test_user_id_none_not_forwarded(self):\n         assert \"user_id\" not in p._init_kwargs\n \n     def test_multiple_providers_all_receive_user_id(self):\n-        from agent.builtin_memory_provider import BuiltinMemoryProvider\n-\n         mgr = MemoryManager()\n-        # Use builtin + one external (MemoryManager only allows one external)\n-        builtin = BuiltinMemoryProvider()\n-        ext = RecordingProvider(\"external\")\n-        mgr.add_provider(builtin)\n-        mgr.add_provider(ext)\n+        # Use one provider named \"builtin\" (always accepted) and one external\n+        p1 = RecordingProvider(\"builtin\")\n+        p2 = RecordingProvider(\"external\")\n+        mgr.add_provider(p1)\n+        mgr.add_provider(p2)\n \n         mgr.initialize_all(\n             session_id=\"sess-multi\",","@@ -124,8 +122,10 @@ def test_multiple_providers_all_receive_user_id(self):\n             user_id=\"slack_U12345\",\n         )\n \n-        assert ext._init_kwargs.get(\"user_id\") == \"slack_U12345\"\n-        assert ext._init_kwargs.get(\"platform\") == \"slack\"\n+        assert p1._init_kwargs.get(\"user_id\") == \"slack_U12345\"\n+        assert p1._init_kwargs.get(\"platform\") == \"slack\"\n+        assert p2._init_kwargs.get(\"user_id\") == \"slack_U12345\"\n+        assert p2._init_kwargs.get(\"platform\") == \"slack\"\n \n \n # ---------------------------------------------------------------------------","@@ -211,17 +211,17 @@ class TestHonchoUserIdScoping:\n     \"\"\"Verify Honcho plugin uses gateway user_id for peer_name when provided.\"\"\"\n \n     def test_gateway_user_id_overrides_peer_name(self):\n-        \"\"\"When user_id is in kwargs, cfg.peer_name should be overridden.\"\"\"\n+        \"\"\"When user_id is in kwargs and no explicit peer_name, user_id should be used.\"\"\"\n         from plugins.memory.honcho import HonchoMemoryProvider\n \n         provider = HonchoMemoryProvider()\n \n-        # Create a mock config with a static peer_name\n+        # Create a mock config with NO explicit peer_name\n         mock_cfg = MagicMock()\n         mock_cfg.enabled = True\n         mock_cfg.api_key = \"[redacted:secret-assignment]\"\n         mock_cfg.base_url = None\n-        mock_cfg.peer_name = \"static-user\"\n+        mock_cfg.peer_name = \"\"  # No explicit peer_name — user_id should fill it\n         mock_cfg.recall_mode = \"tools\"  # Use tools mode to defer session init\n \n         with patch("]},{"file":"tests/cli/test_cli_interrupt_subagent.py","hunks":["@@ -63,6 +63,7 @@ def test_full_delegate_interrupt_flow(self):\n         parent._delegate_depth = 0\n         parent._delegate_spinner = None\n         parent.tool_progress_callback = None\n+        parent._execution_thread_id = None\n \n         # We'll track what happens with _active_children\n         original_children = parent._active_children"]},{"file":"tests/gateway/test_display_config.py","hunks":["@@ -220,41 +220,6 @@ def test_high_tier_streaming_defaults_to_none(self):\n         assert resolve_display_setting({}, \"telegram\", \"streaming\") is None\n \n \n-# ---------------------------------------------------------------------------\n-# get_effective_display / get_platform_defaults\n-# ---------------------------------------------------------------------------\n-\n-class TestHelpers:\n-    \"\"\"Helper functions return correct composite results.\"\"\"\n-\n-    def test_get_effective_display_merges_correctly(self):\n-        from gateway.display_config import get_effective_display\n-\n-        config = {\n-            \"display\": {\n-                \"tool_progress\": \"new\",\n-                \"show_reasoning\": True,\n-                \"platforms\": {\n-                    \"telegram\": {\"tool_progress\": \"verbose\"},\n-                },\n-            }\n-        }\n-        eff = get_effective_display(config, \"telegram\")\n-        assert eff[\"tool_progress\"] == \"verbose\"  # platform override\n-        assert eff[\"show_reasoning\"] is True       # global\n-        assert \"tool_preview_length\" in eff        # default filled in\n-\n-    def test_get_platform_defaults_returns_dict(self):\n-        from gateway.display_config import get_platform_defaults\n-\n-        defaults = get_platform_defaults(\"telegram\")\n-        assert \"tool_progress\" in defaults\n-        assert \"show_reasoning\" in defaults\n-        # Returns a new dict (not the shared tier dict)\n-        defaults[\"tool_progress\"] = \"changed\"\n-        assert get_platform_defaults(\"telegram\")[\"tool_progress\"] != \"changed\"\n-\n-\n # ---------------------------------------------------------------------------\n # Config migration: tool_progress_overrides → display.platforms\n # ---------------------------------------------------------------------------"]},{"file":"tests/gateway/test_email.py","hunks":["@@ -334,10 +334,12 @@ class TestChannelDirectory(unittest.TestCase):\n     \"\"\"Verify email in channel directory session-based discovery.\"\"\"\n \n     def test_email_in_session_discovery(self):\n-        import gateway.channel_directory\n-        import inspect\n-        source = inspect.getsource(gateway.channel_directory.build_channel_directory)\n-        self.assertIn('\"email\"', source)\n+        from gateway.config import Platform\n+        # Verify email is a Platform enum member — the dynamic loop in\n+        # build_channel_directory iterates all Platform members, so email\n+        # is included automatically as long as it's in the enum.\n+        email_values = [p.value for p in Platform]\n+        self.assertIn(\"email\", email_values)\n \n \n class TestGatewaySetup(unittest.TestCase):"]},{"file":"tests/gateway/test_feishu.py","hunks":["@@ -631,6 +631,14 @@ def register_p2_card_action_trigger(self, _handler):\n                 calls.append(\"card_action\")\n                 return self\n \n+            def register_p2_im_chat_member_bot_added_v1(self, _handler):\n+                calls.append(\"bot_added\")\n+                return self\n+\n+            def register_p2_im_chat_member_bot_deleted_v1(self, _handler):\n+                calls.append(\"bot_deleted\")\n+                return self\n+\n             def build(self):\n                 calls.append(\"build\")\n                 return \"handler\"","@@ -654,6 +662,8 @@ def builder(_encrypt_key, _verification_token):\n                 \"reaction_created\",\n                 \"reaction_deleted\",\n                 \"card_action\",\n+                \"bot_added\",\n+                \"bot_deleted\",\n                 \"build\",\n             ],\n         )"]},{"file":"tests/gateway/test_restart_drain.py","hunks":["@@ -13,7 +13,10 @@\n \n \n @pytest.mark.asyncio\n-async def test_restart_command_while_busy_requests_drain_without_interrupt():\n+async def test_restart_command_while_busy_requests_drain_without_interrupt(monkeypatch):\n+    # Ensure INVOCATION_ID is NOT set — systemd sets this in service mode,\n+    # which changes the restart call signature.\n+    monkeypatch.delenv(\"INVOCATION_ID\", raising=False)\n     runner, _adapter = make_restart_runner()\n     runner.request_restart = MagicMock(return_value=True)\n     event = MessageEvent("]},{"file":"tests/gateway/test_session_env.py","hunks":["@@ -186,10 +186,13 @@ def test_set_session_env_includes_session_key():\n         session_key=\"tg:-1001:17585\",\n     )\n \n+    # Capture baseline value before setting (may be non-empty from another\n+    # test in the same pytest-xdist worker sharing the context).\n+    baseline = get_session_env(\"HERMES_SESSION_KEY\")\n     tokens = [redacted:secret-assignment]\n     assert get_session_env(\"HERMES_SESSION_KEY\") == \"tg:-1001:17585\"\n     runner._clear_session_env(tokens)\n-    assert get_session_env(\"HERMES_SESSION_KEY\") == \"\"\n+    assert get_session_env(\"HERMES_SESSION_KEY\") == baseline\n \n \n def test_session_key_no_race_condition_with_contextvars(monkeypatch):"]},{"file":"tests/gateway/test_session_hygiene.py","hunks":["@@ -374,6 +374,7 @@ def _compress_context(self, messages, *_args, **_kwargs):\n             chat_id=\"-1001\",\n             chat_type=\"group\",\n             thread_id=\"17585\",\n+            user_id=\"12345\",\n         ),\n         message_id=\"1\",\n     )"]},{"file":"tests/gateway/test_ws_auth_retry.py","hunks":["@@ -130,13 +130,17 @@ def __init__(self, message):\n \n         sync_count = 0\n \n-        async def fake_sync(timeout=30000):\n+        async def fake_sync(timeout=30000, since=None):\n             nonlocal sync_count\n             sync_count += 1\n             return SyncError(\"M_UNKNOWN_TOKEN: Invalid access token\")\n \n         adapter._client = MagicMock()\n         adapter._client.sync = fake_sync\n+        adapter._client.sync_store = MagicMock()\n+        adapter._client.sync_store.get_next_batch = AsyncMock(return_value=None)\n+        adapter._pending_megolm = []\n+        adapter._joined_rooms = set()\n \n         async def run():\n             import sys","@@ -157,13 +161,17 @@ def test_exception_with_401_stops_loop(self):\n \n         call_count = 0\n \n-        async def fake_sync(timeout=30000):\n+        async def fake_sync(timeout=30000, since=None):\n             nonlocal call_count\n             call_count += 1\n             raise RuntimeError(\"HTTP 401 Unauthorized\")\n \n         adapter._client = MagicMock()\n         adapter._client.sync = fake_sync\n+        adapter._client.sync_store = MagicMock()\n+        adapter._client.sync_store.get_next_batch = AsyncMock(return_value=None)\n+        adapter._pending_megolm = []\n+        adapter._joined_rooms = set()\n \n         async def run():\n             import types","@@ -188,7 +196,7 @@ def test_transient_error_retries(self):\n \n         call_count = 0\n \n-        async def fake_sync(timeout=30000):\n+        async def fake_sync(timeout=30000, since=None):\n             nonlocal call_count\n             call_count += 1\n             if call_count >= 2:","@@ -198,6 +206,10 @@ async def fake_sync(timeout=30000):\n \n         adapter._client = MagicMock()\n         adapter._client.sync = fake_sync\n+        adapter._client.sync_store = MagicMock()\n+        adapter._client.sync_store.get_next_batch = AsyncMock(return_value=None)\n+        adapter._pending_megolm = []\n+        adapter._joined_rooms = set()\n \n         async def run():\n             import types"]},{"file":"tests/hermes_cli/test_auth_provider_gate.py","hunks":["@@ -18,6 +18,13 @@ def _write_auth_store(tmp_path, payload: dict) -> None:\n     (hermes_home / \"auth.json\").write_text(json.dumps(payload, indent=2))\n \n \n[redacted:email](autouse=True)\n+def _clean_anthropic_env(monkeypatch):\n+    \"\"\"Strip Anthropic env vars so CI secrets don't leak into tests.\"\"\"\n+    for key in (\"ANTHROPIC_API_KEY\", \"ANTHROPIC_TOKEN\", \"CLAUDE_CODE_OAUTH_TOKEN\"):\n+        monkeypatch.delenv(key, raising=False)\n+\n+\n def test_returns_false_when_no_config(tmp_path, monkeypatch):\n     monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \"hermes\"))\n     (tmp_path / \"hermes\").mkdir(parents=True, exist_ok=True)"]},{"file":"tests/hermes_cli/test_opencode_go_in_model_list.py","hunks":["@@ -16,8 +16,10 @@ def test_opencode_go_appears_when_api_key_set():\n     \n     assert opencode_go is not None, \"opencode-go should appear when OPENCODE_GO_API_KEY is set\"\n     assert opencode_go[\"models\"] == [\"glm-5\", \"kimi-k2.5\", \"mimo-v2-pro\", \"mimo-v2-omni\", \"minimax-m2.7\", \"minimax-m2.5\"]\n-    # opencode-go is in PROVIDER_TO_MODELS_DEV, so it appears as \"built-in\" (Part 1)\n-    assert opencode_go[\"source\"] == \"built-in\"\n+    # opencode-go can appear as \"built-in\" (from PROVIDER_TO_MODELS_DEV when\n+    # models.dev is reachable) or \"hermes\" (from HERMES_OVERLAYS fallback when\n+    # the API is unavailable, e.g. in CI).\n+    assert opencode_go[\"source\"] in (\"built-in\", \"hermes\")\n \n \n def test_opencode_go_not_appears_when_no_creds():"]},{"file":"tools/approval.py","hunks":["@@ -313,6 +313,17 @@ def disable_session_yolo(session_key: str) -> None:\n         _session_yolo.discard(session_key)\n \n \n+def clear_session(session_key: str) -> None:\n+    \"\"\"Remove all approval and yolo state for a given session.\"\"\"\n+    if not session_key:\n+        return\n+    with _lock:\n+        _session_approved.pop(session_key, None)\n+        _session_yolo.discard(session_key)\n+        _pending.pop(session_key, None)\n+        _gateway_queues.pop(session_key, None)\n+\n+\n def is_session_yolo_enabled(session_key: str) -> bool:\n     \"\"\"Return True when YOLO bypass is enabled for a specific session.\"\"\"\n     if not session_key:\n"]}]},{"kind":"pr","number":10869,"title":"test(memory): remove dead import of non-existent BuiltinMemoryProvider","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"2558d28a9bd90b95019a7c289db89bd0dd9f2d8e","number":0},{"kind":"pr","value":"#14402","number":14402}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":14446,"title":"docs(agent): remove stale BuiltinMemoryProvider references from memory module docstrings","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}