feat(cli): filter MCP server spawning by -t/--toolsets flag + fix orphan subprocess leak - #19000
SelfParody wants to merge 1 commit into
Conversation
Update — v3 force-pushed (force-with-lease)After fusion code review (Codex + Sonnet + Gemini + GPT-5.5 judge), v2 came back as SHIP_WITH_FIXES with 3 required fixes. All 3 applied + 1 strongly-recommended fix: Required fixes (all applied)
Strongly-recommended fix (applied)
Reviewer false-positive identified by GPT-5.5 judgeGemini claimed " Verified locally (post-rework)Patch v3 = 282 lines. Applies cleanly to current upstream/main HEAD. 🤖 Generated with Claude Code |
8999f8b to
a0015fe
Compare
…han subprocess leak v5 — addresses GPT-5.5 fusion-judge SHIP_WITH_FIXES on v4. One-line cosmetic fix: SIGTERM handler now uses cleanup-scope local alias `_os_local.kill(_os_local.getpid(), signum)` instead of bare `os.kill(os.getpid(), signum)`, completing the self-containment intent. Behavior unchanged (verified: SIGTERM still exits 143, SIGINT still 130). ═══ Code review history ═══ - v1: qwen3-max single review → SHIP_WITH_FIXES - v2: Fusion (Codex+Sonnet+Gemini, GPT-5.5 judge) → NEEDS_REWORK (7 fixes) - v3: Fusion → SHIP_WITH_FIXES (3 required + 4 hardening fixes) - v4: Fusion → SHIP_WITH_FIXES (1 cosmetic — use _os_local in SIGTERM) - v5 (this commit): cosmetic fix applied; behavior verified unchanged ═══ Final fix tracking ═══ | v2 required fix | Final | |---------------------------------------|-------| | 1. Fail-open import behavior | FIXED | | 2. SIGTERM exit code 143 | FIXED | | 3. Unmatched -t warning logic | FIXED | | 4. _stdio_pids robustness | FIXED | | 5. time/os imports verified | FIXED | | 6. Poll-loop logger.debug | FIXED | | 7. Idempotency guard | FIXED | | 8. SIGTERM uses self-contained _os | FIXED (v5) | ═══ Verified locally (final) ═══ hermes -z "ACK" -t web : 4.8s wall (was 65s — 93% reduction) hermes -z "ACK" -t slack : 5.5s wall (was 67s — 92% reduction) hermes -z "ACK" (no -t) : 75s (unchanged — backwards-compat) SIGINT mid-flight : exit 130 ✓ SIGTERM mid-flight : exit 143 ✓ Orphan accumulation : 0 new PPID=1 across 10+ runs Related issues: - Fixes the MCP subprocess component of NousResearch#18438 (gateway memory leak) - Supersedes the startup-drag portion of NousResearch#18523 (closed unmerged) - Extends toolset-gating pattern from NousResearch#18166 and NousResearch#5788 (memory) to MCPs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Update — v5 force-pushed (FINAL after 4 fusion rounds)After 4 fusion code review rounds (Codex + Sonnet + Gemini reviewers, GPT-5.5 judge), all concerns addressed: Final fix tracking
Verified locally (final)Notable reviewer false-positive caught by judgeCodex's v4 review claimed the SIGTERM handler would v5 applies the cosmetic fix anyway ( Cumulative review cost
vs estimated cost of merging the v1 broken patch: weeks of "Ctrl+C breaks Hermes CLI" debugging. 🤖 Generated with Claude Code |
a0015fe to
373608e
Compare
|
Thanks for identifying the unnecessary MCP startup work. The filtering goal remains valid on current main, but this patch needs a structural port rather than a direct salvage. Problems
Suggested changes
Automated hermes-sweeper review. |
|
Triage note (perf/P2 sweep, verified against Half landed independently: the orphan-subprocess-leak half is superseded on main — The diff no longer applies to main ( Benchmark: not reproduced — the diff does not apply to current main, so a same-base before/after (MCP servers spawned for |
|
Salvaged onto current main via #101634 — your commit is cherry-picked with authorship preserved (@SelfParody). The orphan-subprocess-reap half had already landed on main, so the salvage carries the Closing this one in favour of the salvage; thanks for the fix. |
Problem
Every
hermes -zinvocation spawns ALL configured MCP servers as fresh subprocesses, regardless of the-t/--toolsetsflag. With 4 MCP servers configured (Notion, Trello, Fireflies, Slack), this adds ~60 seconds of cold-start overhead per invocation (verified via stack-sampling: agent blocks inos_waitpid → __wait4for the entire gap). The-tflag currently only filters which tools the LLM sees in the prompt — it does not prevent unneeded MCP servers from spawning.Additionally, MCP subprocesses orphan to PPID=1 on
hermes -zexit because the existingshutdown_mcp_servers()and_kill_orphaned_mcp_children()helpers are not called from the oneshot CLI exit path. Observed: 184 orphaned MCP processes accumulating 6.5 GB RAM in a single development session.Impact
hermes -z "Say hello" -t webhermes -z "Say hello" -t slackhermes -z "Say hello"(no-t)-zrunsWhat this PR does
Patch B — MCP spawn filtering (
tools/mcp_tool.py+hermes_cli/main.py)allowed_mcp_names: Optional[List[str]] = Nonetodiscover_mcp_tools().-t/--toolsetsis set, only MCP servers whose names appear in the toolset list are spawned. Built-in toolset names (e.g.web,memory) in the list are silently ignored — they don't need MCP spawning anyway.-t, all configured MCPs spawn as before. No regression.Patch C — Orphan cleanup (
hermes_cli/main.py)atexithandler that callsshutdown_mcp_servers()(graceful async close) followed by a 50 ms settle and_kill_orphaned_mcp_children(include_active=True)(force-kill any subprocess that escaped SDK teardown).SIGTERM+SIGINThandlers sokill <pid>and Ctrl+C trigger the same cleanup. SIGKILL still bypasses (kernel-forced), butlaunchctl unload(SIGTERM) and interactive Ctrl+C are the common production cases.ValueErroron non-main thread,OSErroron platforms that don't allow these signals — falls back to atexit-only coverage).Related issues
feat(api_server): honor inbound model and provider fields) — same author, same general direction (per-request filtering)Test plan
time hermes -z "Say hello" -t webdrops from 65s+ to <10s on systems with 4 configured MCP serverstime hermes -z "Say hello"(no-t) unchanged (backwards-compat verified at 76.7s on a 4-MCP system, identical to pre-patch)ps -e -o pid,ppid,command | grep "npm exec.*mcp" | awk '$2==1'shows 0 new entries after consecutivehermes -zrunshermes -zrun; verify no orphan subprocesses remaingit apply --check)Code review
Internal code review by qwen3-max-preview flagged two warnings, both addressed in this commit:
Verdict: APPROVE_WITH_CHANGES (now applied).
🤖 Generated with Claude Code