mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(honcho): scope gateway sessions by runtime user id
This commit is contained in:
parent
ba7da73ca9
commit
5b6792f04d
4 changed files with 75 additions and 29 deletions
|
|
@ -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 -----
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue