diff --git a/plugins/memory/honcho/__init__.py b/plugins/memory/honcho/__init__.py index 68fa86885..d104deb5d 100644 --- a/plugins/memory/honcho/__init__.py +++ b/plugins/memory/honcho/__init__.py @@ -293,14 +293,6 @@ class HonchoMemoryProvider(MemoryProvider): logger.debug("Honcho not configured — plugin inactive") return - # Override peer_name with gateway user_id for per-user memory scoping. - # Only when no explicit peerName was configured — an explicit peerName - # means the user chose their identity; a raw user_id (e.g. Telegram - # chat ID) should not silently replace it. - _gw_user_id = kwargs.get("user_id") - if _gw_user_id and not cfg.peer_name: - cfg.peer_name = _gw_user_id - self._config = cfg # ----- B1: recall_mode from config ----- @@ -359,6 +351,7 @@ class HonchoMemoryProvider(MemoryProvider): honcho=client, config=cfg, context_tokens=cfg.context_tokens, + runtime_user_peer_name=kwargs.get("user_id") or None, ) # ----- B3: resolve_session_name ----- diff --git a/plugins/memory/honcho/session.py b/plugins/memory/honcho/session.py index 7344b517e..79625b5cd 100644 --- a/plugins/memory/honcho/session.py +++ b/plugins/memory/honcho/session.py @@ -78,6 +78,7 @@ class HonchoSessionManager: honcho: Honcho | None = None, context_tokens: int | None = None, config: Any | None = None, + runtime_user_peer_name: str | None = None, ): """ Initialize the session manager. @@ -87,10 +88,12 @@ class HonchoSessionManager: context_tokens: Max tokens for context() calls (None = Honcho default). config: HonchoClientConfig from global config (provides peer_name, ai_peer, write_frequency, observation, etc.). + runtime_user_peer_name: Gateway user identity for per-user memory scoping. """ self._honcho = honcho self._context_tokens = context_tokens self._config = config + self._runtime_user_peer_name = runtime_user_peer_name self._cache: dict[str, HonchoSession] = {} self._peers_cache: dict[str, Any] = {} self._sessions_cache: dict[str, Any] = {} @@ -274,8 +277,10 @@ class HonchoSessionManager: logger.debug("Local session cache hit: %s", key) return self._cache[key] - # Use peer names from global config when available - if self._config and self._config.peer_name: + # Gateway sessions should use the runtime user identity when available. + if self._runtime_user_peer_name: + user_peer_id = self._sanitize_id(self._runtime_user_peer_name) + elif self._config and self._config.peer_name: user_peer_id = self._sanitize_id(self._config.peer_name) else: # Fallback: derive from session key diff --git a/tests/agent/test_memory_user_id.py b/tests/agent/test_memory_user_id.py index c1b82208d..d33753bd2 100644 --- a/tests/agent/test_memory_user_id.py +++ b/tests/agent/test_memory_user_id.py @@ -208,34 +208,81 @@ class TestMem0UserIdScoping: class TestHonchoUserIdScoping: - """Verify Honcho plugin uses gateway user_id for peer_name when provided.""" + """Verify Honcho plugin keeps runtime user scoping separate from config peer_name.""" - def test_gateway_user_id_overrides_peer_name(self): - """When user_id is in kwargs and no explicit peer_name, user_id should be used.""" + def test_gateway_user_id_is_passed_as_runtime_peer(self): + """Gateway user_id should scope Honcho sessions without mutating config peer_name.""" from plugins.memory.honcho import HonchoMemoryProvider provider = HonchoMemoryProvider() - # Create a mock config with NO explicit peer_name mock_cfg = MagicMock() mock_cfg.enabled = True mock_cfg.api_key = "test-key" mock_cfg.base_url = None - mock_cfg.peer_name = "" # No explicit peer_name — user_id should fill it - mock_cfg.recall_mode = "tools" # Use tools mode to defer session init + mock_cfg.peer_name = "static-user" + mock_cfg.recall_mode = "context" + mock_cfg.context_tokens = None + mock_cfg.raw = {} + mock_cfg.dialectic_depth = 1 + mock_cfg.dialectic_depth_levels = None + mock_cfg.init_on_session_start = False + mock_cfg.ai_peer = "hermes" + mock_cfg.resolve_session_name.return_value = "test-sess" + mock_cfg.session_strategy = "shared" with patch( "plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=mock_cfg, - ): + ), patch( + "plugins.memory.honcho.client.get_honcho_client", + return_value=MagicMock(), + ), patch( + "plugins.memory.honcho.session.HonchoSessionManager", + ) as mock_manager_cls: + mock_manager = MagicMock() + mock_manager.get_or_create.return_value = MagicMock(messages=[]) + mock_manager_cls.return_value = mock_manager provider.initialize( session_id="test-sess", user_id="discord_user_789", platform="discord", ) - # The config's peer_name should have been overridden with the user_id - assert mock_cfg.peer_name == "discord_user_789" + assert mock_cfg.peer_name == "static-user" + assert mock_manager_cls.call_args.kwargs["runtime_user_peer_name"] == "discord_user_789" + + def test_session_manager_prefers_runtime_user_id_over_config_peer_name(self): + """Session manager should isolate gateway users even when config peer_name is static.""" + from plugins.memory.honcho.session import HonchoSessionManager + + mock_cfg = MagicMock() + mock_cfg.peer_name = "static-user" + mock_cfg.ai_peer = "hermes" + mock_cfg.write_frequency = "sync" + mock_cfg.dialectic_reasoning_level = "low" + mock_cfg.dialectic_dynamic = True + mock_cfg.dialectic_max_chars = 600 + mock_cfg.observation_mode = "directional" + mock_cfg.user_observe_me = True + mock_cfg.user_observe_others = True + mock_cfg.ai_observe_me = True + mock_cfg.ai_observe_others = True + + manager = HonchoSessionManager( + honcho=MagicMock(), + config=mock_cfg, + runtime_user_peer_name="discord_user_789", + ) + + with patch.object(manager, "_get_or_create_peer", return_value=MagicMock()), patch.object( + manager, + "_get_or_create_honcho_session", + return_value=(MagicMock(), []), + ): + session = manager.get_or_create("discord:channel-1") + + assert session.user_peer_id == "discord_user_789" def test_no_user_id_preserves_config_peer_name(self): """Without user_id, the config peer_name should be preserved.""" diff --git a/tests/honcho_plugin/test_session.py b/tests/honcho_plugin/test_session.py index 7b5ac7e3d..f2a660292 100644 --- a/tests/honcho_plugin/test_session.py +++ b/tests/honcho_plugin/test_session.py @@ -568,15 +568,15 @@ class TestToolsModeInitBehavior: with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \ patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \ - patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager), \ + patch("plugins.memory.honcho.session.HonchoSessionManager", return_value=mock_manager) as mock_manager_cls, \ patch("hermes_constants.get_hermes_home", return_value=MagicMock()): provider.initialize(session_id="test-session-001", **init_kwargs) - return provider, cfg + return provider, cfg, mock_manager_cls def test_tools_lazy_default(self): """tools + initOnSessionStart=false → session NOT initialized after initialize().""" - provider, _ = self._make_provider_with_config( + provider, _, _ = self._make_provider_with_config( recall_mode="tools", init_on_session_start=False, ) assert provider._session_initialized is False @@ -585,7 +585,7 @@ class TestToolsModeInitBehavior: def test_tools_eager_init(self): """tools + initOnSessionStart=true → session IS initialized after initialize().""" - provider, _ = self._make_provider_with_config( + provider, _, _ = self._make_provider_with_config( recall_mode="tools", init_on_session_start=True, ) assert provider._session_initialized is True @@ -593,33 +593,34 @@ class TestToolsModeInitBehavior: def test_tools_eager_prefetch_still_empty(self): """tools mode with eager init still returns empty from prefetch() (no auto-injection).""" - provider, _ = self._make_provider_with_config( + provider, _, _ = self._make_provider_with_config( recall_mode="tools", init_on_session_start=True, ) assert provider.prefetch("test query") == "" def test_tools_lazy_prefetch_empty(self): """tools mode with lazy init also returns empty from prefetch().""" - provider, _ = self._make_provider_with_config( + provider, _, _ = self._make_provider_with_config( recall_mode="tools", init_on_session_start=False, ) assert provider.prefetch("test query") == "" def test_explicit_peer_name_not_overridden_by_user_id(self): """Explicit peerName in config must not be replaced by gateway user_id.""" - _, cfg = self._make_provider_with_config( + _, cfg, _ = self._make_provider_with_config( recall_mode="tools", init_on_session_start=True, peer_name="Kathie", user_id="8439114563", ) assert cfg.peer_name == "Kathie" def test_user_id_used_when_no_peer_name(self): - """Gateway user_id is used as peer_name when no explicit peerName configured.""" - _, cfg = self._make_provider_with_config( + """Gateway user_id is passed separately from config peer_name.""" + _, cfg, mock_manager_cls = self._make_provider_with_config( recall_mode="tools", init_on_session_start=True, peer_name=None, user_id="8439114563", ) - assert cfg.peer_name == "8439114563" + assert cfg.peer_name is None + assert mock_manager_cls.call_args.kwargs["runtime_user_peer_name"] == "8439114563" class TestPerSessionMigrateGuard: