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
70 changes: 53 additions & 17 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15151,7 +15151,50 @@ async def _run_process_watcher(self, watcher: dict) -> None:
("compression", "target_ratio"),
("compression", "protect_last_n"),
("agent", "disabled_toolsets"),
("memory", "provider"),
)
_HONCHO_CACHE_BUSTING_KEYS: tuple[str, ...] = (
"honcho.peer_name",
"honcho.ai_peer",
"honcho.pin_peer_name",
"honcho.runtime_peer_prefix",
"honcho.user_peer_aliases",
)
_HONCHO_CACHE_BUSTING_CONFIG_CACHE: Dict[tuple[str, int, int], Dict[str, Any]] = {}

@classmethod
def _empty_honcho_cache_busting_config(cls) -> Dict[str, Any]:
return {key: None for key in cls._HONCHO_CACHE_BUSTING_KEYS}

@classmethod
def _extract_honcho_cache_busting_config(cls) -> Dict[str, Any]:
"""Read honcho.json-backed cache keys, memoized by config mtime."""
try:
from plugins.memory.honcho.client import HonchoClientConfig, resolve_config_path

config_path = resolve_config_path()
cache_key = None
if config_path.exists():
stat = config_path.stat()
cache_key = (str(config_path), stat.st_mtime_ns, stat.st_size)
cached = cls._HONCHO_CACHE_BUSTING_CONFIG_CACHE.get(cache_key)
if cached is not None:
return dict(cached)

hcfg = HonchoClientConfig.from_global_config(config_path=config_path)
aliases = hcfg.user_peer_aliases or {}
out = {
"honcho.peer_name": hcfg.peer_name,
"honcho.ai_peer": hcfg.ai_peer,
"honcho.pin_peer_name": bool(hcfg.pin_peer_name),
"honcho.runtime_peer_prefix": hcfg.runtime_peer_prefix or "",
"honcho.user_peer_aliases": sorted(aliases.items()) if isinstance(aliases, dict) else [],
}
if cache_key is not None:
cls._HONCHO_CACHE_BUSTING_CONFIG_CACHE[cache_key] = dict(out)
return out
except Exception:
return cls._empty_honcho_cache_busting_config()

@classmethod
def _extract_cache_busting_config(cls, user_config: dict | None) -> dict:
Expand Down Expand Up @@ -15182,27 +15225,20 @@ def _extract_cache_busting_config(cls, user_config: dict | None) -> dict:
except Exception:
out["tools.registry_generation"] = None

memory_cfg = cfg.get("memory")
memory_provider = memory_cfg.get("provider") if isinstance(memory_cfg, dict) else None
if str(memory_provider or "").lower() != "honcho":
out.update(cls._empty_honcho_cache_busting_config())
return out

# Honcho identity-mapping keys live in honcho.json, not user_config.
# HonchoSessionManager freezes the resolved peer_name / ai_peer /
# pin / aliases / prefix at construction; without busting here,
# mid-flight honcho.json edits go unread until the next unrelated
# cache eviction.
try:
from plugins.memory.honcho.client import HonchoClientConfig

hcfg = HonchoClientConfig.from_global_config()
out["honcho.peer_name"] = hcfg.peer_name
out["honcho.ai_peer"] = hcfg.ai_peer
out["honcho.pin_peer_name"] = bool(hcfg.pin_peer_name)
out["honcho.runtime_peer_prefix"] = hcfg.runtime_peer_prefix or ""
aliases = hcfg.user_peer_aliases or {}
out["honcho.user_peer_aliases"] = sorted(aliases.items()) if isinstance(aliases, dict) else []
except Exception:
out["honcho.peer_name"] = None
out["honcho.ai_peer"] = None
out["honcho.pin_peer_name"] = None
out["honcho.runtime_peer_prefix"] = None
out["honcho.user_peer_aliases"] = None
# cache eviction. Only read that file when Honcho is the active
# memory provider; otherwise this path runs for every gateway message
# even though the parsed values cannot affect the cached agent.
out.update(cls._extract_honcho_cache_busting_config())

return out

Expand Down
64 changes: 64 additions & 0 deletions tests/gateway/test_agent_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
- Preserves frozen system prompt across turns
"""

import hashlib
import json
import os
import threading
from types import SimpleNamespace
from unittest.mock import MagicMock, patch


Expand Down Expand Up @@ -276,6 +280,66 @@ def test_extract_includes_live_tool_registry_generation(self, monkeypatch):

assert out["tools.registry_generation"] == 12345

def test_non_honcho_provider_does_not_read_honcho_config(self, monkeypatch):
from gateway.run import GatewayRunner
from plugins.memory.honcho.client import HonchoClientConfig

GatewayRunner._HONCHO_CACHE_BUSTING_CONFIG_CACHE.clear()
monkeypatch.setattr(
HonchoClientConfig,
"from_global_config",
classmethod(lambda cls, **kwargs: pytest.fail("Honcho config should not be loaded")),
)

out = GatewayRunner._extract_cache_busting_config(
{"memory": {"provider": "built_in"}}
)

assert out["memory.provider"] == "built_in"
for key in GatewayRunner._HONCHO_CACHE_BUSTING_KEYS:
assert out[key] is None

def test_honcho_cache_busting_config_is_memoized_by_mtime(self, monkeypatch, tmp_path):
from gateway.run import GatewayRunner
from plugins.memory.honcho import client as honcho_client

GatewayRunner._HONCHO_CACHE_BUSTING_CONFIG_CACHE.clear()
config_path = tmp_path / "honcho.json"
config_path.write_text("{}", encoding="utf-8")
calls = []

def fake_from_global_config(cls, **kwargs):
calls.append(kwargs.get("config_path"))
return SimpleNamespace(
peer_name=f"user-{len(calls)}",
ai_peer="assistant",
pin_peer_name=True,
runtime_peer_prefix="rt",
user_peer_aliases={"u": "User"},
)

monkeypatch.setattr(honcho_client, "resolve_config_path", lambda: config_path)
monkeypatch.setattr(
honcho_client.HonchoClientConfig,
"from_global_config",
classmethod(fake_from_global_config),
)

cfg = {"memory": {"provider": "honcho"}}
first = GatewayRunner._extract_cache_busting_config(cfg)
second = GatewayRunner._extract_cache_busting_config(cfg)

assert first["honcho.peer_name"] == "user-1"
assert second["honcho.peer_name"] == "user-1"
assert calls == [config_path]

stat = config_path.stat()
os.utime(config_path, ns=(stat.st_atime_ns + 1, stat.st_mtime_ns + 1))
third = GatewayRunner._extract_cache_busting_config(cfg)

assert third["honcho.peer_name"] == "user-2"
assert calls == [config_path, config_path]

def test_full_round_trip_busts_cache_on_real_edit(self):
"""End-to-end: simulate a config edit on main and verify the
extracted cache_keys change produces a new signature."""
Expand Down
21 changes: 13 additions & 8 deletions tests/honcho_plugin/test_pin_peer_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,11 +744,13 @@ def test_cache_busting_signature_reflects_pin_peer_name(self, tmp_path, monkeypa
cfg_path = tmp_path / "honcho.json"
monkeypatch.setenv("HERMES_HOME", str(tmp_path))

active_honcho = {"memory": {"provider": "honcho"}}

cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": True}))
sig_pinned = GatewayRunner._extract_cache_busting_config({})
sig_pinned = GatewayRunner._extract_cache_busting_config(active_honcho)

cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": False}))
sig_unpinned = GatewayRunner._extract_cache_busting_config({})
sig_unpinned = GatewayRunner._extract_cache_busting_config(active_honcho)

assert sig_pinned["honcho.pin_peer_name"] != sig_unpinned["honcho.pin_peer_name"]

Expand All @@ -757,16 +759,17 @@ def test_cache_busting_signature_reflects_user_peer_aliases(self, tmp_path, monk

cfg_path = tmp_path / "honcho.json"
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
active_honcho = {"memory": {"provider": "honcho"}}

cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor"}))
sig_no_aliases = GatewayRunner._extract_cache_busting_config({})
sig_no_aliases = GatewayRunner._extract_cache_busting_config(active_honcho)

cfg_path.write_text(json.dumps({
"apiKey": "k",
"peerName": "Igor",
"userPeerAliases": {"86701400": "Igor"},
}))
sig_with_aliases = GatewayRunner._extract_cache_busting_config({})
sig_with_aliases = GatewayRunner._extract_cache_busting_config(active_honcho)

assert sig_no_aliases["honcho.user_peer_aliases"] != sig_with_aliases["honcho.user_peer_aliases"]

Expand All @@ -775,16 +778,17 @@ def test_cache_busting_signature_reflects_runtime_peer_prefix(self, tmp_path, mo

cfg_path = tmp_path / "honcho.json"
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
active_honcho = {"memory": {"provider": "honcho"}}

cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor"}))
sig_no_prefix = GatewayRunner._extract_cache_busting_config({})
sig_no_prefix = GatewayRunner._extract_cache_busting_config(active_honcho)

cfg_path.write_text(json.dumps({
"apiKey": "k",
"peerName": "Igor",
"runtimePeerPrefix": "telegram_",
}))
sig_with_prefix = GatewayRunner._extract_cache_busting_config({})
sig_with_prefix = GatewayRunner._extract_cache_busting_config(active_honcho)

assert sig_no_prefix["honcho.runtime_peer_prefix"] != sig_with_prefix["honcho.runtime_peer_prefix"]

Expand All @@ -799,20 +803,21 @@ def test_cache_busting_signature_reflects_ai_peer(self, tmp_path, monkeypatch):

cfg_path = tmp_path / "honcho.json"
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
active_honcho = {"memory": {"provider": "honcho"}}

cfg_path.write_text(json.dumps({
"apiKey": "k",
"peerName": "Igor",
"aiPeer": "hermes",
}))
sig_before = GatewayRunner._extract_cache_busting_config({})
sig_before = GatewayRunner._extract_cache_busting_config(active_honcho)

cfg_path.write_text(json.dumps({
"apiKey": "k",
"peerName": "Igor",
"aiPeer": "hermetika",
}))
sig_after = GatewayRunner._extract_cache_busting_config({})
sig_after = GatewayRunner._extract_cache_busting_config(active_honcho)

assert sig_before["honcho.ai_peer"] != sig_after["honcho.ai_peer"]

Expand Down
5 changes: 1 addition & 4 deletions tests/tools/test_kanban_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,16 +1329,13 @@ def test_worker_complete_rejects_stale_run_id(worker_env, monkeypatch):
# detect_crashed_workers now gates each running task behind a
# launch-window grace period (c002668ff) so a freshly-spawned worker
# whose PID isn't yet visible on /proc isn't reclaimed. The fixture
# creates the task moments before this assertion, so the grace
# period (default 30s) would skip the liveness check. Zero it out
# rows below intentionally use impossible dead PIDs, so disable grace
# for this test — we WANT immediate reclamation here.
monkeypatch.setenv("HERMES_KANBAN_CRASH_GRACE_SECONDS", "0")

conn = kb.connect()
try:
run1 = kb.latest_run(conn, worker_env)
kb._set_worker_pid(conn, worker_env, 98765)
monkeypatch.setenv("HERMES_KANBAN_CRASH_GRACE_SECONDS", "0")
monkeypatch.setattr(_kb, "_pid_alive", lambda pid: False)
assert kb.detect_crashed_workers(conn) == [worker_env]

Expand Down
Loading