diff --git a/plugins/memory/honcho/__init__.py b/plugins/memory/honcho/__init__.py index e8078ae585..869fe788ae 100644 --- a/plugins/memory/honcho/__init__.py +++ b/plugins/memory/honcho/__init__.py @@ -218,9 +218,11 @@ class HonchoMemoryProvider(MemoryProvider): return # Override peer_name with gateway user_id for per-user memory scoping. - # CLI sessions won't have user_id, so the config default is preserved. + # 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: + if _gw_user_id and not cfg.peer_name: cfg.peer_name = _gw_user_id self._config = cfg @@ -248,6 +250,12 @@ class HonchoMemoryProvider(MemoryProvider): # ----- Port #1957: lazy session init for tools-only mode ----- if self._recall_mode == "tools": + if cfg.init_on_session_start: + # Eager init: create session now so sync_turn() works from turn 1. + # Does NOT enable auto-injection — prefetch() still returns empty. + logger.debug("Honcho tools-only mode — eager session init (initOnSessionStart=true)") + self._do_session_init(cfg, session_id, **kwargs) + return # Defer actual session creation until first tool call self._lazy_init_kwargs = kwargs self._lazy_init_session_id = session_id diff --git a/plugins/memory/honcho/client.py b/plugins/memory/honcho/client.py index e460fd75c2..3c779f64fe 100644 --- a/plugins/memory/honcho/client.py +++ b/plugins/memory/honcho/client.py @@ -189,6 +189,11 @@ class HonchoClientConfig: # "context" — auto-injected context only, Honcho tools removed # "tools" — Honcho tools only, no auto-injected context recall_mode: str = "hybrid" + # When True and recallMode is "tools", create the Honcho session eagerly + # during initialize() instead of deferring to the first tool call. + # This ensures sync_turn() can write from the very first turn. + # Does NOT enable automatic context injection — only changes init timing. + init_on_session_start: bool = False # Observation mode: legacy string shorthand ("directional" or "unified"). # Kept for backward compat; granular per-peer booleans below are preferred. observation_mode: str = "directional" @@ -366,6 +371,11 @@ class HonchoClientConfig: or raw.get("recallMode") or "hybrid" ), + init_on_session_start=_resolve_bool( + host_block.get("initOnSessionStart"), + raw.get("initOnSessionStart"), + default=False, + ), # Migration guard: existing configs without an explicit # observationMode keep the old "unified" default so users # aren't silently switched to full bidirectional observation. diff --git a/tests/honcho_plugin/test_client.py b/tests/honcho_plugin/test_client.py index 71f48351ee..cfb89482d0 100644 --- a/tests/honcho_plugin/test_client.py +++ b/tests/honcho_plugin/test_client.py @@ -500,6 +500,48 @@ class TestObservationModeMigration: assert cfg.ai_observe_others is True +class TestInitOnSessionStart: + """Tests for the initOnSessionStart config field.""" + + def test_default_is_false(self): + config = HonchoClientConfig() + assert config.init_on_session_start is False + + def test_root_level_true(self, tmp_path): + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({ + "apiKey": "k", + "initOnSessionStart": True, + })) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + assert cfg.init_on_session_start is True + + def test_host_block_overrides_root(self, tmp_path): + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({ + "apiKey": "k", + "initOnSessionStart": True, + "hosts": {"hermes": {"initOnSessionStart": False}}, + })) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + assert cfg.init_on_session_start is False + + def test_host_block_true_overrides_root_absent(self, tmp_path): + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({ + "apiKey": "k", + "hosts": {"hermes": {"initOnSessionStart": True}}, + })) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + assert cfg.init_on_session_start is True + + def test_absent_everywhere_defaults_false(self, tmp_path): + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({"apiKey": "k"})) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + assert cfg.init_on_session_start is False + + class TestResetHonchoClient: def test_reset_clears_singleton(self): import plugins.memory.honcho.client as mod diff --git a/tests/honcho_plugin/test_session.py b/tests/honcho_plugin/test_session.py index e3452cf6cb..abf6dee007 100644 --- a/tests/honcho_plugin/test_session.py +++ b/tests/honcho_plugin/test_session.py @@ -275,6 +275,97 @@ class TestPeerLookupHelpers: # --------------------------------------------------------------------------- +# --------------------------------------------------------------------------- +# Provider init behavior: lazy vs eager in tools mode +# --------------------------------------------------------------------------- + + +class TestToolsModeInitBehavior: + """Verify initOnSessionStart controls session init timing in tools mode.""" + + def _make_provider_with_config(self, recall_mode="tools", init_on_session_start=False, + peer_name=None, user_id=None): + """Create a HonchoMemoryProvider with mocked config and dependencies.""" + from plugins.memory.honcho.client import HonchoClientConfig + + cfg = HonchoClientConfig( + api_key="test-key", + enabled=True, + recall_mode=recall_mode, + init_on_session_start=init_on_session_start, + peer_name=peer_name, + ) + + provider = HonchoMemoryProvider() + + # Patch the config loading and session init to avoid real Honcho calls + from unittest.mock import patch, MagicMock + + mock_manager = MagicMock() + mock_session = MagicMock() + mock_session.messages = [] + mock_manager.get_or_create.return_value = mock_session + + init_kwargs = {} + if user_id: + init_kwargs["user_id"] = user_id + + 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("hermes_constants.get_hermes_home", return_value=MagicMock()): + provider.initialize(session_id="test-session-001", **init_kwargs) + + return provider, cfg + + def test_tools_lazy_default(self): + """tools + initOnSessionStart=false → session NOT initialized after initialize().""" + provider, _ = self._make_provider_with_config( + recall_mode="tools", init_on_session_start=False, + ) + assert provider._session_initialized is False + assert provider._manager is None + assert provider._lazy_init_kwargs is not None + + def test_tools_eager_init(self): + """tools + initOnSessionStart=true → session IS initialized after initialize().""" + provider, _ = self._make_provider_with_config( + recall_mode="tools", init_on_session_start=True, + ) + assert provider._session_initialized is True + assert provider._manager is not None + + 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( + 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( + 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( + 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( + recall_mode="tools", init_on_session_start=True, + peer_name=None, user_id="8439114563", + ) + assert cfg.peer_name == "8439114563" + + class TestChunkMessage: def test_short_message_single_chunk(self): result = HonchoMemoryProvider._chunk_message("hello world", 100)