Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1385,7 +1385,7 @@ def _init_agent(self) -> bool:
platform="cli",
session_db=self._session_db,
clarify_callback=self._clarify_callback,
honcho_session_key=self.session_id,
honcho_session_key=None, # resolved by run_agent via config sessions map / title
fallback_model=self._fallback_model,
)
# Apply any pending title now that the session exists in the DB
Expand Down Expand Up @@ -2466,6 +2466,26 @@ def process_command(self, command: str) -> bool:
try:
if self._session_db.set_session_title(self.session_id, new_title):
_cprint(f" Session title set: {new_title}")
# Re-map Honcho session key to new title
if self.agent and getattr(self.agent, '_honcho', None):
try:
hcfg = self.agent._honcho_config
new_key = (
hcfg.resolve_session_name(
session_title=new_title,
session_id=self.agent.session_id,
)
if hcfg else new_title
)
if new_key and new_key != self.agent._honcho_session_key:
old_key = self.agent._honcho_session_key
self.agent._honcho.get_or_create(new_key)
self.agent._honcho_session_key = new_key
from tools.honcho_tools import set_session_context
set_session_context(self.agent._honcho, new_key)
_cprint(f" Honcho session: {old_key} → {new_key}")
except Exception:
pass
else:
_cprint(" Session not found in database.")
except ValueError as e:
Expand Down Expand Up @@ -2738,6 +2758,12 @@ def _manual_compress(self):
f" ✅ Compressed: {original_count} → {new_count} messages "
f"(~{approx_tokens:,} → ~{new_tokens:,} tokens)"
)
# Flush Honcho async queue so queued messages land before context resets
if self.agent and getattr(self.agent, '_honcho', None):
try:
self.agent._honcho.flush_all()
except Exception:
pass
except Exception as e:
print(f" ❌ Compression failed: {e}")

Expand Down Expand Up @@ -3153,7 +3179,8 @@ def run_agent():
if response and pending_message:
response = response + "\n\n---\n_[Interrupted - processing new message]_"

if response:
response_previewed = result.get("response_previewed", False) if result else False
if response and not response_previewed:
w = shutil.get_terminal_size().columns
label = " ⚕ Hermes "
fill = w - 2 - len(label) # 2 for ╭ and ╮
Expand Down Expand Up @@ -3998,6 +4025,12 @@ def process_loop():
# Unregister terminal_tool callbacks to avoid dangling references
set_sudo_password_callback(None)
set_approval_callback(None)
# Flush + shut down Honcho async writer (drains queue before exit)
if self.agent and getattr(self.agent, '_honcho', None):
try:
self.agent._honcho.shutdown()
except Exception:
pass
# Close session in SQLite
if hasattr(self, '_session_db') and self._session_db and self.agent:
try:
Expand Down
80 changes: 79 additions & 1 deletion gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ def __init__(self, config: Optional[GatewayConfig] = None):
# Track pending exec approvals per session
# Key: session_key, Value: {"command": str, "pattern_key": str}
self._pending_approvals: Dict[str, Dict[str, str]] = {}

# Persistent Honcho managers keyed by gateway session key.
# This preserves write_frequency="session" semantics across short-lived
# per-message AIAgent instances.
self._honcho_managers: Dict[str, Any] = {}
self._honcho_configs: Dict[str, Any] = {}

# Initialize session database for session_search tool support
self._session_db = None
Expand All @@ -236,6 +242,63 @@ def __init__(self, config: Optional[GatewayConfig] = None):
# Event hook system
from gateway.hooks import HookRegistry
self.hooks = HookRegistry()

def _get_or_create_gateway_honcho(self, session_key: str):
"""Return a persistent Honcho manager/config pair for this gateway session."""
if not hasattr(self, "_honcho_managers"):
self._honcho_managers = {}
if not hasattr(self, "_honcho_configs"):
self._honcho_configs = {}

if session_key in self._honcho_managers:
return self._honcho_managers[session_key], self._honcho_configs.get(session_key)

try:
from honcho_integration.client import HonchoClientConfig, get_honcho_client
from honcho_integration.session import HonchoSessionManager

hcfg = HonchoClientConfig.from_global_config()
ai_mode = hcfg.peer_memory_mode(hcfg.ai_peer)
user_mode = hcfg.peer_memory_mode(hcfg.peer_name or "user")
if not hcfg.enabled or not hcfg.api_key or (ai_mode == "local" and user_mode == "local"):
return None, hcfg

client = get_honcho_client(hcfg)
manager = HonchoSessionManager(
honcho=client,
config=hcfg,
context_tokens=hcfg.context_tokens,
)
self._honcho_managers[session_key] = manager
self._honcho_configs[session_key] = hcfg
return manager, hcfg
except Exception as e:
logger.debug("Gateway Honcho init failed for %s: %s", session_key, e)
return None, None

def _shutdown_gateway_honcho(self, session_key: str) -> None:
"""Flush and close the persistent Honcho manager for a gateway session."""
managers = getattr(self, "_honcho_managers", None)
configs = getattr(self, "_honcho_configs", None)
if managers is None or configs is None:
return

manager = managers.pop(session_key, None)
configs.pop(session_key, None)
if not manager:
return
try:
manager.shutdown()
except Exception as e:
logger.debug("Gateway Honcho shutdown failed for %s: %s", session_key, e)

def _shutdown_all_gateway_honcho(self) -> None:
"""Flush and close all persistent Honcho managers."""
managers = getattr(self, "_honcho_managers", None)
if not managers:
return
for session_key in list(managers.keys()):
self._shutdown_gateway_honcho(session_key)

def _flush_memories_for_session(self, old_session_id: str):
"""Prompt the agent to save memories/skills before context is lost.
Expand Down Expand Up @@ -288,6 +351,12 @@ def _flush_memories_for_session(self, old_session_id: str):
conversation_history=msgs,
)
logger.info("Pre-reset memory flush completed for session %s", old_session_id)
# Flush any queued Honcho writes before the session is dropped
if getattr(tmp_agent, '_honcho', None):
try:
tmp_agent._honcho.shutdown()
except Exception:
pass
except Exception as e:
logger.debug("Pre-reset memory flush failed for session %s: %s", old_session_id, e)

Expand Down Expand Up @@ -549,6 +618,7 @@ async def _session_expiry_watcher(self, interval: int = 300):
)
try:
await self._async_flush_memories(entry.session_id)
self._shutdown_gateway_honcho(key)
self.session_store._pre_flushed_sessions.add(entry.session_id)
except Exception as e:
logger.debug("Proactive memory flush failed for %s: %s", entry.session_id, e)
Expand All @@ -571,8 +641,9 @@ async def stop(self) -> None:
logger.info("✓ %s disconnected", platform.value)
except Exception as e:
logger.error("✗ %s disconnect error: %s", platform.value, e)

self.adapters.clear()
self._shutdown_all_gateway_honcho()
self._shutdown_event.set()

from gateway.status import remove_pid_file
Expand Down Expand Up @@ -1320,6 +1391,8 @@ async def _handle_reset_command(self, event: MessageEvent) -> str:
asyncio.create_task(self._async_flush_memories(old_entry.session_id))
except Exception as e:
logger.debug("Gateway memory flush on reset failed: %s", e)

self._shutdown_gateway_honcho(session_key)

# Reset the session
new_entry = self.session_store.reset_session(session_key)
Expand Down Expand Up @@ -1880,6 +1953,8 @@ async def _handle_resume_command(self, event: MessageEvent) -> str:
except Exception as e:
logger.debug("Memory flush on resume failed: %s", e)

self._shutdown_gateway_honcho(session_key)

# Clear any running agent for this session key
if session_key in self._running_agents:
del self._running_agents[session_key]
Expand Down Expand Up @@ -2674,6 +2749,7 @@ def run_sync():
}

pr = self._provider_routing
honcho_manager, honcho_config = self._get_or_create_gateway_honcho(session_key)
agent = AIAgent(
model=model,
**runtime_kwargs,
Expand All @@ -2695,6 +2771,8 @@ def run_sync():
step_callback=_step_callback_sync if _hooks_ref.loaded_hooks else None,
platform=platform_key,
honcho_session_key=session_key,
honcho_manager=honcho_manager,
honcho_config=honcho_config,
session_db=self._session_db,
fallback_model=self._fallback_model,
)
Expand Down
2 changes: 1 addition & 1 deletion hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ def ensure_hermes_home():
"description": "Honcho API key for AI-native persistent memory",
"prompt": "Honcho API key",
"url": "https://app.honcho.dev",
"tools": ["query_user_context"],
"tools": ["honcho_context"],
"password": True,
"category": "tool",
},
Expand Down
34 changes: 34 additions & 0 deletions hermes_cli/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,40 @@ def run_doctor(args):
else:
check_warn("No GITHUB_TOKEN", "(60 req/hr rate limit — set in ~/.hermes/.env for better rates)")

# =========================================================================
# Honcho memory
# =========================================================================
print()
print(color("◆ Honcho Memory", Colors.CYAN, Colors.BOLD))

try:
from honcho_integration.client import HonchoClientConfig, GLOBAL_CONFIG_PATH
hcfg = HonchoClientConfig.from_global_config()

if not GLOBAL_CONFIG_PATH.exists():
check_warn("Honcho config not found", f"run: hermes honcho setup")
elif not hcfg.enabled:
check_info("Honcho disabled (set enabled: true in ~/.honcho/config.json to activate)")
elif not hcfg.api_key:
check_fail("Honcho API key not set", "run: hermes honcho setup")
issues.append("No Honcho API key — run 'hermes honcho setup'")
else:
from honcho_integration.client import get_honcho_client, reset_honcho_client
reset_honcho_client()
try:
get_honcho_client(hcfg)
check_ok(
"Honcho connected",
f"workspace={hcfg.workspace_id} mode={hcfg.memory_mode} freq={hcfg.write_frequency}",
)
except Exception as _e:
check_fail("Honcho connection failed", str(_e))
issues.append(f"Honcho unreachable: {_e}")
except ImportError:
check_warn("honcho-ai not installed", "pip install honcho-ai")
except Exception as _e:
check_warn("Honcho check failed", str(_e))

# =========================================================================
# Summary
# =========================================================================
Expand Down
104 changes: 104 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@
hermes cron list # List cron jobs
hermes cron status # Check if cron scheduler is running
hermes doctor # Check configuration and dependencies
hermes honcho setup # Configure Honcho AI memory integration
hermes honcho status # Show Honcho config and connection status
hermes honcho sessions # List directory → session name mappings
hermes honcho map <name> # Map current directory to a session name
hermes honcho peer # Show peer names and dialectic settings
hermes honcho peer --user NAME # Set user peer name
hermes honcho peer --ai NAME # Set AI peer name
hermes honcho peer --reasoning LEVEL # Set dialectic reasoning level
hermes honcho mode # Show current memory mode
hermes honcho mode [hybrid|honcho|local] # Set memory mode
hermes honcho tokens # Show token budget settings
hermes honcho tokens --context N # Set session.context() token cap
hermes honcho tokens --dialectic N # Set dialectic result char cap
hermes honcho identity # Show AI peer identity representation
hermes honcho identity <file> # Seed AI peer identity from a file (SOUL.md etc.)
hermes honcho migrate # Step-by-step migration guide: OpenClaw native → Hermes + Honcho
hermes version # Show version
hermes update # Update to latest version
hermes uninstall # Uninstall Hermes Agent
Expand Down Expand Up @@ -2236,6 +2252,94 @@ def cmd_skills(args):

skills_parser.set_defaults(func=cmd_skills)

# =========================================================================
# honcho command
# =========================================================================
honcho_parser = subparsers.add_parser(
"honcho",
help="Manage Honcho AI memory integration",
description=(
"Honcho is a memory layer that persists across sessions.\n\n"
"Each conversation is stored as a peer interaction in a workspace. "
"Honcho builds a representation of the user over time — conclusions, "
"patterns, context — and surfaces the relevant slice at the start of "
"each turn so Hermes knows who you are without you having to repeat yourself.\n\n"
"Modes: hybrid (Honcho + local MEMORY.md), honcho (Honcho only), "
"local (MEMORY.md only). Write frequency is configurable so memory "
"writes never block the response."
),
formatter_class=__import__("argparse").RawDescriptionHelpFormatter,
)
honcho_subparsers = honcho_parser.add_subparsers(dest="honcho_command")

honcho_subparsers.add_parser("setup", help="Interactive setup wizard for Honcho integration")
honcho_subparsers.add_parser("status", help="Show current Honcho config and connection status")
honcho_subparsers.add_parser("sessions", help="List known Honcho session mappings")

honcho_map = honcho_subparsers.add_parser(
"map", help="Map current directory to a Honcho session name (no arg = list mappings)"
)
honcho_map.add_argument(
"session_name", nargs="?", default=None,
help="Session name to associate with this directory. Omit to list current mappings.",
)

honcho_peer = honcho_subparsers.add_parser(
"peer", help="Show or update peer names and dialectic reasoning level"
)
honcho_peer.add_argument("--user", metavar="NAME", help="Set user peer name")
honcho_peer.add_argument("--ai", metavar="NAME", help="Set AI peer name")
honcho_peer.add_argument(
"--reasoning",
metavar="LEVEL",
choices=("minimal", "low", "medium", "high", "max"),
help="Set default dialectic reasoning level (minimal/low/medium/high/max)",
)

honcho_mode = honcho_subparsers.add_parser(
"mode", help="Show or set memory mode (hybrid/honcho/local)"
)
honcho_mode.add_argument(
"mode", nargs="?", metavar="MODE",
choices=("hybrid", "honcho", "local"),
help="Memory mode to set (hybrid/honcho/local). Omit to show current.",
)

honcho_tokens = honcho_subparsers.add_parser(
"tokens", help="Show or set token budget for context and dialectic"
)
honcho_tokens.add_argument(
"--context", type=int, metavar="N",
help="Max tokens Honcho returns from session.context() per turn",
)
honcho_tokens.add_argument(
"--dialectic", type=int, metavar="N",
help="Max chars of dialectic result to inject into system prompt",
)

honcho_identity = honcho_subparsers.add_parser(
"identity", help="Seed or show the AI peer's Honcho identity representation"
)
honcho_identity.add_argument(
"file", nargs="?", default=None,
help="Path to file to seed from (e.g. SOUL.md). Omit to show usage.",
)
honcho_identity.add_argument(
"--show", action="store_true",
help="Show current AI peer representation from Honcho",
)

honcho_subparsers.add_parser(
"migrate",
help="Step-by-step migration guide from openclaw-honcho to Hermes Honcho",
)

def cmd_honcho(args):
from honcho_integration.cli import honcho_command
honcho_command(args)

honcho_parser.set_defaults(func=cmd_honcho)

# =========================================================================
# tools command
# =========================================================================
Expand Down
Loading
Loading