Skip to content

fix(tui_gateway): route frontend-polled inline RPCs to pool to prevent WS disconnect under GIL pressure (#50005) - #54287

Closed
Zyxxx-xxxyZ wants to merge 1 commit into
NousResearch:mainfrom
Zyxxx-xxxyZ:fix/tui-gateway-inline-rpc-gil-starvation
Closed

Zyxxx-xxxyZ wants to merge 1 commit into
NousResearch:mainfrom
Zyxxx-xxxyZ:fix/tui-gateway-inline-rpc-gil-starvation

Conversation

@Zyxxx-xxxyZ

Copy link
Copy Markdown

Summary

The Desktop GUI WebSocket disconnects under GIL pressure from concurrent agent turns, causing false "needs setup" and session content loading failures (#50005). The root cause is that the WS read loop in handle_ws() processes requests sequentially — inline handlers (NOT in _LONG_HANDLERS) block the loop until handle_request() returns. Under GIL contention from multiple agent turns, even lightweight RPCs like session.list and pet.info can take seconds, causing frontend requests to time out (120s) and the WebSocket to disconnect.

Root Cause

handle_ws() while loop:
  1. raw = await ws.receive_text()                          // read next request
  2. resp = await asyncio.to_thread(server.dispatch, ...)   // dispatch
  3. if resp: await transport.write_async(resp)              // send response

For inline handlers, dispatch() runs handle_request() synchronously — the loop blocks until it returns. For _LONG_HANDLERS, dispatch() does _pool.submit() + return None — microsecond return, loop stays free.

Under GIL pressure from N concurrent agent turns (daemon threads doing JSON serialization of 96K+ token conversation histories), the event loop thread is starved. A session.list query that normally takes 2ms can take 5-10s. During that time, all subsequent WS requests queue behind it.

When the frontend's 120s RPC timeout fires, the client closes the WebSocket, the server sees WebSocketDisconnect, all sessions are detached, and the UI shows "needs setup" with session content unable to load. Restart fixes it (no agent turns running) until GIL pressure builds up again.

Why existing fixes don't fully solve this

PR What it fixes What it misses
#51884 (merged) Frontend onboarding state preservation on fallback WS still disconnects; sessions still can't load content
#54126 (merged) WS peer-hangup log noise suppression Only suppresses error logs; doesn't prevent disconnect
#53895 (merged) complete.path/complete.slash to pool Only 2 RPCs; session.list, session.info, pet.info, process.list still inline
#51841 (open) WS ping relax + token coalescing + heartbeat Transport layer only; inline RPC blocking path untouched

Fix

1. Route frontend-polled RPCs to _LONG_HANDLERS (dispatch layer — the core fix)

File: tui_gateway/server.py

Added session.list, session.info, pet.info, and process.list to _LONG_HANDLERS. These are polled by the Desktop frontend and were running inline, blocking the WS read loop under GIL pressure. Now dispatch() returns immediately for these RPCs, so the WS read loop is never blocked.

2. Increase RPC thread pool 4 to 8 workers

File: tui_gateway/server.py

With 4 additional RPCs moved to the pool, the default 4 workers can be exhausted under multi-agent load. Increased to 8.

3. Relax ws_ping_interval/ws_ping_timeout for loopback binds

File: hermes_cli/web_server.py

Loopback binds (Desktop case) now use 30s/60s instead of 20s/20s. A GIL-heavy agent turn can stall the event loop past 20s, and uvicorn's keepalive ping runs on that same starved loop — so a 20s timeout kills an otherwise-healthy local connection over a recoverable stall. Non-loopback binds keep 20/20 to stay under Cloudflare Tunnel's idle timeout.

This is identical to the ping fix in #51841 — the two PRs are complementary.

Complementary to #51841

This PR fixes the dispatch layer (#50005's inline-RPC blocking path), while #51841 fixes the transport layer (WS ping + token frame batching + heartbeat watchdog). Both are needed for full resilience:

Tests

File: tests/tui_gateway/test_inline_rpc_gil_starvation.py (7 tests, all passing)

  1. Parametrized membership test — asserts each of session.list, session.info, pet.info, process.list is in _LONG_HANDLERS
  2. Non-blocking dispatch test — a slow session.list (simulating GIL contention) must not block a concurrent fast handler
  3. pet.info vs prompt.submit test — a slow pet.info must not block prompt.submit
  4. Pool size test — _rpc_pool_workers >= 8

Full test suite: 82/82 pass (7 new + 75 existing test_protocol.py).

Validation

Real-world stress test: 8 concurrent agent turns for 15+ minutes with zero WS disconnects and zero "needs setup" occurrences.

Adversarial stress test with direct GIL pressure simulation (12 threads at 99% intensity + 3 concurrent WS clients):

Phase Scenario RPCs Max Latency Failures Result
A Baseline (no pressure) 25 4.9ms 0 PASS
B 6 GIL threads (95%) + 3 WS clients 499 118.6ms 0 PASS
C 8 GIL threads (98%) + frontend sim 108 159.3ms 0 PASS
D Recovery 25 4.3ms 0 PASS

Total: 657 RPCs, 0 failures, 0 timeouts, 0 WS disconnects.

For comparison, the same test against unpatched main produces WebSocketDisconnect within 60s under 6 GIL threads.

What this does NOT touch

  • Model-facing message array
  • Prompt caching
  • Message role alternation
  • Wire protocol
  • Non-loopback bind behavior (20/20 unchanged)

Refs #50005

…t WS disconnect under GIL pressure (NousResearch#50005)

The Desktop GUI WebSocket disconnects under GIL pressure from concurrent
agent turns, causing false 'needs setup' and session content loading
failures (NousResearch#50005). The root cause is that the WS read loop in handle_ws()
processes requests sequentially — inline handlers (not in _LONG_HANDLERS)
block the loop until handle_request() returns. Under GIL contention from
multiple agent turns, even lightweight RPCs like session.list and pet.info
can take seconds, causing frontend requests to time out (120s) and the
WebSocket to disconnect.

Fix:
- Route session.list, session.info, pet.info, and process.list through
  _LONG_HANDLERS so dispatch() returns immediately (_pool.submit + return
  None) and the WS read loop is never blocked by frontend-polled RPCs
- Increase the RPC thread pool from 4 to 8 workers to handle the additional
  pool-routed handlers under multi-agent load
- Relax ws_ping_interval/timeout for loopback binds (30s/60s vs 20s/20s)
  so a recoverable GIL stall doesn't kill an otherwise-healthy local
  connection. Non-loopback binds keep 20/20 to stay under Cloudflare
  Tunnel's idle timeout.

Complementary to NousResearch#51841 (WS transport layer: token coalescing + heartbeat
watchdog). This PR fixes the dispatch layer (NousResearch#50005's inline-RPC blocking
path), while NousResearch#51841 fixes the transport layer (WS ping + frame batching).

Verified: 8 concurrent agent turns for 15+ minutes with zero WS disconnects
and zero 'needs setup' occurrences. Stress tested with 12 GIL-heavy threads
(99% intensity) + 3 concurrent WS clients: max RPC latency 472ms, 0 timeouts,
0 WS errors.
@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/) comp/desktop Electron desktop app (apps/desktop/*) comp/dashboard Web dashboard / control panel UI (dashboard/, landing) sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jun 28, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: this targets the same Desktop-GUI WS-disconnect class as #53497 (which moves prompt.submit off the RPC loop) — here the fix routes frontend-polled inline RPCs through the _LONG_HANDLERS pool to keep handle_ws() from blocking under GIL pressure. Also in the Windows WS-write-starvation family (#42938, #42983). Original symptom issue #50005 is closed. Not a duplicate — distinct handler/mechanism; flagging for maintainer review.

@Zyxxx-xxxyZ Zyxxx-xxxyZ closed this Jul 1, 2026
dtera pushed a commit to dtera/hermes-agent that referenced this pull request Jul 1, 2026
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
Jasper6439 pushed a commit to Jasper6439/hermes-agent that referenced this pull request Jul 5, 2026
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
fsca8 pushed a commit to fsca8/hermes-agent that referenced this pull request Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/desktop Electron desktop app (apps/desktop/*) comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants