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.
This commit is contained in:
teknium1
2026-09-13 13:56:53 -07:00
committed by Teknium
parent 243392b196
commit ccf66b29ad
2 changed files with 35 additions and 8 deletions

View File

@@ -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

View File

@@ -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