Skip to content

fix(tui): spawn slash workers on demand instead of one per session - #66783

Closed
Ne0teric wants to merge 1 commit into
NousResearch:mainfrom
Ne0teric:lazy-slash-worker
Closed

fix(tui): spawn slash workers on demand instead of one per session#66783
Ne0teric wants to merge 1 commit into
NousResearch:mainfrom
Ne0teric:lazy-slash-worker

Conversation

@Ne0teric

Copy link
Copy Markdown
Contributor

Problem

Every slash_worker child runs its own MCP discovery (#61891), which forks the full configured stdio MCP fleet — with a handful of stdio servers that's ~20 OS processes per worker once the npx/cmd wrappers are counted. The gateway pre-warms a worker for every session at create/build time, and sessions held by a live transport are (correctly) never reaped — so a desktop app left open for days accumulates one full fleet per retained session.

On a real setup (Windows, 5 stdio MCP servers, desktop app open ~2 days) this reached ~120 processes across 6 retained sessions, pushed system commit charge to ~90%, and Windows began failing unrelated process spawns system-wide with "Not enough memory resources are available to process this command".

Fix

slash.exec already spawns a worker on demand when the session has none, and its error path already recovers from a dead worker the same way — the eager pre-warm is pure pre-warming. This PR:

  • drops the pre-warm in the deferred session-build path (_start_agent_build)
  • drops the pre-warm in _init_session
  • makes _restart_slash_worker a no-op for sessions that never spawned a worker — the next slash.exec builds one with the current session key/model, so no stale-key worker can exist for them

Only sessions that actually run a worker-routed slash command now pay for a fleet. Trade-off: the first worker-routed command in a session takes the CLI-build + MCP-discovery hit that session.create used to absorb (a few seconds on a config with several stdio servers).

Tests

  • The two create/close-race guards now assert the build thread never constructs a worker; their original regression intent (orphaned approval-notify registration) is preserved.
  • test_restart_slash_worker_closes_orphan_when_session_reaped seeds a live worker so the reap-race close path is still exercised (stale + fresh both closed).
  • New test_restart_slash_worker_noop_without_worker pins the no-op.
  • tests/test_tui_gateway_server.py, tests/test_lazy_session_regressions.py, tests/tui_gateway: 745 passed; the 4 failures present are identical on clean main in the same environment (pre-existing, unrelated).

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tui Terminal UI (ui-tui/ + tui_gateway/) tool/mcp MCP client and OAuth sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 18, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to merged #61949 and #39502, but not a duplicate: this avoids creating a slash-worker/MCP fleet for sessions that never need one, rather than changing worker-local discovery or orphan cleanup.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
"event": "COMMENT",

Code Review Summary\n\nVerdict: Approved\n\nLooks good. No obvious issues found.\n\n---\nReviewed by Hermes Agent",

"comments": []
}

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused TUI resource-lifecycle fix. Current main still pre-warms _SlashWorker in both deferred agent build (tui_gateway/server.py:1575) and eager session setup (tui_gateway/server.py:5030), while every worker runs separate MCP discovery (tui_gateway/slash_worker.py:68-86). The existing slash.exec fallback already performs the proposed lazy construction (tui_gateway/server.py:14078-14088), and the adjusted restart behavior preserves replacement only for sessions that actually have a worker.

I found no substantive correctness or design-fit issue in the proposed scope. GitHub reports this branch as conflicting with current main, so it needs conflict-aware salvage rather than a clean merge.

