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:
@@ -54,20 +54,22 @@ def start_background_mcp_discovery(*, logger, thread_name: str) -> None:
|
||||
def _resolve_discovery_timeout(explicit: "float | None") -> float:
|
||||
"""Resolve the MCP discovery wait bound: explicit arg > config > default.
|
||||
|
||||
Reads ``mcp_discovery_timeout`` from config.yaml. Kept lazy and
|
||||
fail-safe — a missing/invalid value falls back to the historical 0.75s so
|
||||
a broken config can never make startup hang or crash.
|
||||
Reads ``mcp_discovery_timeout`` from config.yaml, defaulting to the value in
|
||||
``DEFAULT_CONFIG`` (single source of truth) when the key is absent. Kept lazy
|
||||
and fail-safe — a missing/invalid value or a broken config falls back to a
|
||||
short safe bound so startup can never hang or crash.
|
||||
"""
|
||||
if explicit is not None:
|
||||
return explicit
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
from hermes_cli.config import load_config, DEFAULT_CONFIG
|
||||
|
||||
raw = (load_config() or {}).get("mcp_discovery_timeout", 5.0)
|
||||
default = float(DEFAULT_CONFIG.get("mcp_discovery_timeout", 1.5))
|
||||
raw = (load_config() or {}).get("mcp_discovery_timeout", default)
|
||||
val = float(raw)
|
||||
return val if val > 0 else 0.75
|
||||
return val if val > 0 else default
|
||||
except Exception:
|
||||
return 0.75
|
||||
return 1.5
|
||||
|
||||
|
||||
def wait_for_mcp_discovery(timeout: "float | None" = None) -> None:
|
||||
|
||||
Reference in New Issue
Block a user