From 1b57acf94a036ebb64d8d1ad7eae70dd450ad230 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Fri, 25 Sep 2026 14:28:16 -0400 Subject: [PATCH] fix(mcp): budget discovery-thread GIL so agent build fits the 30s wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Background MCP discovery does bursty CPU work (SDK/pydantic imports, JSON-RPC schema parsing, tool registration) that at the default 5ms switch interval rides the GIL convoy effect and starves the concurrent agent-build thread and the main event loop for tens of seconds after a serve restart — _wait_agent(30s) times out with 'agent initialization timed out' (error 5032). Lower the interpreter switch interval while discovery runs (restored afterwards) so its CPU bursts are sliced finely enough that waiters stay responsive. Fixes #60371 --- hermes_cli/mcp_startup.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/hermes_cli/mcp_startup.py b/hermes_cli/mcp_startup.py index 44a61379d3..33d9ada840 100644 --- a/hermes_cli/mcp_startup.py +++ b/hermes_cli/mcp_startup.py @@ -2,10 +2,11 @@ from __future__ import annotations +import sys import threading -from contextlib import nullcontext +from contextlib import contextmanager, nullcontext from contextvars import copy_context -from typing import Dict, Optional, Set +from typing import Dict, Iterator, Optional, Set from hermes_constants import hermes_home_key @@ -158,6 +159,27 @@ def _resolve_discovery_timeout(explicit: "float | None", *, single_query: bool = return default +# GIL budget for discovery: the discovery thread does bursty CPU work (mcp/pydantic imports, +# JSON-RPC schema parsing, tool registration) that, at the default 5 ms switch interval, rides +# the GIL convoy effect and can starve concurrent threads — the agent-build thread and the main +# event loop — for tens of seconds (#60371: "agent initialization timed out" after a serve +# restart, _wait_agent(30s) error 5032). While discovery runs we drop the switch interval so its +# CPU bursts are sliced finely enough that waiters stay responsive, then restore it. +_DISCOVERY_SWITCH_INTERVAL_S = 0.0005 + + +@contextmanager +def _discovery_gil_budget() -> Iterator[None]: + """Temporarily lower the interpreter switch interval so discovery's CPU work can't + monopolize the GIL against concurrent threads (see _DISCOVERY_SWITCH_INTERVAL_S).""" + prev = sys.getswitchinterval() + sys.setswitchinterval(_DISCOVERY_SWITCH_INTERVAL_S) + try: + yield + finally: + sys.setswitchinterval(prev) + + def _discover_mcp_tools_without_interactive_oauth() -> None: """Run MCP discovery without letting OAuth read from the user's stdin.""" try: @@ -165,7 +187,7 @@ def _discover_mcp_tools_without_interactive_oauth() -> None: except Exception: suppress_interactive_oauth = nullcontext - with suppress_interactive_oauth(): + with _discovery_gil_budget(), suppress_interactive_oauth(): from tools.mcp_tool_discovery import discover_mcp_tools # Only pass the kwarg when a filter is set: many tests (and any