Skip to content

Desktop runtime readiness probes can saturate the shared RPC pool #65151

Description

@reymondmeking-dot

Describe the bug

The Desktop periodically calls setup.runtime_check to decide whether inference is ready. That RPC is routed through the shared _LONG_HANDLERS executor. If provider resolution becomes slow because of GIL pressure, a blocked keyring, OAuth refresh, or filesystem/auth lookup, every overlapping Desktop poll starts another provider-resolution call and occupies another worker in the shared RPC pool.

During a long tool-heavy session this can exhaust the shared pool and delay unrelated JSON-RPC responses. The Desktop then reports that inference is not ready even though credentials are configured and the provider itself is healthy.

Observed log pattern:

event loop stalled 20-30s (GIL pressure suspected)
ws write slow (loop stalled >10.0s)
setup.runtime_check ... timed out / failed
setup.status reports configured credentials, but runtime resolution still failed

Switching model providers does not resolve the symptom because the contention is inside the gateway process rather than at a specific provider.

Environment

  • Windows 11
  • Python 3.11
  • Hermes Desktop / Hermes Agent 0.18.0
  • Reproduced with multiple custom OpenAI-compatible providers

Root cause

setup.runtime_check is correctly routed away from the WebSocket reader thread by PR #57335, but the shared RPC executor does not deduplicate in-flight readiness checks. A slow check can therefore overlap with later 15-second Desktop polls. Each duplicate probe consumes a shared worker while resolving the same provider state.

A deterministic test can reproduce the problem by making resolve_runtime_provider() block and issuing overlapping setup.runtime_check requests. Current main invokes the resolver once per request instead of sharing the existing in-flight probe.

Expected behavior

  • At most one runtime-resolution probe should be active per requested provider.
  • Later polls should reuse the in-flight probe instead of consuming more shared RPC workers.
  • A transient timeout should be treated as an unknown readiness result, not ok=false, because setup.status may still authoritatively confirm that credentials are configured.
  • Unrelated gateway RPCs should remain responsive.

Suggested fix

  1. Run provider resolution on a dedicated single-worker executor instead of recursively consuming the shared RPC pool.
  2. Keep one in-flight future per requested provider and reuse it for overlapping polls.
  3. Allow the first caller a bounded wait; while the probe is still running, return a retryable result without an ok=false value.
  4. Clear the single-flight entry when the background probe completes.

This preserves the intent of #57335—keeping the WebSocket reader responsive—while preventing frontend polling from starving the executor that serves the rest of the gateway.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/desktopElectron desktop app (apps/desktop/*)comp/tuiTerminal UI (ui-tui/ + tui_gateway/)sweeper:risk-message-deliverySweeper risk: may drop, duplicate, misroute, or suppress messagestype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions