fix: make Honcho startup fail open

This commit is contained in:
Stephen Schoettler
2026-05-12 15:03:27 -07:00
committed by Teknium
parent 59510d7b44
commit f24b7ed9d9
2 changed files with 499 additions and 52 deletions

View File

@ -228,6 +228,9 @@ class HonchoMemoryProvider(MemoryProvider):
self._session_initialized = False self._session_initialized = False
self._lazy_init_kwargs: Optional[dict] = None self._lazy_init_kwargs: Optional[dict] = None
self._lazy_init_session_id: Optional[str] = None self._lazy_init_session_id: Optional[str] = None
self._init_thread: Optional[threading.Thread] = None
self._init_lock = threading.Lock()
self._init_error = ""
# Port #4053: cron guard — when True, plugin is fully inactive # Port #4053: cron guard — when True, plugin is fully inactive
self._cron_skipped = False self._cron_skipped = False
@ -326,22 +329,24 @@ class HonchoMemoryProvider(MemoryProvider):
# aiPeer comes from honcho.json (host block or root) only. # aiPeer comes from honcho.json (host block or root) only.
# SOUL.md is persona content, not identity config. # SOUL.md is persona content, not identity config.
# ----- Port #1957: lazy session init for tools-only mode ----- self._lazy_init_kwargs = dict(kwargs)
self._lazy_init_session_id = session_id
self._session_key = self._resolve_session_key(cfg, session_id, **kwargs)
# Network-backed session creation can block on Honcho service or DB
# outages. Startup must fail open for context/hybrid modes, where
# Honcho is initialized only to enrich prompts. Tools-only mode has
# an explicit contract: init_on_session_start=False stays lazy until
# the first tool call, while init_on_session_start=True remains an
# eager, ready-on-return initialization path.
if self._recall_mode == "tools": if self._recall_mode == "tools":
if cfg.init_on_session_start: if cfg.init_on_session_start:
# Eager init even in tools mode (opt-in) self._ensure_session()
self._do_session_init(cfg, session_id, **kwargs)
return return
# Defer actual session creation until first tool call
self._lazy_init_kwargs = kwargs
self._lazy_init_session_id = session_id
# Still need a client reference for _ensure_session
self._config = cfg
logger.debug("Honcho tools-only mode — deferring session init until first tool call") logger.debug("Honcho tools-only mode — deferring session init until first tool call")
return return
# ----- Eager init (context or hybrid mode) ----- self._start_session_init_background(wait_timeout=0.1)
self._do_session_init(cfg, session_id, **kwargs)
except ImportError: except ImportError:
logger.debug("honcho-ai package not installed — plugin inactive") logger.debug("honcho-ai package not installed — plugin inactive")
@ -349,6 +354,66 @@ class HonchoMemoryProvider(MemoryProvider):
logger.warning("Honcho init failed: %s", e) logger.warning("Honcho init failed: %s", e)
self._manager = None self._manager = None
def _resolve_session_key(self, cfg, session_id: str, **kwargs) -> str:
"""Resolve the Honcho session key without touching the network."""
session_title = kwargs.get("session_title")
gateway_session_key = kwargs.get("gateway_session_key")
return (
cfg.resolve_session_name(
session_title=session_title,
session_id=session_id,
gateway_session_key=gateway_session_key,
)
or session_id
or "hermes-default"
)
def _start_session_init_background(self, *, wait_timeout: float = 0.0) -> None:
"""Start Honcho session initialization in a daemon thread.
This keeps Hermes CLI/gateway startup responsive when Honcho is down,
slow, or its database is unhealthy. The thread may still take the SDK
timeout path, but it cannot block agent construction or first prompt
assembly. ``wait_timeout`` lets fast/mock initializations finish before
returning while still failing open for slow backends.
"""
if self._cron_skipped or self._session_initialized:
return
if not self._config or self._lazy_init_kwargs is None:
return
with self._init_lock:
if self._cron_skipped or self._session_initialized:
return
if self._init_thread and self._init_thread.is_alive():
return
if not self._config or self._lazy_init_kwargs is None:
return
cfg = self._config
init_kwargs = dict(self._lazy_init_kwargs)
init_session_id = self._lazy_init_session_id or "hermes-default"
def _run() -> None:
try:
self._do_session_init(cfg, init_session_id, **init_kwargs)
self._lazy_init_kwargs = None
self._lazy_init_session_id = None
self._init_error = ""
except Exception as e:
self._init_error = str(e)
self._manager = None
logger.warning("Honcho background session init failed: %s", e)
self._init_thread = threading.Thread(
target=_run,
daemon=True,
name="honcho-session-init",
)
self._init_thread.start()
if wait_timeout > 0:
self._init_thread.join(timeout=wait_timeout)
def _do_session_init(self, cfg, session_id: str, **kwargs) -> None: def _do_session_init(self, cfg, session_id: str, **kwargs) -> None:
"""Shared session initialization logic for both eager and lazy paths.""" """Shared session initialization logic for both eager and lazy paths."""
from plugins.memory.honcho.client import get_honcho_client from plugins.memory.honcho.client import get_honcho_client
@ -364,22 +429,15 @@ class HonchoMemoryProvider(MemoryProvider):
) )
# ----- B3: resolve_session_name ----- # ----- B3: resolve_session_name -----
session_title = kwargs.get("session_title") self._session_key = self._resolve_session_key(cfg, session_id, **kwargs)
gateway_session_key = kwargs.get("gateway_session_key")
self._session_key = (
cfg.resolve_session_name(
session_title=session_title,
session_id=session_id,
gateway_session_key=gateway_session_key,
)
or session_id
or "hermes-default"
)
logger.debug("Honcho session key resolved: %s", self._session_key) logger.debug("Honcho session key resolved: %s", self._session_key)
# Create session eagerly # Create the remote session before running startup-only migration and
# prewarm work. Do not mark the provider ready until this method's
# synchronous setup has finished; background startup sets _manager before
# get_or_create()/migration/prewarm are complete, and lifecycle hooks must
# not treat that partially initialized state as usable.
session = self._manager.get_or_create(self._session_key) session = self._manager.get_or_create(self._session_key)
self._session_initialized = True
# ----- B6: Memory file migration (one-time, for new sessions) ----- # ----- B6: Memory file migration (one-time, for new sessions) -----
# Skip under per-session strategy: every Hermes run creates a fresh # Skip under per-session strategy: every Hermes run creates a fresh
@ -434,12 +492,15 @@ class HonchoMemoryProvider(MemoryProvider):
self._dialectic_empty_streak += 1 self._dialectic_empty_streak += 1
self._prefetch_thread_started_at = time.monotonic() self._prefetch_thread_started_at = time.monotonic()
self._prefetch_thread = threading.Thread( prewarm_thread = threading.Thread(
target=_prewarm_dialectic, daemon=True, name="honcho-prewarm-dialectic" target=_prewarm_dialectic, daemon=True, name="honcho-prewarm-dialectic"
) )
self._prefetch_thread.start() prewarm_thread.start()
self._prefetch_thread = prewarm_thread
logger.debug("Honcho pre-warm started for session: %s", self._session_key) logger.debug("Honcho pre-warm started for session: %s", self._session_key)
self._session_initialized = True
def _ensure_session(self) -> bool: def _ensure_session(self) -> bool:
"""Lazily initialize the Honcho session (for tools-only mode). """Lazily initialize the Honcho session (for tools-only mode).
@ -449,7 +510,9 @@ class HonchoMemoryProvider(MemoryProvider):
return True return True
if self._cron_skipped: if self._cron_skipped:
return False return False
if not self._config or not self._lazy_init_kwargs: if self._init_thread and self._init_thread.is_alive():
return False
if not self._config or self._lazy_init_kwargs is None:
return False return False
try: try:
@ -463,9 +526,26 @@ class HonchoMemoryProvider(MemoryProvider):
self._lazy_init_session_id = None self._lazy_init_session_id = None
return self._manager is not None return self._manager is not None
except Exception as e: except Exception as e:
self._manager = None
self._session_initialized = False
logger.warning("Honcho lazy session init failed: %s", e) logger.warning("Honcho lazy session init failed: %s", e)
return False return False
def _session_ready(self) -> bool:
"""Return whether a manager/session key can be used safely.
Background initialization sets ``_manager`` before the blocking
get-or-create call completes, so ``_session_initialized`` guards real
async startup. Tests and legacy direct construction may inject a ready
manager/session key without setting that flag; allow that only when no
init thread is currently in flight.
"""
if not self._manager or not self._session_key:
return False
if self._session_initialized:
return True
return not (self._init_thread and self._init_thread.is_alive())
def _format_first_turn_context(self, ctx: dict) -> str: def _format_first_turn_context(self, ctx: dict) -> str:
"""Format the prefetch context dict into a readable system prompt block.""" """Format the prefetch context dict into a readable system prompt block."""
parts = [] parts = []
@ -505,14 +585,8 @@ class HonchoMemoryProvider(MemoryProvider):
if self._cron_skipped: if self._cron_skipped:
return "" return ""
if not self._manager or not self._session_key: if not self._manager or not self._session_key:
# tools-only mode without session yet still returns a minimal block if not self._config:
if self._recall_mode == "tools" and self._config: return ""
return (
"# Honcho Memory\n"
"Active (tools-only mode). Use honcho_profile, honcho_search, "
"honcho_reasoning, honcho_context, and honcho_conclude tools to access user memory."
)
return ""
# ----- B1: adapt text based on recall_mode ----- # ----- B1: adapt text based on recall_mode -----
if self._recall_mode == "context": if self._recall_mode == "context":
@ -563,6 +637,10 @@ class HonchoMemoryProvider(MemoryProvider):
if self._recall_mode == "tools": if self._recall_mode == "tools":
return "" return ""
if not self._session_ready():
self._start_session_init_background()
return ""
# B5: injection_frequency — if "first-turn" and past first turn, return empty. # B5: injection_frequency — if "first-turn" and past first turn, return empty.
# _turn_count is 1-indexed (first user message = 1), so > 1 means "past first". # _turn_count is 1-indexed (first user message = 1), so > 1 means "past first".
if self._injection_frequency == "first-turn" and self._turn_count > 1: if self._injection_frequency == "first-turn" and self._turn_count > 1:
@ -575,18 +653,17 @@ class HonchoMemoryProvider(MemoryProvider):
parts = [] parts = []
# ----- Layer 1: Base context (representation + card) ----- # ----- Layer 1: Base context (representation + card) -----
# On first call, fetch synchronously so turn 1 isn't empty. # First fetch is asynchronous: a slow Honcho backend must not block the
# After that, serve from cache and refresh in background on cadence. # first response. Serve empty context now and consume the background
# result on a later turn.
with self._base_context_lock: with self._base_context_lock:
if self._base_context_cache is None: if self._base_context_cache is None:
# First call — synchronous fetch self._base_context_cache = ""
self._last_context_turn = self._turn_count
try: try:
ctx = self._manager.get_prefetch_context(self._session_key) self._manager.prefetch_context(self._session_key, query or None)
self._base_context_cache = self._format_first_turn_context(ctx) if ctx else ""
self._last_context_turn = self._turn_count
except Exception as e: except Exception as e:
logger.debug("Honcho base context fetch failed: %s", e) logger.debug("Honcho base context prefetch failed: %s", e)
self._base_context_cache = ""
base_context = self._base_context_cache base_context = self._base_context_cache
# Check if background context prefetch has a fresher result # Check if background context prefetch has a fresher result
@ -641,10 +718,11 @@ class HonchoMemoryProvider(MemoryProvider):
self._dialectic_empty_streak += 1 self._dialectic_empty_streak += 1
self._prefetch_thread_started_at = time.monotonic() self._prefetch_thread_started_at = time.monotonic()
self._prefetch_thread = threading.Thread( first_turn_thread = threading.Thread(
target=_run_first_turn, daemon=True, name="honcho-prefetch-first" target=_run_first_turn, daemon=True, name="honcho-prefetch-first"
) )
self._prefetch_thread.start() first_turn_thread.start()
self._prefetch_thread = first_turn_thread
self._prefetch_thread.join(timeout=_first_turn_timeout) self._prefetch_thread.join(timeout=_first_turn_timeout)
if self._prefetch_thread.is_alive(): if self._prefetch_thread.is_alive():
logger.debug( logger.debug(
@ -709,13 +787,14 @@ class HonchoMemoryProvider(MemoryProvider):
""" """
if self._cron_skipped: if self._cron_skipped:
return return
if not self._manager or not self._session_key or not query:
return
# B1: tools-only mode — no prefetch # B1: tools-only mode — no prefetch
if self._recall_mode == "tools": if self._recall_mode == "tools":
return return
if not self._session_ready() or not query:
self._start_session_init_background()
return
# Trivial prompts don't warrant either a context refresh or a dialectic call. # Trivial prompts don't warrant either a context refresh or a dialectic call.
if self._is_trivial_prompt(query): if self._is_trivial_prompt(query):
return return
@ -769,10 +848,11 @@ class HonchoMemoryProvider(MemoryProvider):
self._dialectic_empty_streak += 1 self._dialectic_empty_streak += 1
self._prefetch_thread_started_at = time.monotonic() self._prefetch_thread_started_at = time.monotonic()
self._prefetch_thread = threading.Thread( prefetch_thread = threading.Thread(
target=_run, daemon=True, name="honcho-prefetch" target=_run, daemon=True, name="honcho-prefetch"
) )
self._prefetch_thread.start() prefetch_thread.start()
self._prefetch_thread = prefetch_thread
# ----- Dialectic depth: multi-pass .chat() with cold/warm prompts ----- # ----- Dialectic depth: multi-pass .chat() with cold/warm prompts -----
@ -1126,7 +1206,10 @@ class HonchoMemoryProvider(MemoryProvider):
""" """
if self._cron_skipped: if self._cron_skipped:
return return
if not self._manager or not self._session_key: if self._recall_mode == "tools" and not self._session_ready():
return
if not self._session_ready():
self._start_session_init_background()
return return
msg_limit = self._config.message_max_chars if self._config else 25000 msg_limit = self._config.message_max_chars if self._config else 25000
@ -1169,7 +1252,10 @@ class HonchoMemoryProvider(MemoryProvider):
return return
if self._cron_skipped: if self._cron_skipped:
return return
if not self._manager or not self._session_key: if self._recall_mode == "tools" and not self._session_ready():
return
if not self._session_ready():
self._start_session_init_background()
return return
def _write(): def _write():
@ -1187,6 +1273,8 @@ class HonchoMemoryProvider(MemoryProvider):
return return
if not self._manager: if not self._manager:
return return
if not self._session_initialized and self._init_thread and self._init_thread.is_alive():
return
# Wait for pending sync # Wait for pending sync
if self._sync_thread and self._sync_thread.is_alive(): if self._sync_thread and self._sync_thread.is_alive():
self._sync_thread.join(timeout=10.0) self._sync_thread.join(timeout=10.0)
@ -1213,6 +1301,8 @@ class HonchoMemoryProvider(MemoryProvider):
# Port #1957: ensure session is initialized for tools-only mode # Port #1957: ensure session is initialized for tools-only mode
if not self._session_initialized: if not self._session_initialized:
if self._init_thread and self._init_thread.is_alive():
return tool_error("Honcho session is still initializing; try again shortly.")
if not self._ensure_session(): if not self._ensure_session():
return tool_error("Honcho session could not be initialized.") return tool_error("Honcho session could not be initialized.")
@ -1313,7 +1403,7 @@ class HonchoMemoryProvider(MemoryProvider):
if t and t.is_alive(): if t and t.is_alive():
t.join(timeout=5.0) t.join(timeout=5.0)
# Flush any remaining messages # Flush any remaining messages
if self._manager: if self._manager and not (self._init_thread and self._init_thread.is_alive() and not self._session_initialized):
try: try:
self._manager.flush_all() self._manager.flush_all()
except Exception: except Exception:

View File

@ -0,0 +1,357 @@
"""Regression tests for Honcho startup fail-open behavior."""
from __future__ import annotations
import json
import threading
import time
from types import SimpleNamespace
from plugins.memory.honcho import HonchoMemoryProvider
class _FakeHonchoConfig(SimpleNamespace):
def resolve_session_name(self, **kwargs):
return "test-session"
def _configured_hybrid_config() -> _FakeHonchoConfig:
return _FakeHonchoConfig(
enabled=True,
api_key=None,
base_url="http://127.0.0.1:8000",
recall_mode="hybrid",
init_on_session_start=False,
dialectic_depth=1,
dialectic_depth_levels=None,
reasoning_heuristic=True,
reasoning_level_cap="high",
context_tokens=None,
message_max_chars=25000,
session_strategy="per-directory",
)
def _configured_tools_config(*, init_on_session_start: bool = False) -> _FakeHonchoConfig:
cfg = _configured_hybrid_config()
cfg.recall_mode = "tools"
cfg.init_on_session_start = init_on_session_start
return cfg
def test_honcho_hybrid_initialize_returns_without_waiting_for_session_init(monkeypatch):
"""Slow Honcho session creation must not block agent startup."""
provider = HonchoMemoryProvider()
cfg = _configured_hybrid_config()
started = threading.Event()
release = threading.Event()
monkeypatch.setattr(
"plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
lambda: cfg,
)
def slow_session_init(self, cfg, session_id, **kwargs):
started.set()
release.wait(timeout=5)
self._session_initialized = True
monkeypatch.setattr(HonchoMemoryProvider, "_do_session_init", slow_session_init)
start = time.perf_counter()
provider.initialize("session-1", platform="cli")
elapsed = time.perf_counter() - start
try:
assert elapsed < 0.5
assert started.wait(timeout=1)
assert provider._session_key == "test-session"
finally:
release.set()
init_thread = getattr(provider, "_init_thread", None)
if init_thread:
init_thread.join(timeout=1)
def test_honcho_background_init_rechecks_state_after_lock_race():
"""Startup should not spawn/crash if init completes while waiting for lock."""
provider = HonchoMemoryProvider()
provider._config = _configured_hybrid_config()
provider._lazy_init_kwargs = {"platform": "cli"}
provider._lazy_init_session_id = "session-1"
class RacingLock:
def __enter__(self):
provider._session_initialized = True
provider._lazy_init_kwargs = None
return self
def __exit__(self, exc_type, exc, tb):
return False
provider._init_lock = RacingLock()
provider._start_session_init_background()
assert provider._init_thread is None
assert provider._session_initialized is True
def test_honcho_prefetch_returns_without_waiting_for_first_context_fetch():
"""First-turn context injection must fail open when Honcho is slow."""
provider = HonchoMemoryProvider()
cfg = _configured_hybrid_config()
cfg.timeout = 0.1
fetch_started = threading.Event()
class SlowManager:
def get_prefetch_context(self, session_key, user_message=None):
fetch_started.set()
time.sleep(5)
return {"representation": "late"}
def prefetch_context(self, session_key, user_message=None):
fetch_started.set()
def pop_context_result(self, session_key):
return {}
provider._config = cfg
provider._manager = SlowManager()
provider._session_key = "test-session"
provider._session_initialized = True
provider._turn_count = 1
start = time.perf_counter()
result = provider.prefetch("what do you know about me?")
elapsed = time.perf_counter() - start
assert result == ""
assert elapsed < 0.5
assert fetch_started.is_set()
def test_honcho_sync_turn_does_not_start_network_write_before_session_init():
"""Session-end sync must not create a blocking writer before init finishes."""
provider = HonchoMemoryProvider()
cfg = _configured_hybrid_config()
get_started = threading.Event()
background_started = threading.Event()
release_init = threading.Event()
class SlowManager:
def get_or_create(self, session_key):
get_started.set()
time.sleep(5)
return SimpleNamespace()
def _flush_session(self, session):
pass
provider._config = cfg
provider._manager = SlowManager()
provider._session_key = "test-session"
provider._session_initialized = False
provider._start_session_init_background = background_started.set
provider._init_thread = threading.Thread(
target=lambda: release_init.wait(timeout=5), daemon=True
)
provider._init_thread.start()
try:
provider.sync_turn("hello", "world")
assert provider._sync_thread is None
assert background_started.is_set()
assert not get_started.wait(timeout=0.1)
finally:
release_init.set()
provider._init_thread.join(timeout=1)
def test_honcho_sync_turn_waits_for_full_background_startup(monkeypatch):
"""Manager assignment alone is not readiness while background init continues."""
provider = HonchoMemoryProvider()
cfg = _configured_hybrid_config()
session_created = threading.Event()
migration_started = threading.Event()
release_migration = threading.Event()
get_calls = []
class StartupManager:
def __init__(self, *args, **kwargs):
pass
def get_or_create(self, session_key):
get_calls.append(session_key)
session_created.set()
return SimpleNamespace(messages=[])
def migrate_memory_files(self, session_key, mem_dir):
migration_started.set()
release_migration.wait(timeout=5)
def prefetch_context(self, session_key, user_message=None):
pass
def _flush_session(self, session):
pass
monkeypatch.setattr(
"plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
lambda: cfg,
)
monkeypatch.setattr("plugins.memory.honcho.client.get_honcho_client", lambda cfg: object())
monkeypatch.setattr("plugins.memory.honcho.session.HonchoSessionManager", StartupManager)
provider.initialize("session-1", platform="cli")
try:
assert session_created.wait(timeout=1)
assert migration_started.wait(timeout=1)
assert provider._manager is not None
assert provider._session_initialized is False
provider.sync_turn("hello", "world")
assert provider._sync_thread is None
assert get_calls == ["test-session"]
finally:
release_migration.set()
init_thread = getattr(provider, "_init_thread", None)
if init_thread:
init_thread.join(timeout=1)
if provider._prefetch_thread:
provider._prefetch_thread.join(timeout=1)
assert provider._session_initialized is True
def test_honcho_system_prompt_advertises_active_while_background_init_runs(monkeypatch):
"""Prompt metadata should not require a completed network session."""
provider = HonchoMemoryProvider()
cfg = _configured_hybrid_config()
release = threading.Event()
monkeypatch.setattr(
"plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
lambda: cfg,
)
def slow_session_init(self, cfg, session_id, **kwargs):
release.wait(timeout=5)
self._session_initialized = True
monkeypatch.setattr(HonchoMemoryProvider, "_do_session_init", slow_session_init)
provider.initialize("session-1", platform="cli")
try:
prompt = provider.system_prompt_block()
assert "Honcho Memory" in prompt
assert "hybrid mode" in prompt
finally:
release.set()
init_thread = getattr(provider, "_init_thread", None)
if init_thread:
init_thread.join(timeout=1)
def test_honcho_tools_eager_init_still_ready_on_return(monkeypatch):
"""tools + initOnSessionStart=true keeps its ready-on-return contract."""
provider = HonchoMemoryProvider()
cfg = _configured_tools_config(init_on_session_start=True)
monkeypatch.setattr(
"plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
lambda: cfg,
)
def fake_session_init(self, cfg, session_id, **kwargs):
self._manager = SimpleNamespace()
self._session_key = "test-session"
self._session_initialized = True
monkeypatch.setattr(HonchoMemoryProvider, "_do_session_init", fake_session_init)
provider.initialize("session-1", platform="cli")
assert provider._session_initialized is True
assert provider._manager is not None
assert provider._init_thread is None
def test_honcho_tools_eager_init_failure_does_not_leave_ready_manager(monkeypatch):
"""Failed eager tools startup must not leave hooks seeing a ready session."""
provider = HonchoMemoryProvider()
cfg = _configured_tools_config(init_on_session_start=True)
monkeypatch.setattr(
"plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
lambda: cfg,
)
def failing_session_init(self, cfg, session_id, **kwargs):
self._manager = SimpleNamespace()
self._session_key = "test-session"
raise RuntimeError("boom")
monkeypatch.setattr(HonchoMemoryProvider, "_do_session_init", failing_session_init)
provider.initialize("session-1", platform="cli")
assert provider._session_initialized is False
assert provider._manager is None
background_started = threading.Event()
provider._start_session_init_background = background_started.set
provider.sync_turn("hello", "world")
provider.on_memory_write("add", "user", "prefers safe Honcho startup")
assert provider._sync_thread is None
assert not background_started.is_set()
result = json.loads(provider.handle_tool_call("honcho_profile", {"peer": "user"}))
assert "could not be initialized" in result["error"]
assert provider._manager is None
def test_honcho_tools_lazy_hooks_do_not_prestart_background_init(monkeypatch):
"""tools lazy mode lets the first tool call own session initialization."""
provider = HonchoMemoryProvider()
cfg = _configured_tools_config(init_on_session_start=False)
monkeypatch.setattr(
"plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
lambda: cfg,
)
provider.initialize("session-1", platform="cli")
background_started = threading.Event()
provider._start_session_init_background = background_started.set
provider.prefetch("what do you know?")
provider.queue_prefetch("what do you know?")
provider.sync_turn("hello", "world")
provider.on_memory_write("add", "user", "prefers fail-open memory")
assert not background_started.is_set()
assert provider._session_initialized is False
class ToolManager:
def get_peer_card(self, session_key, peer="user"):
return ["ready"]
init_calls = []
def fake_session_init(self, cfg, session_id, **kwargs):
init_calls.append(session_id)
self._manager = ToolManager()
self._session_key = "test-session"
self._session_initialized = True
monkeypatch.setattr(HonchoMemoryProvider, "_do_session_init", fake_session_init)
result = json.loads(provider.handle_tool_call("honcho_profile", {"peer": "user"}))
assert result == {"result": ["ready"]}
assert init_calls == ["session-1"]
assert not background_started.is_set()