From ccf66b29ad35b9c6b9a102ef020027409e2d01b5 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:56:53 -0700 Subject: [PATCH] fix: reload.mcp rediscovers under every live session's profile scope `_do_full_reload` calls `shutdown_mcp_servers()` unscoped, which tears down every profile's servers, but `discover_mcp_tools()` ran only under the launch home. The all-sessions refresh then rebuilt a secondary-profile session's tool snapshot under its own scope against a registry whose overlay was deregistered and never rediscovered, so that session lost its MCP tools until its own reload (main at least left its stale snapshot intact). After the pool rebuild, rediscover once per distinct live `profile_home` under that profile's runtime scope before refreshing the sessions. --- .../test_mcp_reload_all_sessions.py | 32 ++++++++++++++----- tui_gateway/methods_tools.py | 11 +++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/tests/tui_gateway/test_mcp_reload_all_sessions.py b/tests/tui_gateway/test_mcp_reload_all_sessions.py index e89b4cf46f..03403a2cdf 100644 --- a/tests/tui_gateway/test_mcp_reload_all_sessions.py +++ b/tests/tui_gateway/test_mcp_reload_all_sessions.py @@ -12,6 +12,7 @@ from types import SimpleNamespace import pytest +import hermes_constants from tools import mcp_tool_agent as _mcp_agent from tools import mcp_tool_discovery as _mcp_discovery from tools import mcp_tool_lifecycle as _mcp_lifecycle @@ -19,10 +20,12 @@ import tui_gateway.server as srv @pytest.fixture() -def reload_env(monkeypatch): +def reload_env(monkeypatch, tmp_path): refreshed: list[str] = [] + discovered_homes: list[str] = [] monkeypatch.setattr(_mcp_lifecycle, "shutdown_mcp_servers", lambda: None) - monkeypatch.setattr(_mcp_discovery, "discover_mcp_tools", lambda: None) + monkeypatch.setattr(_mcp_discovery, "discover_mcp_tools", + lambda: discovered_homes.append(hermes_constants.hermes_home_key())) monkeypatch.setattr(_mcp_agent, "refresh_agent_mcp_tools", lambda agent, **_kw: refreshed.append(agent.name) or set()) monkeypatch.setattr(srv, "_compute_mcp_rev", lambda: "rev-a") @@ -31,25 +34,38 @@ def reload_env(monkeypatch): monkeypatch.setattr(srv, "_mcp_reload_gen", 0) monkeypatch.setattr(srv, "_mcp_reload_loaded_rev", "") - def _session(name): - return {"agent": SimpleNamespace(name=name), "history": [], "history_lock": threading.RLock(), "running": False} + def _session(name, profile_home=None): + return {"agent": SimpleNamespace(name=name), "history": [], "history_lock": threading.RLock(), + "running": False, "profile_home": profile_home} + profile_b = tmp_path / "profile-b" + profile_b.mkdir() monkeypatch.setattr(srv, "_sessions", { - "A": _session("agent-A"), "B": _session("agent-B"), + "A": _session("agent-A"), "B": _session("agent-B", profile_home=str(profile_b)), "lazy": {"agent": None, "history_lock": threading.RLock()}, }) - return refreshed + return SimpleNamespace(refreshed=refreshed, discovered_homes=discovered_homes, profile_b=profile_b) def test_reload_from_one_session_refreshes_every_live_agent(reload_env): resp = srv._methods["reload.mcp"](1, {"session_id": "A", "confirm": True}) assert resp["result"]["status"] == "reloaded" - assert sorted(reload_env) == ["agent-A", "agent-B"] + assert sorted(reload_env.refreshed) == ["agent-A", "agent-B"] def test_reload_without_session_id_still_refreshes_live_agents(reload_env): resp = srv._methods["reload.mcp"](1, {"confirm": True}) assert resp["result"]["status"] == "reloaded" - assert sorted(reload_env) == ["agent-A", "agent-B"] + assert sorted(reload_env.refreshed) == ["agent-A", "agent-B"] + + +def test_reload_rediscovers_under_each_live_profile_scope(reload_env): + """The unscoped shutdown tears down every profile's servers; discovery under the ambient home + alone would leave a secondary-profile session refreshing against a registry that never + regained its overlay, so it loses its MCP tools until its own reload.""" + srv._methods["reload.mcp"](1, {"session_id": "A", "confirm": True}) + + assert hermes_constants.hermes_home_key() in reload_env.discovered_homes + assert hermes_constants.hermes_home_key(reload_env.profile_b) in reload_env.discovered_homes diff --git a/tui_gateway/methods_tools.py b/tui_gateway/methods_tools.py index 358cedb604..3424139267 100644 --- a/tui_gateway/methods_tools.py +++ b/tui_gateway/methods_tools.py @@ -314,6 +314,17 @@ def _(rid, params: dict) -> dict: if after == loaded: break loaded = after + # The unscoped shutdown tore down every profile's servers, but discover_mcp_tools() above + # only rebuilt the launch profile's overlay; a secondary-profile session refreshed against + # that registry would lose its MCP tools until its own reload. + with _sessions_lock: + homes = {sess.get("profile_home") for sess in _sessions.values() if sess.get("agent") is not None} + for home in sorted(homes - {None}): + try: + with _session_profile_runtime_scope({"profile_home": home}): + _mcp_discovery.discover_mcp_tools() + except Exception as _exc: + logger.warning("MCP rediscovery failed for profile %s: %s", home, _exc) _refresh_session_agent() _mcp_reload_loaded_rev = loaded _mcp_reload_gen += 1