{"repo":"NousResearch/hermes-agent","public_id":"5726","display_label":"Honcho startup blocking and slow init issues","summary":"These issues and fixes address Honcho initialization blocking Desktop startup and resume, with changes to make tool init non-blocking, startup fail-open, and chat resumption lazy to avoid multi-minute delays.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":24847},"resolution":{"state":"partially_resolved","by":"pr-24847"},"freshness":{"state":"verified","last_verified_at":"2026-06-02T03:13:43+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"fix_edges":[{"issue_number":5726,"pr_number":24847,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":5726,"title":"Slow startup: Honcho memory provider blocks initialization for ~60s+ per step","url":null,"state":"closed"},{"kind":"issue","number":51492,"title":"Honcho initOnSessionStart can block Desktop resume/startup","url":null,"state":"closed"},{"kind":"pr","number":24847,"title":"fix(honcho): make startup fail open","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/honcho/__init__.py","tests/test_honcho_startup_fail_open.py"],"fix_hunks":[{"file":"plugins/memory/honcho/__init__.py","hunks":["@@ -228,6 +228,9 @@ def __init__(self):\n         self._session_initialized = False\n         self._lazy_init_kwargs: Optional[dict] = None\n         self._lazy_init_session_id: Optional[str] = None\n+        self._init_thread: Optional[threading.Thread] = None\n+        self._init_lock = threading.Lock()\n+        self._init_error = \"\"\n \n         # Port #4053: cron guard — when True, plugin is fully inactive\n         self._cron_skipped = False","@@ -326,22 +329,24 @@ def initialize(self, session_id: str, **kwargs) -> None:\n             # only come from honcho.json (host block or root) or the default.\n             # See scratch/memory-plugin-ux-specs.md #10 for rationale.\n \n-            # ----- Port #1957: lazy session init for tools-only mode -----\n+            self._lazy_init_kwargs = dict(kwargs)\n+            self._lazy_init_session_id = session_id\n+            self._session_key = self._resolve_session_key(cfg, session_id, **kwargs)\n+\n+            # Network-backed session creation can block on Honcho service or DB\n+            # outages. Startup must fail open for context/hybrid modes, where\n+            # Honcho is initialized only to enrich prompts. Tools-only mode has\n+            # an explicit contract: init_on_session_start=False stays lazy until\n+            # the first tool call, while init_on_session_start=True remains an\n+            # eager, ready-on-return initialization path.\n             if self._recall_mode == \"tools\":\n                 if cfg.init_on_session_start:\n-                    # Eager init even in tools mode (opt-in)\n-                    self._do_session_init(cfg, session_id, **kwargs)\n+                    self._ensure_session()\n                     return\n-                # Defer actual session creation until first tool call\n-                self._lazy_init_kwargs = kwargs\n-                self._lazy_init_session_id = session_id\n-                # Still need a client reference for _ensure_session\n-                self._config = cfg\n                 logger.debug(\"Honcho tools-only mode — deferring session init until first tool call\")\n                 return\n \n-            # ----- Eager init (context or hybrid mode) -----\n-            self._do_session_init(cfg, session_id, **kwargs)\n+            self._start_session_init_background(wait_timeout=0.1)\n \n         except ImportError:\n             logger.debug(\"honcho-ai package not installed — plugin inactive\")","@@ -349,6 +354,66 @@ def initialize(self, session_id: str, **kwargs) -> None:\n             logger.warning(\"Honcho init failed: %s\", e)\n             self._manager = None\n \n+    def _resolve_session_key(self, cfg, session_id: str, **kwargs) -> str:\n+        \"\"\"Resolve the Honcho session key without touching the network.\"\"\"\n+        session_title = kwargs.get(\"session_title\")\n+        gateway_session_key = kwargs.get(\"gateway_session_key\")\n+        return (\n+            cfg.resolve_session_name(\n+                session_title=session_title,\n+                session_id=session_id,\n+                gateway_session_key=gateway_session_key,\n+            )\n+            or session_id\n+            or \"hermes-default\"\n+        )\n+\n+    def _start_session_init_background(self, *, wait_timeout: float = 0.0) -> None:\n+        \"\"\"Start Honcho session initialization in a daemon thread.\n+\n+        This keeps Hermes CLI/gateway startup responsive when Honcho is down,\n+        slow, or its database is unhealthy. The thread may still take the SDK\n+        timeout path, but it cannot block agent construction or first prompt\n+        assembly. ``wait_timeout`` lets fast/mock initializations finish before\n+        returning while still failing open for slow backends.\n+        \"\"\"\n+        if self._cron_skipped or self._session_initialized:\n+            return\n+        if not self._config or self._lazy_init_kwargs is None:\n+            return\n+\n+        with self._init_lock:\n+            if self._cron_skipped or self._session_initialized:\n+                return\n+            if self._init_thread and self._init_thread.is_alive():\n+                return\n+            if not self._config or self._lazy_init_kwargs is None:\n+                return\n+\n+            cfg = self._config\n+            init_kwargs = dict(self._lazy_init_kwargs)\n+            init_session_id = self._lazy_init_session_id or \"hermes-default\"\n+\n+            def _run() -> None:\n+                try:\n+                    self._do_session_init(cfg, init_session_id, **init_kwargs)\n+                    self._lazy_init_kwargs = None\n+                    self._lazy_init_session_id = None\n+                    self._init_error = \"\"\n+                except Exception as e:\n+                    self._init_error = str(e)\n+                    self._manager = None\n+                    logger.warning(\"Honcho background session init failed: %s\", e)\n+\n+            self._init_thread = threading.Thread(\n+                target=_run,\n+                daemon=True,\n+                name=\"honcho-session-init\",\n+            )\n+            self._init_thread.start()\n+            if wait_timeout > 0:\n+                self._init_thread.join(timeout=wait_timeout)\n+\n     def _do_session_init(self, cfg, session_id: str, **kwargs) -> None:\n         \"\"\"Shared session initialization logic for both eager and lazy paths.\"\"\"\n         from plugins.memory.honcho.client import get_honcho_client","@@ -363,22 +428,15 @@ def _do_session_init(self, cfg, session_id: str, **kwargs) -> None:\n         )\n \n         # ----- B3: resolve_session_name -----\n-        session_title = kwargs.get(\"session_title\")\n-        gateway_session_key = kwargs.get(\"gateway_session_key\")\n-        self._session_key = (\n-            cfg.resolve_session_name(\n-                session_title=session_title,\n-                session_id=session_id,\n-                gateway_session_key=gateway_session_key,\n-            )\n-            or session_id\n-            or \"hermes-default\"\n-        )\n+        self._session_key = self._resolve_session_key(cfg, session_id, **kwargs)\n         logger.debug(\"Honcho session key resolved: %s\", self._session_key)\n \n-        # Create session eagerly\n+        # Create the remote session before running startup-only migration and\n+        # prewarm work. Do not mark the provider ready until this method's\n+        # synchronous setup has finished; background startup sets _manager before\n+        # get_or_create()/migration/prewarm are complete, and lifecycle hooks must\n+        # not treat that partially initialized state as usable.\n         session = self._manager.get_or_create(self._session_key)\n-        self._session_initialized = True\n \n         # ----- B6: Memory file migration (one-time, for new sessions) -----\n         # Skip under per-session strategy: every Hermes run creates a fresh","@@ -433,12 +491,15 @@ def _prewarm_dialectic() -> None:\n                     self._dialectic_empty_streak += 1\n \n             self._prefetch_thread_started_at = time.monotonic()\n-            self._prefetch_thread = threading.Thread(\n+            prewarm_thread = threading.Thread(\n                 target=_prewarm_dialectic, daemon=True, name=\"honcho-prewarm-dialectic\"\n             )\n-            self._prefetch_thread.start()\n+            prewarm_thread.start()\n+            self._prefetch_thread = prewarm_thread\n             logger.debug(\"Honcho pre-warm started for session: %s\", self._session_key)\n \n+        self._session_initialized = True\n+\n     def _ensure_session(self) -> bool:\n         \"\"\"Lazily initialize the Honcho session (for tools-only mode).\n ","@@ -448,7 +509,9 @@ def _ensure_session(self) -> bool:\n             return True\n         if self._cron_skipped:\n             return False\n-        if not self._config or not self._lazy_init_kwargs:\n+        if self._init_thread and self._init_thread.is_alive():\n+            return False\n+        if not self._config or self._lazy_init_kwargs is None:\n             return False\n \n         try:","@@ -462,9 +525,26 @@ def _ensure_session(self) -> bool:\n             self._lazy_init_session_id = None\n             return self._manager is not None\n         except Exception as e:\n+            self._manager = None\n+            self._session_initialized = False\n             logger.warning(\"Honcho lazy session init failed: %s\", e)\n             return False\n \n+    def _session_ready(self) -> bool:\n+        \"\"\"Return whether a manager/session key can be used safely.\n+\n+        Background initialization sets ``_manager`` before the blocking\n+        get-or-create call completes, so ``_session_initialized`` guards real\n+        async startup. Tests and legacy direct construction may inject a ready\n+        manager/session key without setting that flag; allow that only when no\n+        init thread is currently in flight.\n+        \"\"\"\n+        if not self._manager or not self._session_key:\n+            return False\n+        if self._session_initialized:\n+            return True\n+        return not (self._init_thread and self._init_thread.is_alive())\n+\n     def _format_first_turn_context(self, ctx: dict) -> str:\n         \"\"\"Format the prefetch context dict into a readable system prompt block.\"\"\"\n         parts = []","@@ -504,14 +584,8 @@ def system_prompt_block(self) -> str:\n         if self._cron_skipped:\n             return \"\"\n         if not self._manager or not self._session_key:\n-            # tools-only mode without session yet still returns a minimal block\n-            if self._recall_mode == \"tools\" and self._config:\n-                return (\n-                    \"# Honcho Memory\\n\"\n-                    \"Active (tools-only mode). Use honcho_profile, honcho_search, \"\n-                    \"honcho_reasoning, honcho_context, and honcho_conclude tools to access user memory.\"\n-                )\n-            return \"\"\n+            if not self._config:\n+                return \"\"\n \n         # ----- B1: adapt text based on recall_mode -----\n         if self._recall_mode == \"context\":","@@ -562,6 +636,10 @@ def prefetch(self, query: str, *, session_id: str = \"\") -> str:\n         if self._recall_mode == \"tools\":\n             return \"\"\n \n+        if not self._session_ready():\n+            self._start_session_init_background()\n+            return \"\"\n+\n         # B5: injection_frequency — if \"first-turn\" and past first turn, return empty.\n         # _turn_count is 1-indexed (first user message = 1), so > 1 means \"past first\".\n         if self._injection_frequency == \"first-turn\" and self._turn_count > 1:","@@ -574,18 +652,17 @@ def prefetch(self, query: str, *, session_id: str = \"\") -> str:\n         parts = []\n \n         # ----- Layer 1: Base context (representation + card) -----\n-        # On first call, fetch synchronously so turn 1 isn't empty.\n-        # After that, serve from cache and refresh in background on cadence.\n+        # First fetch is asynchronous: a slow Honcho backend must not block the\n+        # first response. Serve empty context now and consume the background\n+        # result on a later turn.\n         with self._base_context_lock:\n             if self._base_context_cache is None:\n-                # First call — synchronous fetch\n+                self._base_context_cache = \"\"\n+                self._last_context_turn = self._turn_count\n                 try:\n-                    ctx = self._manager.get_prefetch_context(self._session_key)\n-                    self._base_context_cache = self._format_first_turn_context(ctx) if ctx else \"\"\n-                    self._last_context_turn = self._turn_count\n+                    self._manager.prefetch_context(self._session_key, query or None)\n                 except Exception as e:\n-                    logger.debug(\"Honcho base context fetch failed: %s\", e)\n-                    self._base_context_cache = \"\"\n+                    logger.debug(\"Honcho base context prefetch failed: %s\", e)\n             base_context = self._base_context_cache\n \n         # Check if background context prefetch has a fresher result","@@ -640,10 +717,11 @@ def _run_first_turn() -> None:\n                     self._dialectic_empty_streak += 1\n \n             self._prefetch_thread_started_at = time.monotonic()\n-            self._prefetch_thread = threading.Thread(\n+            first_turn_thread = threading.Thread(\n                 target=_run_first_turn, daemon=True, name=\"honcho-prefetch-first\"\n             )\n-            self._prefetch_thread.start()\n+            first_turn_thread.start()\n+            self._prefetch_thread = first_turn_thread\n             self._prefetch_thread.join(timeout=_first_turn_timeout)\n             if self._prefetch_thread.is_alive():\n                 logger.debug(","@@ -708,13 +786,14 @@ def queue_prefetch(self, query: str, *, session_id: str = \"\") -> None:\n         \"\"\"\n         if self._cron_skipped:\n             return\n-        if not self._manager or not self._session_key or not query:\n-            return\n-\n         # B1: tools-only mode — no prefetch\n         if self._recall_mode == \"tools\":\n             return\n \n+        if not self._session_ready() or not query:\n+            self._start_session_init_background()\n+            return\n+\n         # Trivial prompts don't warrant either a context refresh or a dialectic call.\n         if self._is_trivial_prompt(query):\n             return","@@ -768,10 +847,11 @@ def _run():\n                 self._dialectic_empty_streak += 1\n \n         self._prefetch_thread_started_at = time.monotonic()\n-        self._prefetch_thread = threading.Thread(\n+        prefetch_thread = threading.Thread(\n             target=_run, daemon=True, name=\"honcho-prefetch\"\n         )\n-        self._prefetch_thread.start()\n+        prefetch_thread.start()\n+        self._prefetch_thread = prefetch_thread\n \n     # ----- Dialectic depth: multi-pass .chat() with cold/warm prompts -----\n ","@@ -1125,7 +1205,10 @@ def sync_turn(self, user_content: str, assistant_content: str, *, session_id: st\n         \"\"\"\n         if self._cron_skipped:\n             return\n-        if not self._manager or not self._session_key:\n+        if self._recall_mode == \"tools\" and not self._session_ready():\n+            return\n+        if not self._session_ready():\n+            self._start_session_init_background()\n             return\n \n         msg_limit = self._config.message_max_chars if self._config else 25000","@@ -1168,7 +1251,10 @@ def on_memory_write(\n             return\n         if self._cron_skipped:\n             return\n-        if not self._manager or not self._session_key:\n+        if self._recall_mode == \"tools\" and not self._session_ready():\n+            return\n+        if not self._session_ready():\n+            self._start_session_init_background()\n             return\n \n         def _write():","@@ -1186,6 +1272,8 @@ def on_session_end(self, messages: List[Dict[str, Any]]) -> None:\n             return\n         if not self._manager:\n             return\n+        if not self._session_initialized and self._init_thread and self._init_thread.is_alive():\n+            return\n         # Wait for pending sync\n         if self._sync_thread and self._sync_thread.is_alive():\n             self._sync_thread.join(timeout=10.0)","@@ -1212,6 +1300,8 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:\n \n         # Port #1957: ensure session is initialized for tools-only mode\n         if not self._session_initialized:\n+            if self._init_thread and self._init_thread.is_alive():\n+                return tool_error(\"Honcho session is still initializing; try again shortly.\")\n             if not self._ensure_session():\n                 return tool_error(\"Honcho session could not be initialized.\")\n ","@@ -1312,7 +1402,7 @@ def shutdown(self) -> None:\n             if t and t.is_alive():\n                 t.join(timeout=5.0)\n         # Flush any remaining messages\n-        if self._manager:\n+        if self._manager and not (self._init_thread and self._init_thread.is_alive() and not self._session_initialized):\n             try:\n                 self._manager.flush_all()\n             except Exception:"]},{"file":"tests/test_honcho_startup_fail_open.py","hunks":["@@ -0,0 +1,357 @@\n+\"\"\"Regression tests for Honcho startup fail-open behavior.\"\"\"\n+\n+from __future__ import annotations\n+\n+import json\n+import threading\n+import time\n+from types import SimpleNamespace\n+\n+from plugins.memory.honcho import HonchoMemoryProvider\n+\n+\n+class _FakeHonchoConfig(SimpleNamespace):\n+    def resolve_session_name(self, **kwargs):\n+        return \"test-session\"\n+\n+\n+def _configured_hybrid_config() -> _FakeHonchoConfig:\n+    return _FakeHonchoConfig(\n+        enabled=True,\n+        api_key=None,\n+        base_url=\"http://127.0.0.1:8000\",\n+        recall_mode=\"hybrid\",\n+        init_on_session_start=False,\n+        dialectic_depth=1,\n+        dialectic_depth_levels=None,\n+        reasoning_heuristic=True,\n+        reasoning_level_cap=\"high\",\n+        context_tokens=None,\n+        message_max_chars=25000,\n+        session_strategy=\"per-directory\",\n+    )\n+\n+\n+def _configured_tools_config(*, init_on_session_start: bool = False) -> _FakeHonchoConfig:\n+    cfg = _configured_hybrid_config()\n+    cfg.recall_mode = \"tools\"\n+    cfg.init_on_session_start = init_on_session_start\n+    return cfg\n+\n+\n+def test_honcho_hybrid_initialize_returns_without_waiting_for_session_init(monkeypatch):\n+    \"\"\"Slow Honcho session creation must not block agent startup.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    cfg = _configured_hybrid_config()\n+    started = threading.Event()\n+    release = threading.Event()\n+\n+    monkeypatch.setattr(\n+        \"plugins.memory.honcho.client.HonchoClientConfig.from_global_config\",\n+        lambda: cfg,\n+    )\n+\n+    def slow_session_init(self, cfg, session_id, **kwargs):\n+        started.set()\n+        release.wait(timeout=5)\n+        self._session_initialized = True\n+\n+    monkeypatch.setattr(HonchoMemoryProvider, \"_do_session_init\", slow_session_init)\n+\n+    start = time.perf_counter()\n+    provider.initialize(\"session-1\", platform=\"cli\")\n+    elapsed = time.perf_counter() - start\n+\n+    try:\n+        assert elapsed < 0.5\n+        assert started.wait(timeout=1)\n+        assert provider._session_key == \"test-session\"\n+    finally:\n+        release.set()\n+        init_thread = getattr(provider, \"_init_thread\", None)\n+        if init_thread:\n+            init_thread.join(timeout=1)\n+\n+\n+def test_honcho_background_init_rechecks_state_after_lock_race():\n+    \"\"\"Startup should not spawn/crash if init completes while waiting for lock.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    provider._config = _configured_hybrid_config()\n+    provider._lazy_init_kwargs = {\"platform\": \"cli\"}\n+    provider._lazy_init_session_id = \"session-1\"\n+\n+    class RacingLock:\n+        def __enter__(self):\n+            provider._session_initialized = True\n+            provider._lazy_init_kwargs = None\n+            return self\n+\n+        def __exit__(self, exc_type, exc, tb):\n+            return False\n+\n+    provider._init_lock = RacingLock()\n+\n+    provider._start_session_init_background()\n+\n+    assert provider._init_thread is None\n+    assert provider._session_initialized is True\n+\n+\n+def test_honcho_prefetch_returns_without_waiting_for_first_context_fetch():\n+    \"\"\"First-turn context injection must fail open when Honcho is slow.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    cfg = _configured_hybrid_config()\n+    cfg.timeout = 0.1\n+    fetch_started = threading.Event()\n+\n+    class SlowManager:\n+        def get_prefetch_context(self, session_key, user_message=None):\n+            fetch_started.set()\n+            time.sleep(5)\n+            return {\"representation\": \"late\"}\n+\n+        def prefetch_context(self, session_key, user_message=None):\n+            fetch_started.set()\n+\n+        def pop_context_result(self, session_key):\n+            return {}\n+\n+    provider._config = cfg\n+    provider._manager = SlowManager()\n+    provider._session_key = \"test-session\"\n+    provider._session_initialized = True\n+    provider._turn_count = 1\n+\n+    start = time.perf_counter()\n+    result = provider.prefetch(\"what do you know about me?\")\n+    elapsed = time.perf_counter() - start\n+\n+    assert result == \"\"\n+    assert elapsed < 0.5\n+    assert fetch_started.is_set()\n+\n+\n+\n+def test_honcho_sync_turn_does_not_start_network_write_before_session_init():\n+    \"\"\"Session-end sync must not create a blocking writer before init finishes.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    cfg = _configured_hybrid_config()\n+    get_started = threading.Event()\n+    background_started = threading.Event()\n+    release_init = threading.Event()\n+\n+    class SlowManager:\n+        def get_or_create(self, session_key):\n+            get_started.set()\n+            time.sleep(5)\n+            return SimpleNamespace()\n+\n+        def _flush_session(self, session):\n+            pass\n+\n+    provider._config = cfg\n+    provider._manager = SlowManager()\n+    provider._session_key = \"test-session\"\n+    provider._session_initialized = False\n+    provider._start_session_init_background = background_started.set\n+    provider._init_thread = threading.Thread(\n+        target=lambda: release_init.wait(timeout=5), daemon=True\n+    )\n+    provider._init_thread.start()\n+\n+    try:\n+        provider.sync_turn(\"hello\", \"world\")\n+\n+        assert provider._sync_thread is None\n+        assert background_started.is_set()\n+        assert not get_started.wait(timeout=0.1)\n+    finally:\n+        release_init.set()\n+        provider._init_thread.join(timeout=1)\n+\n+\n+def test_honcho_sync_turn_waits_for_full_background_startup(monkeypatch):\n+    \"\"\"Manager assignment alone is not readiness while background init continues.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    cfg = _configured_hybrid_config()\n+    session_created = threading.Event()\n+    migration_started = threading.Event()\n+    release_migration = threading.Event()\n+    get_calls = []\n+\n+    class StartupManager:\n+        def __init__(self, *args, **kwargs):\n+            pass\n+\n+        def get_or_create(self, session_key):\n+            get_calls.append(session_key)\n+            session_created.set()\n+            return SimpleNamespace(messages=[])\n+\n+        def migrate_memory_files(self, session_key, mem_dir):\n+            migration_started.set()\n+            release_migration.wait(timeout=5)\n+\n+        def prefetch_context(self, session_key, user_message=None):\n+            pass\n+\n+        def _flush_session(self, session):\n+            pass\n+\n+    monkeypatch.setattr(\n+        \"plugins.memory.honcho.client.HonchoClientConfig.from_global_config\",\n+        lambda: cfg,\n+    )\n+    monkeypatch.setattr(\"plugins.memory.honcho.client.get_honcho_client\", lambda cfg: object())\n+    monkeypatch.setattr(\"plugins.memory.honcho.session.HonchoSessionManager\", StartupManager)\n+\n+    provider.initialize(\"session-1\", platform=\"cli\")\n+    try:\n+        assert session_created.wait(timeout=1)\n+        assert migration_started.wait(timeout=1)\n+        assert provider._manager is not None\n+        assert provider._session_initialized is False\n+\n+        provider.sync_turn(\"hello\", \"world\")\n+\n+        assert provider._sync_thread is None\n+        assert get_calls == [\"test-session\"]\n+    finally:\n+        release_migration.set()\n+        init_thread = getattr(provider, \"_init_thread\", None)\n+        if init_thread:\n+            init_thread.join(timeout=1)\n+        if provider._prefetch_thread:\n+            provider._prefetch_thread.join(timeout=1)\n+\n+    assert provider._session_initialized is True\n+\n+\n+def test_honcho_system_prompt_advertises_active_while_background_init_runs(monkeypatch):\n+    \"\"\"Prompt metadata should not require a completed network session.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    cfg = _configured_hybrid_config()\n+    release = threading.Event()\n+\n+    monkeypatch.setattr(\n+        \"plugins.memory.honcho.client.HonchoClientConfig.from_global_config\",\n+        lambda: cfg,\n+    )\n+\n+    def slow_session_init(self, cfg, session_id, **kwargs):\n+        release.wait(timeout=5)\n+        self._session_initialized = True\n+\n+    monkeypatch.setattr(HonchoMemoryProvider, \"_do_session_init\", slow_session_init)\n+\n+    provider.initialize(\"session-1\", platform=\"cli\")\n+    try:\n+        prompt = provider.system_prompt_block()\n+        assert \"Honcho Memory\" in prompt\n+        assert \"hybrid mode\" in prompt\n+    finally:\n+        release.set()\n+        init_thread = getattr(provider, \"_init_thread\", None)\n+        if init_thread:\n+            init_thread.join(timeout=1)\n+\n+\n+def test_honcho_tools_eager_init_still_ready_on_return(monkeypatch):\n+    \"\"\"tools + initOnSessionStart=true keeps its ready-on-return contract.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    cfg = _configured_tools_config(init_on_session_start=True)\n+\n+    monkeypatch.setattr(\n+        \"plugins.memory.honcho.client.HonchoClientConfig.from_global_config\",\n+        lambda: cfg,\n+    )\n+\n+    def fake_session_init(self, cfg, session_id, **kwargs):\n+        self._manager = SimpleNamespace()\n+        self._session_key = \"test-session\"\n+        self._session_initialized = True\n+\n+    monkeypatch.setattr(HonchoMemoryProvider, \"_do_session_init\", fake_session_init)\n+\n+    provider.initialize(\"session-1\", platform=\"cli\")\n+\n+    assert provider._session_initialized is True\n+    assert provider._manager is not None\n+    assert provider._init_thread is None\n+\n+\n+def test_honcho_tools_eager_init_failure_does_not_leave_ready_manager(monkeypatch):\n+    \"\"\"Failed eager tools startup must not leave hooks seeing a ready session.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    cfg = _configured_tools_config(init_on_session_start=True)\n+\n+    monkeypatch.setattr(\n+        \"plugins.memory.honcho.client.HonchoClientConfig.from_global_config\",\n+        lambda: cfg,\n+    )\n+\n+    def failing_session_init(self, cfg, session_id, **kwargs):\n+        self._manager = SimpleNamespace()\n+        self._session_key = \"test-session\"\n+        raise RuntimeError(\"boom\")\n+\n+    monkeypatch.setattr(HonchoMemoryProvider, \"_do_session_init\", failing_session_init)\n+\n+    provider.initialize(\"session-1\", platform=\"cli\")\n+    assert provider._session_initialized is False\n+    assert provider._manager is None\n+\n+    background_started = threading.Event()\n+    provider._start_session_init_background = background_started.set\n+    provider.sync_turn(\"hello\", \"world\")\n+    provider.on_memory_write(\"add\", \"user\", \"prefers safe Honcho startup\")\n+\n+    assert provider._sync_thread is None\n+    assert not background_started.is_set()\n+\n+    result = json.loads(provider.handle_tool_call(\"honcho_profile\", {\"peer\": \"user\"}))\n+    assert \"could not be initialized\" in result[\"error\"]\n+    assert provider._manager is None\n+\n+\n+def test_honcho_tools_lazy_hooks_do_not_prestart_background_init(monkeypatch):\n+    \"\"\"tools lazy mode lets the first tool call own session initialization.\"\"\"\n+    provider = HonchoMemoryProvider()\n+    cfg = _configured_tools_config(init_on_session_start=False)\n+\n+    monkeypatch.setattr(\n+        \"plugins.memory.honcho.client.HonchoClientConfig.from_global_config\",\n+        lambda: cfg,\n+    )\n+\n+    provider.initialize(\"session-1\", platform=\"cli\")\n+    background_started = threading.Event()\n+    provider._start_session_init_background = background_started.set\n+\n+    provider.prefetch(\"what do you know?\")\n+    provider.queue_prefetch(\"what do you know?\")\n+    provider.sync_turn(\"hello\", \"world\")\n+    provider.on_memory_write(\"add\", \"user\", \"prefers fail-open memory\")\n+\n+    assert not background_started.is_set()\n+    assert provider._session_initialized is False\n+\n+    class ToolManager:\n+        def get_peer_card(self, session_key, peer=\"user\"):\n+            return [\"ready\"]\n+\n+    init_calls = []\n+\n+    def fake_session_init(self, cfg, session_id, **kwargs):\n+        init_calls.append(session_id)\n+        self._manager = ToolManager()\n+        self._session_key = \"test-session\"\n+        self._session_initialized = True\n+\n+    monkeypatch.setattr(HonchoMemoryProvider, \"_do_session_init\", fake_session_init)\n+\n+    result = json.loads(provider.handle_tool_call(\"honcho_profile\", {\"peer\": \"user\"}))\n+\n+    assert result == {\"result\": [\"ready\"]}\n+    assert init_calls == [\"session-1\"]\n+    assert not background_started.is_set()\n"]}]},{"kind":"pr","number":51501,"title":"fix(desktop): lazy resume chats and harden backend cleanup","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#51492","number":51492}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":51562,"title":"fix(honcho): make tools eager init nonblocking","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#62290","number":62290}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}