Skip to content

fix(desktop): foreground bot opens take a reserved spawn slot from the first dial (#102281, salvage #102496) - #104139

Merged
kshitijk4poor merged 7 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/102496-foreground-spawn-priority
Sep 6, 2026
Merged

kshitijk4poor merged 7 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/102496-foreground-spawn-priority

Conversation

@kshitijk4poor

@kshitijk4poor kshitijk4poor commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Clicking a bot that is not yet running no longer waits out (or fails behind) the launch-time roster hydration queue — the click's dial takes a reserved foreground slot from its very first IPC.

Based on #102496 by @bounce12340 (cherry-picked, authorship preserved) with fixes on top. Fixes #102281.

Why

LocalBackendSpawnCoordinator (landed in #101930) caps local backends at maxBackends=3, FIFO, 30 s slot wait. With a large roster, launch hydration queues ~27 ensureBackend() dials; a user click queues behind them and dies with Local backend start for "…" timed out while waiting for a free slot.

Changes

From #102496 (@bounce12340):

  • Coordinator gains a foreground | background priority: background may hold at most limit - 1 slots when limit >= 2, foreground waiters drain first, a queued request can be promote()d.
  • Renderer user-open paths (openGatewayForProfile / openGatewayForAgent / activation doors / canonical chat open) pass spawnPriority: 'foreground' through preload → IPC → ensureBackend / ensureRegistryBackend / spawnPoolBackend. Roster hydration, hover prewarm and untagged getConnection(profile) stay background.
  • Background slot-wait timeouts are a distinct LocalBackendSlotWaitTimeoutError and are logged as "will retry on the next hydration" instead of "failed to start".
  • Default cap is unchanged (3).

Fixes on top (3 commits, ours):

  • The first IPC of a user open is the route probe (sharedPrimaryRoute / isAttachedSharedRemote), which was untagged — main started the spawn as a background wait and the click still stalled for the probe's 20 s timeout before the second, tagged dial promoted it. The probes now carry the priority.
  • pendingForegroundSpawns marks leaked — on the common path (entry already in the pool) and on every dial that never requests a slot (primary route, registry remote scope, guard rejection) — and later upgraded an unrelated hydration dial for the same key to foreground. Mark only when no entry exists; the IPC handler clears an unconsumed mark once its claim settles.
  • "waiting for a free local slot" was logged for a foreground request granted immediately; the request now reports queued and main logs only then.
  • Removed dead renderer code (an isBackgroundSlotWaitTimeout whose only caller rethrew on both branches; Electron rebuilds IPC rejections as plain Error, so its checks could never fire) and deduplicated the promote / log snippets in main.ts.

Validation

Check Result
vitest electron pool-spawn-coordinator + backend-dial-claim, ui src/store/**, src/sdk/** 125 files, 1568 passed
tsc -p . and tsc -p tsconfig.electron.json clean
eslint + prettier on touched files clean
Mutation: main's coordinator + this PR's coordinator tests 5 fail (tests have teeth)
Mutation: #102496-head gateway.ts + new gateway-spawn-priority.test.ts red on "route probe tagged"

Live Electron run with a large roster was not performed here; behaviour is verified by unit tests and code trace. Trade-off to note: with the default cap, hydration warm-up now uses 2 concurrent slots instead of 3.

Related open work: #102828 (default cap 3→8, orthogonal), #103634 (host.warmProfile bypassing the prewarm guard, complementary).

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) labels Sep 6, 2026
kshitijk4poor and others added 7 commits September 6, 2026 14:18
LocalBackendSpawnCoordinator is FIFO with maxBackends=3, so launch
hydration queues ~27 ensureBackend calls and a user click times out
waiting for a slot. Reserve a foreground slot, drain foreground first,
and fail background slot-wait quietly.

Fixes NousResearch#102281.
Co-authored-by: Josh Tsai <bounce12340@users.noreply.github.com>
Co-authored-by: Josh Tsai <bounce12340@users.noreply.github.com>
…aits

Follow-ups to the NousResearch#102496 salvage in the Electron main process:

- pendingForegroundSpawns leaked: promoteInFlightLocalSpawn marked the key
  even when the pool entry already existed (the common click path), and
  nothing consumed it. After that backend was reaped, the next dial for the
  key - normally 10 s roster hydration - spawned as foreground and sat in the
  reserved slot. Mark only when no entry exists yet; spawnPoolBackend consumes
  the mark before any early return (remote route included) so it never
  outlives the dial.
- The "waiting for a free local slot" log fired for a foreground request that
  was granted the reserved slot immediately (condition was queuedCount > 0
  after request()). The request now reports `queued`; log only then.
- One promotePoolEntry() and one logPoolSpawnFailure() replace three copies of
  the promote snippet and two copies of the background/foreground log branch;
  spawnPoolBackend reads entry.spawnPriority instead of a second opts channel;
  isBackgroundSlotWaitTimeout is an instanceof check (same process as the
  class, no duck typing).
Every user open first probes its route (sharedPrimaryRoute /
isAttachedSharedRemote) with getConnection / getConnectionFor, and only then
dials the secondary. With NousResearch#102496 only the second dial carried
priority: 'foreground', so main started (or joined) the spawn as a background
slot wait on the probe and the click still waited out the probe's 20 s
RECONNECT_ATTEMPT_TIMEOUT_MS before promotion kicked in. Thread the priority
into both probes; the activation doors (ensureGatewayForProfile /
ensureGatewayForAgent) pass 'foreground' explicitly.

Also drop the renderer-side isBackgroundSlotWaitTimeout + the try/catch whose
two branches both rethrew: Electron rebuilds IPC rejections as a plain Error,
so name/silent/priority never reached the renderer and the helper was dead.

Test: gateway-spawn-priority.test.ts asserts every dial of a foreground open
carries the tag and an untagged open never does (red on the NousResearch#102496 head).
… settles

spawnPoolBackend() is not on every dial path: a primary route (startHermes),
a registry remote scope (connectRegistryBackend), a reused primary SSH
backend, or a guard rejection all settle the claim without requesting a
slot, so a foreground mark set for that dial stayed in pendingForegroundSpawns
and would have upgraded the next background hydration spawn of the same key.
applySpawnPriority() now returns the cleanup; both IPC handlers run it in a
finally around the claim. The mark is also taken right before the slot
request instead of at function entry, so the remote branch never consumes it.
@kshitijk4poor
kshitijk4poor force-pushed the salvage/102496-foreground-spawn-priority branch from 7e224d3 to 72afbea Compare September 6, 2026 08:49
@kshitijk4poor
kshitijk4poor merged commit 08f170a into NousResearch:main Sep 6, 2026
35 checks passed
austinpickett added a commit that referenced this pull request Sep 16, 2026
… foreground priority (#113269)

* fix(desktop): foreground spawn priority through requestGatewayForAgent/retainGatewayForAgent and the session-create dials

The #102281/#104139 chain tagged every activation-door dial
(openGatewayForAgent/openGatewayForProfile/ensureGatewayForAgent/
sharedPrimaryRoute) as 'foreground' so a user-initiated open takes the
pool's reserved slot instead of queuing behind background roster
hydration. requestGatewayForAgent and retainGatewayForAgent never got a
spawnPriority, so they always dialed 'background', and they are the only
dial path createBackendSessionForSend (first send on a fresh chat) and
openNewSessionTile ("New session" / tab-strip "+") use. On a saturated
3-slot local pool the user's first message or new-tab click could wait
out the background slot timeout.

Add `{ spawnPriority }` options (main's requestGatewayForProfile shape,
5eb0ed4) to both functions and forward it into every dial they make:
requestGatewayForProfile on the scope===key and primary-registry
branches, isAttachedSharedRemote, openSecondary, and the plain-profile
retain branch's gatewayForProfile(key, true, ...) which neither #105390
nor #110354 covered. The four session-create call sites pass
'foreground'.

Tests: gateway-spawn-priority.test.ts gains both polarities for
request/retain plus the plain-profile retain branch; the
use-session-actions and default-new-session matchers pin the tagged
dial. SpawnPriority is now exported for the SDK.

* fix(desktop): canonical Bot Chat lookup dials foreground through host.requestProfile options

Clicking a bot in the roster runs findExistingCanonicalChat ->
requestForBot(bot, 'session.list') -> host.requestProfile(route, ...)
-> requestGatewayForAgent with no intent, so the first RPC of the
gesture, the one that cold-spawns the bot's backend on a local pool,
dialed 'background'. With three roster backends already hydrating the
click produced no backend activity and the fail-closed lookup surfaced
as a "try again" toast (#105104). The later host.openSession is already
foreground, but it never runs until this lookup returns.

host.requestProfile gains a fifth `options?: { spawnPriority }`
argument; `timeoutMs` stays the fourth positional so no existing caller
changes shape, and the SDK keeps its exact call arity when no options
are given. requestForBot takes the same options and forwards them only
when set, so passive roster warming (profiles.list, ui_meta) still
dials background. The canonical `session.list` and the `session.create`
that follows on a first-ever open both pass 'foreground'.

Tests: profile-routing.test.ts (options form with and without a
timeout), routing.test.ts (tag forwarded, untagged call keeps three
args), canonical-chat-registry.test.ts (session.list and session.create
carry foreground; session.title does not).

---------

Co-authored-by: nftpoetrist <264138787+nftpoetrist@users.noreply.github.com>
Co-authored-by: jxfjosh <282433633+jxfjosh@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop: large bot roster can't open bots after launch — spawn cap (#100985) starves foreground opens

4 participants