Automated hermes-sweeper review.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 18, 2026
Every slash_worker child runs its own MCP discovery (NousResearch#61891), which
forks the full configured stdio MCP fleet — on a config with a handful
of stdio servers that is ~20 OS processes per worker once npx/cmd
wrappers are counted. The gateway pre-warmed a worker for every session
at create/build time, and sessions held by a live transport are (by
design) never reaped, so a desktop app left open for days accumulates
one fleet per retained session. On a real setup this reached ~120
processes across 6 sessions and pushed Windows commit charge to the
point where CreateProcess started failing system-wide ("Not enough
memory resources are available to process this command").

slash.exec already spawns a worker on demand when the session has none
and already recovers from a dead worker the same way, so the eager
pre-warm is pure pre-warming:

- drop the pre-warm in the deferred session-build path
- drop the pre-warm in _init_session
- make _restart_slash_worker a no-op for sessions that never spawned a
  worker (the next slash.exec builds one with the current session
  key/model, so no stale-key worker can exist)

Only sessions that actually run a worker-routed slash command now pay
for a fleet. Cost: the first such command in a session takes the CLI
build + MCP discovery hit that session.create used to absorb.

Tests: the two create/close-race guards now assert the build thread
never constructs a worker (the notify-unregister guarantees are kept);
the restart-orphan guard seeds a live worker so the close path is still
exercised; new test pins the restart no-op for workerless sessions.
@Ne0teric
Ne0teric force-pushed the lazy-slash-worker branch from 1f0de39 to 961b0d0 Compare July 19, 2026 05:30
@Ne0teric

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (e598cef) — conflicts resolved. The only conflict was positional: the new test_restart_slash_worker_noop_without_worker was inserted at the same spot where test_session_close_rpc_delegates_to_close_session_by_id was rewritten into test_session_close_rpc_claims_then_tears_down; both are kept. The three server.py changes applied cleanly over the reworked teardown path. Gateway suites re-run post-rebase: 755 passed, remaining failures are the same 4 that fail identically on clean main in this environment (pre-existing, unrelated).

@teknium1 teknium1 added the area/sessions Session lifecycle, resume, persistence, history label Jul 19, 2026
@dschnurbusch

Copy link
Copy Markdown
Contributor

Independent macOS incident evidence strongly confirms the failure mode and impact described here.

On a 48 GB Apple Silicon Mac, Hermes Desktop was left open overnight and macOS reported the application above 50 GB before the machine froze. The system jetsam snapshot attributed 49.69 GiB current / 57.23 GiB lifetime-max to the Hermes coalition:

  • 164 node processes: about 44.99 GiB
  • 84 python3.11 processes: about 3.32 GiB
  • Electron renderer: about 0.80 GiB
  • Hermes parent: about 0.20 GiB

The desktop backend's final disconnect diagnostic reported detached_sessions=20. Runtime inspection confirms each live session eagerly owns a _SlashWorker, and each worker performs its own stdio MCP discovery. The configured npx MCP servers therefore multiplied across those retained sessions. Several individual Node children reached roughly 3.5-4.2 GiB during the incident.

I independently reproduced and tested this PR's fix shape on current origin/main (a61183b56): remove both eager worker creation sites and preserve slash_worker=None in _restart_slash_worker until the existing slash.exec lazy path is used.

Validation on current main:

  • focused lifecycle regressions: 7 passed
  • tests/test_tui_gateway_server.py: 431 passed
  • remaining TUI gateway/slash-worker suite: 427 passed

One MCP integration cleanup test failed because the live-system test guard refused to terminate its own reparented subprocess; it fails identically on an untouched detached origin/main, and the MCP response assertions pass before cleanup.

This is not just idle-memory optimization. It prevents a confirmed system-wide out-of-memory failure on macOS, and it directly addresses the process multiplication seen in the diagnostic report.

@alt-glitch alt-glitch added P1 High — major feature broken, no workaround and removed P2 Medium — degraded but workaround exists labels Jul 24, 2026
teknium1 added a commit that referenced this pull request Jul 24, 2026
With the eager pre-warm removed (PR #66783), slash.exec is the only spawn
path — and it runs on the RPC thread pool, so two concurrent worker-routed
commands on a fresh session could both see slash_worker=None and each fork
a full stdio-MCP-fleet worker (the _attach_worker race loser leaking
unclosed). Add a per-session spawn lock with a double-check, plus a
regression test racing two slash.exec calls through handle_request.

Also maps Ne0teric's contributor email.
teknium1 added a commit that referenced this pull request Jul 24, 2026
With the eager pre-warm removed (PR #66783), slash.exec is the only spawn
path — and it runs on the RPC thread pool, so two concurrent worker-routed
commands on a fresh session could both see slash_worker=None and each fork
a full stdio-MCP-fleet worker (the _attach_worker race loser leaking
unclosed). Add a per-session spawn lock with a double-check, plus a
regression test racing two slash.exec calls through handle_request.

Also maps Ne0teric's contributor email.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #71006 with your authorship preserved — on-demand spawn shipped plus a per-session spawn lock on top (two concurrent slash commands could double-fork MCP fleets). The OOM reports in your thread made the impact case. Thanks.

@teknium1 teknium1 closed this Jul 24, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
With the eager pre-warm removed (PR NousResearch#66783), slash.exec is the only spawn
path — and it runs on the RPC thread pool, so two concurrent worker-routed
commands on a fresh session could both see slash_worker=None and each fork
a full stdio-MCP-fleet worker (the _attach_worker race loser leaking
unclosed). Add a per-session spawn lock with a double-check, plus a
regression test racing two slash.exec calls through handle_request.

Also maps Ne0teric's contributor email.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/tui Terminal UI (ui-tui/ + tui_gateway/) P1 High — major feature broken, no workaround sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants