From f3e967aae56a9b568c39677697b32e5091aa1652 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Fri, 19 Jun 2026 23:01:01 +0530 Subject: [PATCH] =?UTF-8?q?fix(mcp):=20round-3=20polish=20=E2=80=94=20gene?= =?UTF-8?q?ration=20capture=20adjacency=20+=20gateway=20contract=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third review pass (Hermes subagent) declared convergence: no BLOCKING, the round-2 generation-aware publish / context-engine staging / CLI reload / ACP routing all verified correct by hand and by test. - agent_init: capture _tool_snapshot_generation immediately before the tool snapshot (was ~425 lines earlier); removes a harmless skew window so the recorded generation always matches the snapshot it describes. - gateway/run.py _execute_mcp_reload: keep preserving each cached agent's build-time enabled_toolsets EXACTLY (do NOT merge newly-connected servers like CLI/TUI do) and document WHY — gateway sessions can be deliberately locked down, and test_reload_mcp_preserves_per_agent_toolset_overrides asserts this. A reviewer suggested "parity" here; it would have violated that contract. --- agent/agent_init.py | 17 ++++++++++------- gateway/run.py | 9 +++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/agent/agent_init.py b/agent/agent_init.py index 90e363c7f2b..2d443241367 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -537,12 +537,8 @@ def init_agent( agent._skip_mcp_refresh = False # Registry generation the current tool snapshot was derived from. Lets a # late/concurrent refresh reject a stale (older-generation) rebuild instead - # of clobbering a newer one. See tools.mcp_tool.refresh_agent_mcp_tools. - try: - from tools.registry import registry as _registry - agent._tool_snapshot_generation = _registry._generation - except Exception: - agent._tool_snapshot_generation = 0 + # of clobbering a newer one. Set adjacent to the tool snapshot below. + agent._tool_snapshot_generation = 0 # Rate limit tracking — updated from x-ratelimit-* response headers # after each API call. Accessed by /usage slash command. agent._rate_limit_state: Optional["RateLimitState"] = None @@ -964,7 +960,14 @@ def init_agent( print(f"🔄 Fallback chain ({len(agent._fallback_chain)} providers): " + " → ".join(f"{f['model']} ({f['provider']})" for f in agent._fallback_chain)) - # Get available tools with filtering + # Get available tools with filtering. Capture the registry generation this + # snapshot is derived from FIRST, so a later concurrent refresh can tell + # whether it holds a newer or staler view (see refresh_agent_mcp_tools). + try: + from tools.registry import registry as _snapshot_registry + agent._tool_snapshot_generation = _snapshot_registry._generation + except Exception: + agent._tool_snapshot_generation = 0 agent.tools = _ra().get_tool_definitions( enabled_toolsets=enabled_toolsets, disabled_toolsets=disabled_toolsets, diff --git a/gateway/run.py b/gateway/run.py index 4a65501b6d2..2672ab43e95 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -11668,6 +11668,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew continue if _agent is None: continue + # Preserve each cached agent's build-time toolset + # selection EXACTLY: a gateway session built with a + # restricted enabled_toolsets (e.g. ["safe"]) must + # NOT silently gain tools after a reload. This is the + # opposite of the interactive CLI/TUI /reload-mcp, + # which is a single user re-applying their own config + # edit; gateway agents are per-session and may be + # deliberately locked down. (Contract is asserted by + # test_reload_mcp_preserves_per_agent_toolset_overrides.) refresh_agent_mcp_tools(_agent, quiet_mode=True) except Exception as _exc: logger.debug(