fix(mcp): address adversarial review round 2 (stale-publish race, parity holes)

Second review pass (Codex + Hermes subagent). Codex reproduced a real race with
a two-thread harness; both converged on the remaining issues.

- Generation-aware publish (fixes a lost-update race): two refresh callers (the
  late-refresh daemon and the between-turns prologue around turn 1) could each
  compute a snapshot outside the lock; a SLOWER caller holding an OLDER registry
  generation could acquire the publish lock after a newer caller and clobber it,
  deleting just-landed tools. refresh_agent_mcp_tools now captures
  registry._generation before computing and refuses to publish a stale set;
  agent._tool_snapshot_generation tracks the published generation.
- Context-engine routing names (_context_engine_tool_names) are now staged on a
  local and published atomically with the snapshot, and only claimed when this
  rebuild actually appended the schema — matching agent_init's dedup so a
  registry/plugin tool of the same name keeps its own dispatch. (Previously
  mutated live, before the publish lock, and on no-change refreshes.)
- CLI /reload-mcp: self.enabled_toolsets is resolved once at startup, so a
  server newly ENABLED in config mid-session wasn't picked up (TUI already
  re-resolved). Merge now-connected MCP server names into the override (unless
  the user pinned all/*), mirroring startup, and keep self.enabled_toolsets in
  sync. Closes the CLI/TUI parity hole.
- ACP (acp_adapter/server.py) routed through the shared helper — it was a 5th
  sibling rebuild that re-injected memory tools but NOT context-engine tools and
  bypassed the atomic/name-diff path (inert today, fragile).
- mcp_startup._resolve_discovery_timeout pulls its default from DEFAULT_CONFIG
  (single source of truth) instead of a stale hardcoded 5.0 literal.
- Tests: stale-generation-no-clobber, _skip_mcp_refresh honored, timeout
  fallback uses DEFAULT_CONFIG.
This commit is contained in:
alt-glitch 2026-06-19 22:52:47 +05:30 committed by Teknium
parent b6e2a54a94
commit 88d523220f
7 changed files with 137 additions and 40 deletions

21
cli.py
View file

@ -9668,13 +9668,28 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
# memory-provider and context-engine tools survive the rebuild).
if self.agent is not None:
from tools.mcp_tool import refresh_agent_mcp_tools
# Explicit reload: re-resolve enabled toolsets so a server the
# user just enabled in config this session is picked up.
# Explicit reload: pick up MCP servers the user ENABLED in config
# this session. self.enabled_toolsets was resolved once at
# startup; merge in any now-connected server names (unless the
# user pinned `all`/`*`, which already includes everything) so a
# freshly-added server isn't filtered out. Mirrors startup, where
# MCP server names are part of enabled_toolsets (see __init__).
enabled_override = None
et = self.enabled_toolsets
if et and "all" not in et and "*" not in et:
merged = list(et)
for _name in sorted(connected_servers):
if _name not in merged:
merged.append(_name)
enabled_override = merged
refresh_agent_mcp_tools(
self.agent,
enabled_override=self.enabled_toolsets,
enabled_override=enabled_override,
quiet_mode=True,
)
# Keep the CLI's own list in sync with what the agent now uses.
if enabled_override is not None:
self.enabled_toolsets = enabled_override
# Inject a message at the END of conversation history so the
# model knows tools changed. Appended after all existing