fix(honcho): resolve review blockers for merge
Address merge-blocking review feedback by removing unsafe signal handler overrides, wiring next-turn Honcho prefetch, restoring per-directory session defaults, and exposing all Honcho tools to the model surface. Also harden prefetch cache access with public thread-safe accessors and remove duplicate browser cleanup code. Made-with: Cursor
This commit is contained in:
parent
4c54c2709c
commit
047b118299
9 changed files with 162 additions and 69 deletions
|
|
@ -157,11 +157,11 @@ def cmd_setup(args) -> None:
|
||||||
cfg["recallMode"] = new_recall
|
cfg["recallMode"] = new_recall
|
||||||
|
|
||||||
# Session strategy
|
# Session strategy
|
||||||
current_strat = cfg.get("sessionStrategy", "per-session")
|
current_strat = cfg.get("sessionStrategy", "per-directory")
|
||||||
print(f"\n Session strategy options:")
|
print(f"\n Session strategy options:")
|
||||||
print(" per-session — new Honcho session each run, named by Hermes session ID (default)")
|
print(" per-directory — one session per working directory (default)")
|
||||||
print(" per-repo — one session per git repository (uses repo root name)")
|
print(" per-repo — one session per git repository (uses repo root name)")
|
||||||
print(" per-directory — one session per working directory")
|
print(" per-session — new Honcho session each run, named by Hermes session ID")
|
||||||
print(" global — single session across all directories")
|
print(" global — single session across all directories")
|
||||||
new_strat = _prompt("Session strategy", default=current_strat)
|
new_strat = _prompt("Session strategy", default=current_strat)
|
||||||
if new_strat in ("per-session", "per-repo", "per-directory", "global"):
|
if new_strat in ("per-session", "per-repo", "per-directory", "global"):
|
||||||
|
|
@ -199,6 +199,7 @@ def cmd_setup(args) -> None:
|
||||||
print(f" honcho_context — ask Honcho a question about you (LLM-synthesized)")
|
print(f" honcho_context — ask Honcho a question about you (LLM-synthesized)")
|
||||||
print(f" honcho_search — semantic search over your history (no LLM)")
|
print(f" honcho_search — semantic search over your history (no LLM)")
|
||||||
print(f" honcho_profile — your peer card, key facts (no LLM)")
|
print(f" honcho_profile — your peer card, key facts (no LLM)")
|
||||||
|
print(f" honcho_conclude — persist a user fact to Honcho memory (no LLM)")
|
||||||
print(f"\n Other commands:")
|
print(f"\n Other commands:")
|
||||||
print(f" hermes honcho status — show full config")
|
print(f" hermes honcho status — show full config")
|
||||||
print(f" hermes honcho mode — show or change memory mode")
|
print(f" hermes honcho mode — show or change memory mode")
|
||||||
|
|
@ -710,10 +711,11 @@ def cmd_migrate(args) -> None:
|
||||||
print(" honcho_context — ask Honcho a question, get a synthesized answer (LLM)")
|
print(" honcho_context — ask Honcho a question, get a synthesized answer (LLM)")
|
||||||
print(" honcho_search — semantic search over stored context (no LLM)")
|
print(" honcho_search — semantic search over stored context (no LLM)")
|
||||||
print(" honcho_profile — fast peer card snapshot (no LLM)")
|
print(" honcho_profile — fast peer card snapshot (no LLM)")
|
||||||
|
print(" honcho_conclude — write a conclusion/fact back to memory (no LLM)")
|
||||||
print()
|
print()
|
||||||
print(" Session naming")
|
print(" Session naming")
|
||||||
print(" OpenClaw: no persistent session concept — files are global.")
|
print(" OpenClaw: no persistent session concept — files are global.")
|
||||||
print(" Hermes: per-session by default — each run gets a new Honcho session")
|
print(" Hermes: per-directory by default — each project gets its own session")
|
||||||
print(" Map a custom name: hermes honcho map <session-name>")
|
print(" Map a custom name: hermes honcho map <session-name>")
|
||||||
|
|
||||||
# ── Step 6: Next steps ────────────────────────────────────────────────────
|
# ── Step 6: Next steps ────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ class HonchoClientConfig:
|
||||||
# "tools" — no pre-loaded context, rely on tool calls only
|
# "tools" — no pre-loaded context, rely on tool calls only
|
||||||
recall_mode: str = "hybrid"
|
recall_mode: str = "hybrid"
|
||||||
# Session resolution
|
# Session resolution
|
||||||
session_strategy: str = "per-session"
|
session_strategy: str = "per-directory"
|
||||||
session_peer_prefix: bool = False
|
session_peer_prefix: bool = False
|
||||||
sessions: dict[str, str] = field(default_factory=dict)
|
sessions: dict[str, str] = field(default_factory=dict)
|
||||||
# Raw global config for anything else consumers need
|
# Raw global config for anything else consumers need
|
||||||
|
|
@ -201,7 +201,7 @@ class HonchoClientConfig:
|
||||||
or raw.get("recallMode")
|
or raw.get("recallMode")
|
||||||
or "hybrid"
|
or "hybrid"
|
||||||
),
|
),
|
||||||
session_strategy=raw.get("sessionStrategy", "per-session"),
|
session_strategy=raw.get("sessionStrategy", "per-directory"),
|
||||||
session_peer_prefix=raw.get("sessionPeerPrefix", False),
|
session_peer_prefix=raw.get("sessionPeerPrefix", False),
|
||||||
sessions=raw.get("sessions", {}),
|
sessions=raw.get("sessions", {}),
|
||||||
raw=raw,
|
raw=raw,
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ class HonchoSessionManager:
|
||||||
# Prefetch caches: session_key → last result (consumed once per turn)
|
# Prefetch caches: session_key → last result (consumed once per turn)
|
||||||
self._context_cache: dict[str, dict] = {}
|
self._context_cache: dict[str, dict] = {}
|
||||||
self._dialectic_cache: dict[str, str] = {}
|
self._dialectic_cache: dict[str, str] = {}
|
||||||
|
self._prefetch_cache_lock = threading.Lock()
|
||||||
self._dialectic_reasoning_level: str = (
|
self._dialectic_reasoning_level: str = (
|
||||||
config.dialectic_reasoning_level if config else "low"
|
config.dialectic_reasoning_level if config else "low"
|
||||||
)
|
)
|
||||||
|
|
@ -496,18 +497,26 @@ class HonchoSessionManager:
|
||||||
def _run():
|
def _run():
|
||||||
result = self.dialectic_query(session_key, query)
|
result = self.dialectic_query(session_key, query)
|
||||||
if result:
|
if result:
|
||||||
self._dialectic_cache[session_key] = result
|
self.set_dialectic_result(session_key, result)
|
||||||
|
|
||||||
t = threading.Thread(target=_run, name="honcho-dialectic-prefetch", daemon=True)
|
t = threading.Thread(target=_run, name="honcho-dialectic-prefetch", daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
def set_dialectic_result(self, session_key: str, result: str) -> None:
|
||||||
|
"""Store a prefetched dialectic result in a thread-safe way."""
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
with self._prefetch_cache_lock:
|
||||||
|
self._dialectic_cache[session_key] = result
|
||||||
|
|
||||||
def pop_dialectic_result(self, session_key: str) -> str:
|
def pop_dialectic_result(self, session_key: str) -> str:
|
||||||
"""
|
"""
|
||||||
Return and clear the cached dialectic result for this session.
|
Return and clear the cached dialectic result for this session.
|
||||||
|
|
||||||
Returns empty string if no result is ready yet.
|
Returns empty string if no result is ready yet.
|
||||||
"""
|
"""
|
||||||
return self._dialectic_cache.pop(session_key, "")
|
with self._prefetch_cache_lock:
|
||||||
|
return self._dialectic_cache.pop(session_key, "")
|
||||||
|
|
||||||
def prefetch_context(self, session_key: str, user_message: str | None = None) -> None:
|
def prefetch_context(self, session_key: str, user_message: str | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -519,18 +528,26 @@ class HonchoSessionManager:
|
||||||
def _run():
|
def _run():
|
||||||
result = self.get_prefetch_context(session_key, user_message)
|
result = self.get_prefetch_context(session_key, user_message)
|
||||||
if result:
|
if result:
|
||||||
self._context_cache[session_key] = result
|
self.set_context_result(session_key, result)
|
||||||
|
|
||||||
t = threading.Thread(target=_run, name="honcho-context-prefetch", daemon=True)
|
t = threading.Thread(target=_run, name="honcho-context-prefetch", daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
def set_context_result(self, session_key: str, result: dict[str, str]) -> None:
|
||||||
|
"""Store a prefetched context result in a thread-safe way."""
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
with self._prefetch_cache_lock:
|
||||||
|
self._context_cache[session_key] = result
|
||||||
|
|
||||||
def pop_context_result(self, session_key: str) -> dict[str, str]:
|
def pop_context_result(self, session_key: str) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Return and clear the cached context result for this session.
|
Return and clear the cached context result for this session.
|
||||||
|
|
||||||
Returns empty dict if no result is ready yet (first turn).
|
Returns empty dict if no result is ready yet (first turn).
|
||||||
"""
|
"""
|
||||||
return self._context_cache.pop(session_key, {})
|
with self._prefetch_cache_lock:
|
||||||
|
return self._context_cache.pop(session_key, {})
|
||||||
|
|
||||||
def get_prefetch_context(self, session_key: str, user_message: str | None = None) -> dict[str, str]:
|
def get_prefetch_context(self, session_key: str, user_message: str | None = None) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
85
run_agent.py
85
run_agent.py
|
|
@ -20,6 +20,7 @@ Usage:
|
||||||
response = agent.run_conversation("Tell me about the latest Python updates")
|
response = agent.run_conversation("Tell me about the latest Python updates")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
import copy
|
import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
@ -31,6 +32,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
import weakref
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
@ -550,6 +552,7 @@ class AIAgent:
|
||||||
self._honcho = None # HonchoSessionManager | None
|
self._honcho = None # HonchoSessionManager | None
|
||||||
self._honcho_session_key = honcho_session_key
|
self._honcho_session_key = honcho_session_key
|
||||||
self._honcho_config = None # HonchoClientConfig | None
|
self._honcho_config = None # HonchoClientConfig | None
|
||||||
|
self._honcho_exit_hook_registered = False
|
||||||
if not skip_memory:
|
if not skip_memory:
|
||||||
try:
|
try:
|
||||||
if honcho_manager is not None:
|
if honcho_manager is not None:
|
||||||
|
|
@ -1427,28 +1430,46 @@ class AIAgent:
|
||||||
try:
|
try:
|
||||||
ctx = self._honcho.get_prefetch_context(self._honcho_session_key)
|
ctx = self._honcho.get_prefetch_context(self._honcho_session_key)
|
||||||
if ctx:
|
if ctx:
|
||||||
self._honcho._context_cache[self._honcho_session_key] = ctx
|
self._honcho.set_context_result(self._honcho_session_key, ctx)
|
||||||
logger.debug("Honcho context pre-warmed for first turn")
|
logger.debug("Honcho context pre-warmed for first turn")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Honcho context prefetch failed (non-fatal): %s", exc)
|
logger.debug("Honcho context prefetch failed (non-fatal): %s", exc)
|
||||||
|
|
||||||
import signal as _signal
|
self._register_honcho_exit_hook()
|
||||||
import threading as _threading
|
|
||||||
|
|
||||||
honcho_ref = self._honcho
|
def _register_honcho_exit_hook(self) -> None:
|
||||||
|
"""Register a process-exit flush hook without clobbering signal handlers."""
|
||||||
|
if self._honcho_exit_hook_registered or not self._honcho:
|
||||||
|
return
|
||||||
|
|
||||||
if _threading.current_thread() is _threading.main_thread():
|
honcho_ref = weakref.ref(self._honcho)
|
||||||
def _honcho_flush_handler(signum, frame):
|
|
||||||
try:
|
|
||||||
honcho_ref.flush_all()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if signum == _signal.SIGINT:
|
|
||||||
raise KeyboardInterrupt
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
_signal.signal(_signal.SIGTERM, _honcho_flush_handler)
|
def _flush_honcho_on_exit():
|
||||||
_signal.signal(_signal.SIGINT, _honcho_flush_handler)
|
manager = honcho_ref()
|
||||||
|
if manager is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
manager.flush_all()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("Honcho flush on exit failed (non-fatal): %s", exc)
|
||||||
|
|
||||||
|
atexit.register(_flush_honcho_on_exit)
|
||||||
|
self._honcho_exit_hook_registered = True
|
||||||
|
|
||||||
|
def _queue_honcho_prefetch(self, user_message: str) -> None:
|
||||||
|
"""Queue turn-end Honcho prefetch so the next turn can consume cached results."""
|
||||||
|
if not self._honcho or not self._honcho_session_key:
|
||||||
|
return
|
||||||
|
|
||||||
|
recall_mode = (self._honcho_config.recall_mode if self._honcho_config else "hybrid")
|
||||||
|
if recall_mode == "tools":
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._honcho.prefetch_context(self._honcho_session_key, user_message)
|
||||||
|
self._honcho.prefetch_dialectic(self._honcho_session_key, user_message or "What were we working on?")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("Honcho background prefetch failed (non-fatal): %s", exc)
|
||||||
|
|
||||||
def _honcho_prefetch(self, user_message: str) -> str:
|
def _honcho_prefetch(self, user_message: str) -> str:
|
||||||
"""Assemble the first-turn Honcho context from the pre-warmed cache."""
|
"""Assemble the first-turn Honcho context from the pre-warmed cache."""
|
||||||
|
|
@ -1472,6 +1493,10 @@ class AIAgent:
|
||||||
if ai_card:
|
if ai_card:
|
||||||
parts.append(ai_card)
|
parts.append(ai_card)
|
||||||
|
|
||||||
|
dialectic = self._honcho.pop_dialectic_result(self._honcho_session_key)
|
||||||
|
if dialectic:
|
||||||
|
parts.append(f"## Continuity synthesis\n{dialectic}")
|
||||||
|
|
||||||
if not parts:
|
if not parts:
|
||||||
return ""
|
return ""
|
||||||
header = (
|
header = (
|
||||||
|
|
@ -3379,15 +3404,23 @@ class AIAgent:
|
||||||
)
|
)
|
||||||
self._iters_since_skill = 0
|
self._iters_since_skill = 0
|
||||||
|
|
||||||
# Honcho: on the first turn only, read the pre-warmed context snapshot and
|
# Honcho prefetch consumption:
|
||||||
# bake it into the system prompt. We intentionally avoid per-turn refreshes
|
# - First turn: bake into cached system prompt (stable for the session).
|
||||||
# here because changing the system prompt would destroy provider prompt-cache
|
# - Later turns: inject as ephemeral system context for this API call only.
|
||||||
# reuse for the rest of the session.
|
#
|
||||||
|
# This keeps the persisted/cached prompt stable while still allowing
|
||||||
|
# turn N to consume background prefetch results from turn N-1.
|
||||||
self._honcho_context = ""
|
self._honcho_context = ""
|
||||||
|
self._honcho_turn_context = ""
|
||||||
_recall_mode = (self._honcho_config.recall_mode if self._honcho_config else "hybrid")
|
_recall_mode = (self._honcho_config.recall_mode if self._honcho_config else "hybrid")
|
||||||
if self._honcho and self._honcho_session_key and not conversation_history and _recall_mode != "tools":
|
if self._honcho and self._honcho_session_key and _recall_mode != "tools":
|
||||||
try:
|
try:
|
||||||
self._honcho_context = self._honcho_prefetch(user_message)
|
prefetched_context = self._honcho_prefetch(user_message)
|
||||||
|
if prefetched_context:
|
||||||
|
if not conversation_history:
|
||||||
|
self._honcho_context = prefetched_context
|
||||||
|
else:
|
||||||
|
self._honcho_turn_context = prefetched_context
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Honcho prefetch failed (non-fatal): %s", e)
|
logger.debug("Honcho prefetch failed (non-fatal): %s", e)
|
||||||
|
|
||||||
|
|
@ -3566,15 +3599,12 @@ class AIAgent:
|
||||||
api_messages.append(api_msg)
|
api_messages.append(api_msg)
|
||||||
|
|
||||||
# Build the final system message: cached prompt + ephemeral system prompt.
|
# Build the final system message: cached prompt + ephemeral system prompt.
|
||||||
# The ephemeral part is appended here (not baked into the cached prompt)
|
# Ephemeral additions are API-call-time only (not persisted to session DB).
|
||||||
# so it stays out of the session DB and logs.
|
|
||||||
# Note: Honcho context is baked into _cached_system_prompt on the first
|
|
||||||
# turn and stored in the session DB, so it does NOT need to be injected
|
|
||||||
# here. This keeps the system message identical across all turns in a
|
|
||||||
# session, maximizing Anthropic prompt cache hits.
|
|
||||||
effective_system = active_system_prompt or ""
|
effective_system = active_system_prompt or ""
|
||||||
if self.ephemeral_system_prompt:
|
if self.ephemeral_system_prompt:
|
||||||
effective_system = (effective_system + "\n\n" + self.ephemeral_system_prompt).strip()
|
effective_system = (effective_system + "\n\n" + self.ephemeral_system_prompt).strip()
|
||||||
|
if self._honcho_turn_context:
|
||||||
|
effective_system = (effective_system + "\n\n" + self._honcho_turn_context).strip()
|
||||||
if effective_system:
|
if effective_system:
|
||||||
api_messages = [{"role": "system", "content": effective_system}] + api_messages
|
api_messages = [{"role": "system", "content": effective_system}] + api_messages
|
||||||
|
|
||||||
|
|
@ -4656,6 +4686,7 @@ class AIAgent:
|
||||||
# Sync conversation to Honcho for user modeling
|
# Sync conversation to Honcho for user modeling
|
||||||
if final_response and not interrupted:
|
if final_response and not interrupted:
|
||||||
self._honcho_sync(original_user_message, final_response)
|
self._honcho_sync(original_user_message, final_response)
|
||||||
|
self._queue_honcho_prefetch(original_user_message)
|
||||||
|
|
||||||
# Build result with interrupt info if applicable
|
# Build result with interrupt info if applicable
|
||||||
result = {
|
result = {
|
||||||
|
|
|
||||||
|
|
@ -487,3 +487,22 @@ class TestNewConfigFieldDefaults:
|
||||||
cfg = HonchoClientConfig(memory_mode="hybrid", peer_memory_modes={"hermes": "local"})
|
cfg = HonchoClientConfig(memory_mode="hybrid", peer_memory_modes={"hermes": "local"})
|
||||||
assert cfg.peer_memory_mode("hermes") == "local"
|
assert cfg.peer_memory_mode("hermes") == "local"
|
||||||
assert cfg.peer_memory_mode("other") == "hybrid"
|
assert cfg.peer_memory_mode("other") == "hybrid"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrefetchCacheAccessors:
|
||||||
|
def test_set_and_pop_context_result(self):
|
||||||
|
mgr = _make_manager(write_frequency="turn")
|
||||||
|
payload = {"representation": "Known user", "card": "prefers concise replies"}
|
||||||
|
|
||||||
|
mgr.set_context_result("cli:test", payload)
|
||||||
|
|
||||||
|
assert mgr.pop_context_result("cli:test") == payload
|
||||||
|
assert mgr.pop_context_result("cli:test") == {}
|
||||||
|
|
||||||
|
def test_set_and_pop_dialectic_result(self):
|
||||||
|
mgr = _make_manager(write_frequency="turn")
|
||||||
|
|
||||||
|
mgr.set_dialectic_result("cli:test", "Resume with toolset cleanup")
|
||||||
|
|
||||||
|
assert mgr.pop_dialectic_result("cli:test") == "Resume with toolset cleanup"
|
||||||
|
assert mgr.pop_dialectic_result("cli:test") == ""
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class TestHonchoClientConfigDefaults:
|
||||||
assert config.environment == "production"
|
assert config.environment == "production"
|
||||||
assert config.enabled is False
|
assert config.enabled is False
|
||||||
assert config.save_messages is True
|
assert config.save_messages is True
|
||||||
assert config.session_strategy == "per-session"
|
assert config.session_strategy == "per-directory"
|
||||||
assert config.recall_mode == "hybrid"
|
assert config.recall_mode == "hybrid"
|
||||||
assert config.session_peer_prefix is False
|
assert config.session_peer_prefix is False
|
||||||
assert config.linked_hosts == []
|
assert config.linked_hosts == []
|
||||||
|
|
@ -140,7 +140,7 @@ class TestFromGlobalConfig:
|
||||||
config_file = tmp_path / "config.json"
|
config_file = tmp_path / "config.json"
|
||||||
config_file.write_text(json.dumps({"apiKey": "key"}))
|
config_file.write_text(json.dumps({"apiKey": "key"}))
|
||||||
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
config = HonchoClientConfig.from_global_config(config_path=config_file)
|
||||||
assert config.session_strategy == "per-session"
|
assert config.session_strategy == "per-directory"
|
||||||
|
|
||||||
def test_context_tokens_host_block_wins(self, tmp_path):
|
def test_context_tokens_host_block_wins(self, tmp_path):
|
||||||
"""Host block contextTokens should override root."""
|
"""Host block contextTokens should override root."""
|
||||||
|
|
|
||||||
|
|
@ -1192,17 +1192,15 @@ class TestSystemPromptStability:
|
||||||
|
|
||||||
assert "User prefers Python over JavaScript" in agent._cached_system_prompt
|
assert "User prefers Python over JavaScript" in agent._cached_system_prompt
|
||||||
|
|
||||||
def test_honcho_prefetch_skipped_on_continuing_session(self):
|
def test_honcho_prefetch_runs_on_continuing_session(self):
|
||||||
"""Honcho prefetch should not be called when conversation_history
|
"""Honcho prefetch is consumed on continuing sessions via ephemeral context."""
|
||||||
is non-empty (continuing session)."""
|
|
||||||
conversation_history = [
|
conversation_history = [
|
||||||
{"role": "user", "content": "hello"},
|
{"role": "user", "content": "hello"},
|
||||||
{"role": "assistant", "content": "hi there"},
|
{"role": "assistant", "content": "hi there"},
|
||||||
]
|
]
|
||||||
|
recall_mode = "hybrid"
|
||||||
# The guard: `not conversation_history` is False when history exists
|
should_prefetch = bool(conversation_history) and recall_mode != "tools"
|
||||||
should_prefetch = not conversation_history
|
assert should_prefetch is True
|
||||||
assert should_prefetch is False
|
|
||||||
|
|
||||||
def test_honcho_prefetch_runs_on_first_turn(self):
|
def test_honcho_prefetch_runs_on_first_turn(self):
|
||||||
"""Honcho prefetch should run when conversation_history is empty."""
|
"""Honcho prefetch should run when conversation_history is empty."""
|
||||||
|
|
@ -1273,4 +1271,49 @@ class TestHonchoActivation:
|
||||||
assert agent._honcho is manager
|
assert agent._honcho is manager
|
||||||
manager.get_or_create.assert_called_once_with("gateway-session")
|
manager.get_or_create.assert_called_once_with("gateway-session")
|
||||||
manager.get_prefetch_context.assert_called_once_with("gateway-session")
|
manager.get_prefetch_context.assert_called_once_with("gateway-session")
|
||||||
|
manager.set_context_result.assert_called_once_with(
|
||||||
|
"gateway-session",
|
||||||
|
{"representation": "Known user", "card": ""},
|
||||||
|
)
|
||||||
mock_client.assert_not_called()
|
mock_client.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHonchoPrefetchScheduling:
|
||||||
|
def test_honcho_prefetch_includes_cached_dialectic(self, agent):
|
||||||
|
agent._honcho = MagicMock()
|
||||||
|
agent._honcho_session_key = "session-key"
|
||||||
|
agent._honcho.pop_context_result.return_value = {}
|
||||||
|
agent._honcho.pop_dialectic_result.return_value = "Continue with the migration checklist."
|
||||||
|
|
||||||
|
context = agent._honcho_prefetch("what next?")
|
||||||
|
|
||||||
|
assert "Continuity synthesis" in context
|
||||||
|
assert "migration checklist" in context
|
||||||
|
|
||||||
|
def test_queue_honcho_prefetch_skips_tools_mode(self, agent):
|
||||||
|
agent._honcho = MagicMock()
|
||||||
|
agent._honcho_session_key = "session-key"
|
||||||
|
agent._honcho_config = HonchoClientConfig(
|
||||||
|
enabled=True,
|
||||||
|
api_key="honcho-key",
|
||||||
|
recall_mode="tools",
|
||||||
|
)
|
||||||
|
|
||||||
|
agent._queue_honcho_prefetch("what next?")
|
||||||
|
|
||||||
|
agent._honcho.prefetch_context.assert_not_called()
|
||||||
|
agent._honcho.prefetch_dialectic.assert_not_called()
|
||||||
|
|
||||||
|
def test_queue_honcho_prefetch_runs_when_context_enabled(self, agent):
|
||||||
|
agent._honcho = MagicMock()
|
||||||
|
agent._honcho_session_key = "session-key"
|
||||||
|
agent._honcho_config = HonchoClientConfig(
|
||||||
|
enabled=True,
|
||||||
|
api_key="honcho-key",
|
||||||
|
recall_mode="hybrid",
|
||||||
|
)
|
||||||
|
|
||||||
|
agent._queue_honcho_prefetch("what next?")
|
||||||
|
|
||||||
|
agent._honcho.prefetch_context.assert_called_once_with("session-key", "what next?")
|
||||||
|
agent._honcho.prefetch_dialectic.assert_called_once_with("session-key", "what next?")
|
||||||
|
|
|
||||||
|
|
@ -1640,25 +1640,6 @@ def _cleanup_old_recordings(max_age_hours=72):
|
||||||
logger.debug("Recording cleanup error (non-critical): %s", e)
|
logger.debug("Recording cleanup error (non-critical): %s", e)
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_old_recordings(max_age_hours=72):
|
|
||||||
"""Remove browser recordings older than max_age_hours to prevent disk bloat."""
|
|
||||||
import time
|
|
||||||
try:
|
|
||||||
hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
|
|
||||||
recordings_dir = hermes_home / "browser_recordings"
|
|
||||||
if not recordings_dir.exists():
|
|
||||||
return
|
|
||||||
cutoff = time.time() - (max_age_hours * 3600)
|
|
||||||
for f in recordings_dir.glob("session_*.webm"):
|
|
||||||
try:
|
|
||||||
if f.stat().st_mtime < cutoff:
|
|
||||||
f.unlink()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Cleanup and Management Functions
|
# Cleanup and Management Functions
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -1764,7 +1745,7 @@ def cleanup_browser(task_id: Optional[str] = None) -> None:
|
||||||
pid_file = os.path.join(socket_dir, f"{session_name}.pid")
|
pid_file = os.path.join(socket_dir, f"{session_name}.pid")
|
||||||
if os.path.isfile(pid_file):
|
if os.path.isfile(pid_file):
|
||||||
try:
|
try:
|
||||||
daemon_pid = int(open(pid_file).read().strip())
|
daemon_pid = int(Path(pid_file).read_text().strip())
|
||||||
os.kill(daemon_pid, signal.SIGTERM)
|
os.kill(daemon_pid, signal.SIGTERM)
|
||||||
logger.debug("Killed daemon pid %s for %s", daemon_pid, session_name)
|
logger.debug("Killed daemon pid %s for %s", daemon_pid, session_name)
|
||||||
except (ProcessLookupError, ValueError, PermissionError, OSError):
|
except (ProcessLookupError, ValueError, PermissionError, OSError):
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ _HERMES_CORE_TOOLS = [
|
||||||
"schedule_cronjob", "list_cronjobs", "remove_cronjob",
|
"schedule_cronjob", "list_cronjobs", "remove_cronjob",
|
||||||
# Cross-platform messaging (gated on gateway running via check_fn)
|
# Cross-platform messaging (gated on gateway running via check_fn)
|
||||||
"send_message",
|
"send_message",
|
||||||
# Honcho user context (gated on honcho being active via check_fn)
|
# Honcho memory tools (gated on honcho being active via check_fn)
|
||||||
"honcho_context",
|
"honcho_context", "honcho_profile", "honcho_search", "honcho_conclude",
|
||||||
# Home Assistant smart home control (gated on HASS_TOKEN via check_fn)
|
# Home Assistant smart home control (gated on HASS_TOKEN via check_fn)
|
||||||
"ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service",
|
"ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service",
|
||||||
]
|
]
|
||||||
|
|
@ -192,7 +192,7 @@ TOOLSETS = {
|
||||||
|
|
||||||
"honcho": {
|
"honcho": {
|
||||||
"description": "Honcho AI-native memory for persistent cross-session user modeling",
|
"description": "Honcho AI-native memory for persistent cross-session user modeling",
|
||||||
"tools": ["honcho_context"],
|
"tools": ["honcho_context", "honcho_profile", "honcho_search", "honcho_conclude"],
|
||||||
"includes": []
|
"includes": []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue