Merge PR #736: feat(honcho): async writes, memory modes, session title integration, setup CLI

Authored by erosika. Builds on #38 and #243.

Adds async write support, configurable memory modes, context prefetch pipeline,
4 new Honcho tools (honcho_context, honcho_profile, honcho_search, honcho_conclude),
full 'hermes honcho' CLI, session strategies, AI peer identity, recallMode A/B,
gateway lifecycle management, and comprehensive docs.

Cherry-picks fixes from PRs #831/#832 (adavyas).

Co-authored-by: erosika <erosika@users.noreply.github.com>
Co-authored-by: adavyas <adavyas@users.noreply.github.com>
This commit is contained in:
Teknium 2026-03-12 19:05:11 -07:00 committed by GitHub
commit 475dd58a8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 4688 additions and 355 deletions

View file

@ -250,6 +250,12 @@ class GatewayRunner:
# 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
@ -266,6 +272,61 @@ class GatewayRunner:
# 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()
if not hcfg.enabled or not hcfg.api_key:
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.
@ -324,6 +385,12 @@ class GatewayRunner:
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)
@ -634,6 +701,7 @@ class GatewayRunner:
)
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)
@ -656,8 +724,9 @@ class GatewayRunner:
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
@ -1503,6 +1572,8 @@ class GatewayRunner:
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)
@ -2435,6 +2506,8 @@ class GatewayRunner:
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]
@ -3274,6 +3347,7 @@ class GatewayRunner:
}
pr = self._provider_routing
honcho_manager, honcho_config = self._get_or_create_gateway_honcho(session_key)
agent = AIAgent(
model=model,
**runtime_kwargs,
@ -3295,6 +3369,8 @@ class GatewayRunner:
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,
)