Skip to content

perf(serve): move Desktop runtime services after bind - #96751

Closed
helix4u wants to merge 2 commits into
NousResearch:mainfrom
helix4u:feat/desktop-post-bind-runtime-discovery
Closed

helix4u wants to merge 2 commits into
NousResearch:mainfrom
helix4u:feat/desktop-post-bind-runtime-discovery

Conversation

@helix4u

@helix4u helix4u commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

The packaged Desktop only needs its authenticated loopback hermes serve socket before the renderer can connect and paint, but agent plugin discovery, plugin API loading, MCP startup, cron initialization, and orphan-gateway cleanup currently run ahead of that ready point.

This moves those Desktop-only runtime services behind the bound-socket event. The optimization is deliberately unavailable to ordinary Dashboard launches, non-headless serves, non-loopback binds, non-Desktop processes, or any configuration with a public dashboard URL, because those paths may require plugin-provided authentication before bind.

Plugin API requests arriving during the short deferred window wait for route mounting instead of receiving a transient 404. Agent construction continues to use the existing plugin and MCP readiness gates before snapshotting tools.

Desktop performance series

This change is one independently reviewable layer of the same Desktop startup and first-interaction performance pass.

The three Python backend PRs share startup files but solve separate stages. Recommended landing order is #96749, then #96750, then #96751, rebasing the next PR only after the preceding one lands. #97032 is an independently reviewable Electron ordering change. The remaining renderer and Bot Mode PRs can also land independently; their effects compose without making cached state authoritative.
This PR owns the backend readiness boundary: the verified local Desktop socket binds before nonessential runtime services finish loading.

Related Issue

N/A

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • Add a fail-closed detector for the packaged, headless, loopback Desktop path with no configured public URL.
  • Defer agent plugin discovery, plugin API route mounting, and MCP startup until after Uvicorn binds and emits the ready sentinel.
  • Return a bounded retryable 503 only if deferred plugin routes cannot become ready within 10 seconds; unrelated requests never wait.
  • Start Desktop cron and startup orphan cleanup after the same bound-socket event.
  • Preserve eager plugin/auth ordering for Dashboard and public surfaces.
  • Add behavioral tests for the guard matrix, deferred ordering, route wait/release behavior, timer ownership, failure release, cron, and orphan cleanup.

How to Test

  1. Run scripts/run_tests.sh tests/hermes_cli/test_desktop_post_bind_runtime_discovery.py tests/hermes_cli/test_plugin_api_compat.py tests/hermes_cli/test_web_server_cron_profiles.py tests/hermes_cli/test_dashboard_auth_plugin_hook.py -q -j 4.
  2. Run scripts/run_tests.sh tests/hermes_cli/test_web_server_boot_handshake.py tests/tui_gateway/test_cold_start_gil_stall.py tests/hermes_cli/test_windows_gateway_cold_start_desktop_lifecycle.py -q -j 4.
  3. Confirm the focused groups pass 50/50 and 16/16 respectively, then launch packaged Desktop and verify the ready socket is available before runtime discovery completes.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Windows 11

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

Focused results: 66 tests passed across the two explicit groups.

@alt-glitch alt-glitch added type/perf Performance improvement or optimization P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard labels Aug 28, 2026
@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

