From 3ea0bd6f456a2e952bef9ed31b728e8a2e8289eb Mon Sep 17 00:00:00 2001 From: Ziyang Guo <121015044+RunMarshal@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:15:51 +0800 Subject: [PATCH] fix(hermes): log memory provider startup state Signed-off-by: Ziyang Guo <121015044+RunMarshal@users.noreply.github.com> --- .../memory/memory_tencentdb/__init__.py | 77 ++++++++++++++++++ .../tests/test_startup_observability.py | 81 +++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 hermes-plugin/memory/memory_tencentdb/tests/test_startup_observability.py diff --git a/hermes-plugin/memory/memory_tencentdb/__init__.py b/hermes-plugin/memory/memory_tencentdb/__init__.py index 86350fa..7f4ea05 100644 --- a/hermes-plugin/memory/memory_tencentdb/__init__.py +++ b/hermes-plugin/memory/memory_tencentdb/__init__.py @@ -154,6 +154,73 @@ def _resolve_gateway_api_key() -> Optional[str]: return None +def _flag_is_false(value: Any) -> bool: + if value is False: + return True + if isinstance(value, str): + return value.strip().lower() in {"false", "0", "no", "off"} + return False + + +def _get_host_memory_flag(kwargs: Dict[str, Any], name: str) -> Any: + """Best-effort lookup for Hermes memory flags passed through initialize().""" + if name in kwargs: + return kwargs[name] + + memory = kwargs.get("memory") + if isinstance(memory, dict) and name in memory: + return memory[name] + + config = kwargs.get("config") + if isinstance(config, dict): + config_memory = config.get("memory") + if isinstance(config_memory, dict) and name in config_memory: + return config_memory[name] + + return None + + +def _log_startup_observability( + *, + session_id: str, + user_id: str, + host: str, + port: int, + gateway_cmd: Optional[str], + api_key: Optional[str], + kwargs: Dict[str, Any], +) -> None: + gateway_mode = "auto-start" if gateway_cmd else "connect-only" + auth_mode = "bearer" if api_key else "none" + logger.info( + "memory-tencentdb provider loaded: reason=Hermes memory.provider selected " + "memory_tencentdb; session=%s user=%s gateway=http://%s:%d " + "gatewayMode=%s auth=%s. The provider will attach/start the Gateway, " + "serve recall/search tools, and capture turns for L0-L3 memory.", + session_id, + user_id, + host, + port, + gateway_mode, + auth_mode, + ) + + suspicious_flags = [ + name + for name in ("memory_enabled", "user_profile_enabled") + if _flag_is_false(_get_host_memory_flag(kwargs, name)) + ] + if suspicious_flags: + logger.warning( + "memory-tencentdb loaded even though Hermes %s false. In Hermes " + "these fields only control system-prompt/tool advertisement; they " + "do not disable this provider once memory.provider=memory_tencentdb. " + "To disable memory-tencentdb, unset memory.provider or set it to a " + "different provider.", + " and ".join(suspicious_flags), + ) + + # Candidate locations searched by _discover_gateway_cmd() when the user has not # set MEMORY_TENCENTDB_GATEWAY_CMD. Order matters: in-tree checkout (next to # this file) wins over ad-hoc clones in ``$HOME``. @@ -751,6 +818,16 @@ def initialize(self, session_id: str, **kwargs) -> None: # Gateway side directly so both ends agree on the secret. api_key = _resolve_gateway_api_key() + _log_startup_observability( + session_id=session_id, + user_id=self._user_id, + host=host, + port=port, + gateway_cmd=gateway_cmd, + api_key=api_key, + kwargs=kwargs, + ) + self._supervisor = GatewaySupervisor( host=host, port=port, diff --git a/hermes-plugin/memory/memory_tencentdb/tests/test_startup_observability.py b/hermes-plugin/memory/memory_tencentdb/tests/test_startup_observability.py new file mode 100644 index 0000000..94636b4 --- /dev/null +++ b/hermes-plugin/memory/memory_tencentdb/tests/test_startup_observability.py @@ -0,0 +1,81 @@ +"""Tests for Hermes startup observability diagnostics.""" + +from __future__ import annotations + +import logging +import pathlib +import sys +import types +import unittest + +_THIS_FILE = pathlib.Path(__file__).resolve() +_HERE = _THIS_FILE.parent +for candidate in ( + _HERE.parents[2] if len(_HERE.parents) >= 3 else None, + _HERE.parents[3] if len(_HERE.parents) >= 4 else None, + _HERE.parents[4] if len(_HERE.parents) >= 5 else None, +): + if candidate is not None and ((candidate / "plugins").is_dir() or (candidate / "memory").is_dir()): + if str(candidate) not in sys.path: + sys.path.insert(0, str(candidate)) + +if "agent.memory_provider" not in sys.modules: + agent_module = types.ModuleType("agent") + memory_provider_module = types.ModuleType("agent.memory_provider") + + class MemoryProvider: # minimal Hermes test stub + pass + + memory_provider_module.MemoryProvider = MemoryProvider + sys.modules.setdefault("agent", agent_module) + sys.modules["agent.memory_provider"] = memory_provider_module + +try: + try: + import plugins.memory.memory_tencentdb as mod + except ImportError: + import memory.memory_tencentdb as mod +except ImportError as e: # pragma: no cover - env-dependent + raise unittest.SkipTest( + f"memory_tencentdb provider not importable ({e}); set HERMES_AGENT_ROOT " + "to a hermes-agent checkout if running from the plugin repo." + ) + + +class StartupObservabilityTest(unittest.TestCase): + def test_startup_banner_explains_provider_reason(self): + with self.assertLogs(mod.logger.name, level="INFO") as logs: + mod._log_startup_observability( + session_id="s1", + user_id="u1", + host="127.0.0.1", + port=8420, + gateway_cmd="node gateway", + api_key=None, + kwargs={}, + ) + + text = "\n".join(logs.output) + self.assertIn("memory.provider selected memory_tencentdb", text) + self.assertIn("gateway=http://127.0.0.1:8420", text) + self.assertIn("capture turns for L0-L3 memory", text) + + def test_startup_warns_when_hermes_prompt_flags_are_false(self): + with self.assertLogs(mod.logger.name, level="WARNING") as logs: + mod._log_startup_observability( + session_id="s1", + user_id="u1", + host="127.0.0.1", + port=8420, + gateway_cmd=None, + api_key="secret", + kwargs={"memory": {"memory_enabled": False, "user_profile_enabled": "false"}}, + ) + + text = "\n".join(logs.output) + self.assertIn("memory_enabled and user_profile_enabled false", text) + self.assertIn("do not disable this provider", text) + + +if __name__ == "__main__": + unittest.main()