fix(desktop): foreground bot opens take a reserved spawn slot from the first dial (#102281, salvage #102496) - #104139
Merged
kshitijk4poor merged 7 commits intoSep 6, 2026
Conversation
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
force-pushed
the
salvage/102496-foreground-spawn-priority
branch
from
September 6, 2026 08:49
7e224d3 to
72afbea
Compare
This was referenced Sep 6, 2026
This was referenced Sep 7, 2026
5 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 atmaxBackends=3, FIFO, 30 s slot wait. With a large roster, launch hydration queues ~27ensureBackend()dials; a user click queues behind them and dies withLocal backend start for "…" timed out while waiting for a free slot.Changes
From #102496 (@bounce12340):
foreground | backgroundpriority: background may hold at mostlimit - 1slots whenlimit >= 2, foreground waiters drain first, a queued request can bepromote()d.openGatewayForProfile/openGatewayForAgent/ activation doors / canonical chat open) passspawnPriority: 'foreground'through preload → IPC →ensureBackend/ensureRegistryBackend/spawnPoolBackend. Roster hydration, hover prewarm and untaggedgetConnection(profile)stay background.LocalBackendSlotWaitTimeoutErrorand are logged as "will retry on the next hydration" instead of "failed to start".Fixes on top (3 commits, ours):
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.pendingForegroundSpawnsmarks 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.queuedand main logs only then.isBackgroundSlotWaitTimeoutwhose only caller rethrew on both branches; Electron rebuilds IPC rejections as plainError, so its checks could never fire) and deduplicated the promote / log snippets inmain.ts.Validation
vitestelectronpool-spawn-coordinator+backend-dial-claim, uisrc/store/**,src/sdk/**tsc -p .andtsc -p tsconfig.electron.jsongateway.ts+ newgateway-spawn-priority.test.tsLive 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.warmProfilebypassing the prewarm guard, complementary).