perf(serve): move Desktop runtime services after bind — verified local-only deferral, with 503 guard for transient plugin 404s.

  • hermes_cli/main.py:_desktop_runtime_discovery_can_follow_bind — four-gate check (headless_backend + HERMES_DESKTOP==1 + loopback host + no public_url) decides whether discovery may follow the ready socket. Reads dashboard.public_url from raw config and HERMES_DASHBOARD_PUBLIC_URL env, failing closed to "<unresolved>" on read error so ordinary/public dashboards retain pre-bind ordering. Only the packaged Desktop headless loopback path reaches the deferred branch.
  • hermes_cli/web_server.py — import-time one-shot HERMES_DEFER_DESKTOP_PLUGIN_API_MOUNT is consumed and popped so child processes cannot inherit it. _DEFER_DESKTOP_PLUGIN_API_MOUNT also requires HERMES_DESKTOP==1 + HERMES_SERVE_HEADLESS==1 at import time. When deferred: cron/orphan-reap are moved to _start_desktop_services_after_bind (threads wait on desktop_post_bind Event set in _serve after bind), plugin API mount + MCP discovery move to _run_deferred_runtime_discovery scheduled 0.75s after bind via daemon Timer, and a middleware _wait_for_deferred_plugin_api_routes holds /api/plugins/* for up to 10s (→ 503 + Retry-After:1 on timeout) so transient 404s do not surface.
  • start_server(defer_runtime_discovery=...) resets app.state.defer_desktop_services_until_bind on every call so _lifespan (also exercised by TestClient without start_server) cannot leak policy.
  • Tests in test_desktop_post_bind_runtime_discovery.py cover all gates: defer only for local headless Desktop, only for loopback/public_url absent, 503 wait on pending mount, unrelated path never waits, discovery order plugins→mount→mcp, mount failure still sets ready so waiters unblock, Timer is daemonized, and cron/reap wait for the bind Event. Well-scoped for a boot-order change.

@helix4u

helix4u commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author
image

teknium1 added a commit that referenced this pull request Sep 2, 2026
…y imports the SDK

`cmd_dashboard` started the background MCP discovery thread before importing
`hermes_cli.web_server`. The thread's first act is the ~350ms `mcp` SDK
import, which holds the GIL against the main thread's own web_server import,
so the HERMES_BACKEND_READY sentinel — and every renderer paint behind it —
moved ~300ms later on every Desktop cold start with any MCP server configured.

Desktop `serve` (headless + HERMES_DESKTOP=1) now arms discovery one second
after the sentinel instead. Starting it AT the bind was measured to give back
most of the gain (the renderer's WebSocket connect + first hydration reads
contend on the same loop). An agent build inside that window pulls the
deferred start forward itself via `wait_for_mcp_discovery`, so the bounded
join and the late-binding tool refresh behave exactly as before. Dashboard
and non-Desktop `serve` keep the eager pre-import ordering.

Minimal reimplementation of the MCP-deferral slice of #96751 by @helix4u;
the plugin-route deferral / 503 middleware / cron-after-bind slices were
measured at ~0-10ms each and are not taken.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
@teknium1

teknium1 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Thanks — the measured win in this PR (the mcp SDK import on the discovery thread holding the GIL during web_server import) landed on main via #101298 as a minimal slice with you co-authored: discovery armed after bind, wait_for_mcp_discovery pulls it forward. Real Desktop cold start READY 1667→1397ms. The other service moves measured no gain so were left out. Closing in favor of the merged slice.

@teknium1 teknium1 closed this Sep 2, 2026
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…y imports the SDK

`cmd_dashboard` started the background MCP discovery thread before importing
`hermes_cli.web_server`. The thread's first act is the ~350ms `mcp` SDK
import, which holds the GIL against the main thread's own web_server import,
so the HERMES_BACKEND_READY sentinel — and every renderer paint behind it —
moved ~300ms later on every Desktop cold start with any MCP server configured.

Desktop `serve` (headless + HERMES_DESKTOP=1) now arms discovery one second
after the sentinel instead. Starting it AT the bind was measured to give back
most of the gain (the renderer's WebSocket connect + first hydration reads
contend on the same loop). An agent build inside that window pulls the
deferred start forward itself via `wait_for_mcp_discovery`, so the bounded
join and the late-binding tool refresh behave exactly as before. Dashboard
and non-Desktop `serve` keep the eager pre-import ordering.

Minimal reimplementation of the MCP-deferral slice of NousResearch#96751 by @helix4u;
the plugin-route deferral / 503 middleware / cron-after-bind slices were
measured at ~0-10ms each and are not taken.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
github-actions Bot pushed a commit to RecursiveIntell/Ares that referenced this pull request Sep 8, 2026
…y imports the SDK

`cmd_dashboard` started the background MCP discovery thread before importing
`hermes_cli.web_server`. The thread's first act is the ~350ms `mcp` SDK
import, which holds the GIL against the main thread's own web_server import,
so the HERMES_BACKEND_READY sentinel — and every renderer paint behind it —
moved ~300ms later on every Desktop cold start with any MCP server configured.

Desktop `serve` (headless + HERMES_DESKTOP=1) now arms discovery one second
after the sentinel instead. Starting it AT the bind was measured to give back
most of the gain (the renderer's WebSocket connect + first hydration reads
contend on the same loop). An agent build inside that window pulls the
deferred start forward itself via `wait_for_mcp_discovery`, so the bounded
join and the late-binding tool refresh behave exactly as before. Dashboard
and non-Desktop `serve` keep the eager pre-import ordering.

Minimal reimplementation of the MCP-deferral slice of NousResearch#96751 by @helix4u;
the plugin-route deferral / 503 middleware / cron-after-bind slices were
measured at ~0-10ms each and are not taken.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
(cherry picked from commit 4155ea9)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants