fix(honcho): preserve deferred routing invariants

Thread logical session cwd through deferred Desktop/TUI builds, normalize absent cwd during construction, and share title provenance constants between SessionDB and Honcho.

(cherry picked from commit 2693f4f27c776ac819d92c9b52e8a03ad2a985d8)
This commit is contained in:
outpoints
2026-08-28 16:05:23 -07:00
committed by Teknium
parent 5237cab756
commit 44ba32a565
8 changed files with 137 additions and 8 deletions
+41 -3
View File
@@ -8,6 +8,8 @@ import json
import os
from unittest.mock import MagicMock, patch
import pytest
from agent.memory_provider import MemoryProvider
from agent.memory_manager import MemoryManager
@@ -108,13 +110,13 @@ class TestMemoryManagerUserIdThreading:
session_db.get_session_title_source.return_value = "llm"
with patch(
"run_agent.get_tool_definitions",
"model_tools.get_tool_definitions",
return_value=[],
), patch(
"run_agent.check_toolset_requirements",
"model_tools.check_toolset_requirements",
return_value={},
), patch(
"run_agent.OpenAI",
"agent.process_bootstrap.OpenAI",
), patch(
"hermes_cli.config.load_config_readonly",
return_value={"memory": {"provider": "recording"}},
@@ -143,6 +145,42 @@ class TestMemoryManagerUserIdThreading:
assert provider._init_kwargs["cwd"] == str(tmp_path)
agent.close()
@pytest.mark.parametrize("cwd", [None, ""])
def test_cwdless_agent_still_initializes_memory_provider(self, cwd):
from run_agent import AIAgent
provider = RecordingProvider()
with patch(
"model_tools.get_tool_definitions",
return_value=[],
), patch(
"model_tools.check_toolset_requirements",
return_value={},
), patch(
"agent.process_bootstrap.OpenAI",
), patch(
"hermes_cli.config.load_config_readonly",
return_value={"memory": {"provider": "recording"}},
), patch(
"plugins.memory.load_memory_provider",
return_value=provider,
):
agent = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
platform="cli",
session_id="session-without-cwd",
cwd=cwd,
)
assert agent.session_cwd is None
assert provider._init_session_id == "session-without-cwd"
assert "cwd" not in provider._init_kwargs
agent.close()
# ---------------------------------------------------------------------------
# Mem0 provider user_id tests
+16
View File
@@ -12,6 +12,9 @@ from hermes_cli.profiles import _get_default_hermes_home
import pytest
import hermes_state_common
from hermes_state import SessionDB
from plugins.memory.honcho import client as honcho_client
from plugins.memory.honcho.client import (
HonchoClientConfig,
get_honcho_client,
@@ -24,6 +27,19 @@ from plugins.memory.honcho.client import (
class TestHonchoClientConfigDefaults:
def test_automatic_title_sources_share_state_store_vocabulary(self):
automatic_sources = frozenset(
{
hermes_state_common.TITLE_SOURCE_DERIVED,
hermes_state_common.TITLE_SOURCE_LLM,
}
)
assert honcho_client._AUTOMATIC_SESSION_TITLE_SOURCES == automatic_sources
assert SessionDB.TITLE_SOURCE_DERIVED == hermes_state_common.TITLE_SOURCE_DERIVED
assert SessionDB.TITLE_SOURCE_LLM == hermes_state_common.TITLE_SOURCE_LLM
assert SessionDB.TITLE_SOURCE_USER == hermes_state_common.TITLE_SOURCE_USER
def test_default_values(self):
config = HonchoClientConfig()
assert config.host == "hermes"
@@ -0,0 +1,59 @@
"""Regression coverage for logical cwd propagation in deferred TUI/Desktop builds."""
import threading
import uuid
from types import SimpleNamespace
from tui_gateway import server
def test_deferred_agent_build_threads_session_cwd(monkeypatch, tmp_path):
"""A cold Desktop build must not fall back to the serve process cwd."""
workspace = tmp_path / "workspace"
workspace.mkdir()
captured = {}
built = threading.Event()
ready = threading.Event()
sid = f"cwd-build-{uuid.uuid4().hex[:8]}"
session = {
"agent_ready": ready,
"session_key": f"cwd-key-{uuid.uuid4().hex[:8]}",
"cwd": str(workspace),
}
def fake_set_session_context(key, cwd=None):
captured["context_key"] = key
captured["context_cwd"] = cwd
return []
def fake_make_agent(*args, **kwargs):
captured["agent_cwd"] = kwargs.get("cwd_override")
built.set()
return SimpleNamespace(model="test", session_id=session["session_key"])
monkeypatch.setattr(server, "_set_session_context", fake_set_session_context)
monkeypatch.setattr(server, "_clear_session_context", lambda _tokens: None)
monkeypatch.setattr(server, "_make_agent", fake_make_agent)
monkeypatch.setattr(
"tui_gateway.entry.ensure_mcp_discovery_started", lambda: None
)
monkeypatch.setattr(server, "_wire_callbacks", lambda _sid: None)
monkeypatch.setattr(server, "_config_model_target", lambda: ("", ""))
monkeypatch.setattr(server, "_start_notification_poller", lambda *a, **k: None)
monkeypatch.setattr(server, "_schedule_mcp_late_refresh", lambda *a, **k: None)
monkeypatch.setattr(server, "_emit", lambda *a, **k: None)
monkeypatch.setattr(server, "_notify_session_boundary", lambda *a, **k: None)
server._sessions[sid] = session
try:
server._start_agent_build(sid, session)
assert built.wait(timeout=15), "agent build thread never called _make_agent"
assert ready.wait(timeout=5), "agent_ready never set after build"
finally:
server._sessions.pop(sid, None)
from tools.approval import unregister_gateway_notify
unregister_gateway_notify(session["session_key"])
assert captured["context_cwd"] == str(workspace)
assert captured["agent_cwd"] == str(workspace)
+3 -1
View File
@@ -19682,7 +19682,9 @@ def test_start_agent_build_passes_session_model_override(
captured.update(kwargs)
return types.SimpleNamespace(model="claude-sonnet-4.6")
monkeypatch.setattr(server, "_set_session_context", lambda target: [])
monkeypatch.setattr(
server, "_set_session_context", lambda target, cwd=None: []
)
monkeypatch.setattr(server, "_clear_session_context", lambda tokens: None)
monkeypatch.setattr(server, "_make_agent", fake_make_agent)
monkeypatch.setattr(server, "_SlashWorker", FakeWorker)