Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,10 @@ describe('createBackendSessionForSend profile routing', () => {
'source-a',
'default',
'session.create',
expect.objectContaining({ profile: 'backend-default', source: 'desktop' })
expect.objectContaining({ profile: 'backend-default', source: 'desktop' }),
undefined,
undefined,
'foreground'
)
expect(ambientRequest).not.toHaveBeenCalledWith('session.create', expect.anything())
})
Expand Down
22 changes: 17 additions & 5 deletions apps/desktop/src/app/session/hooks/use-session-actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,8 +569,12 @@ export function useSessionActions({
// foreground hold below takes over from that point until the created
// chat is selected. Between the two, nothing may close the socket
// that just minted the runtime.
//
// 'foreground' spawn priority (#102281 primitive): this is the user
// hitting send on a fresh chat, so the dial must not queue behind
// background roster hydration on a saturated pool.
const releaseCreateLease = capturedRoute
? await retainGatewayForAgent(capturedRoute.connectionId, capturedRoute.profile)
? await retainGatewayForAgent(capturedRoute.connectionId, capturedRoute.profile, 'foreground')
: () => undefined

let created: SessionCreateResponse
Expand All @@ -582,7 +586,10 @@ export function useSessionActions({
capturedRoute.connectionId,
capturedRoute.profile,
'session.create',
params
params,
undefined,
undefined,
'foreground'
)
: await requestGateway<SessionCreateResponse>('session.create', params)

Expand Down Expand Up @@ -765,9 +772,11 @@ export function useSessionActions({

// Same lease chain as createBackendSessionForSend: owner socket held
// across the create, then the foreground hold carries it until the
// tile is mounted ($sessionTiles names the owner from then on).
// tile is mounted ($sessionTiles names the owner from then on). Same
// 'foreground' spawn priority too — this is also a direct user click
// ("New session" / tab-strip "+"), not background hydration.
const releaseCreateLease = capturedRoute
? await retainGatewayForAgent(capturedRoute.connectionId, capturedRoute.profile)
? await retainGatewayForAgent(capturedRoute.connectionId, capturedRoute.profile, 'foreground')
: () => undefined

let created: SessionCreateResponse
Expand All @@ -779,7 +788,10 @@ export function useSessionActions({
capturedRoute.connectionId,
capturedRoute.profile,
'session.create',
params
params,
undefined,
undefined,
'foreground'
)
: await requestGateway<SessionCreateResponse>('session.create', params)

Expand Down
57 changes: 57 additions & 0 deletions apps/desktop/src/store/gateway-spawn-priority.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ vi.mock('@/hermes', () => ({
}
onEvent = vi.fn(() => () => {})
onState = vi.fn(() => () => {})
request = vi.fn(async () => ({}))
}
}))
vi.mock('@/store/session', () => ({ setConnection: vi.fn(), setGatewayState: vi.fn() }))
Expand All @@ -31,6 +32,8 @@ const {
ensureGatewayForProfile,
openGatewayForAgent,
openGatewayForProfile,
requestGatewayForAgent,
retainGatewayForAgent,
setPrimaryGateway
} = await import('./gateway')

Expand Down Expand Up @@ -109,4 +112,58 @@ describe('user opens dial main as foreground from the first IPC (#102281)', () =
expect(seen.length).toBeGreaterThanOrEqual(1)
expect(seen.every(priority => priority === 'foreground')).toBe(true)
})

// requestGatewayForAgent/retainGatewayForAgent are the session-scoped RPC
// lease pair createBackendSessionForSend/openNewSessionTile dial through for
// a user's first message / "New session" click on a not-yet-open registry
// route — the same user-initiated-open contract as the activation doors
// above, just reached through a different pair of functions. Unlike those
// doors, neither of these hardcodes 'foreground' or forwarded a caller's
// priority before, so a saturated pool queued the click behind background
// roster hydration exactly like the pre-#102281 bug.
it('requestGatewayForAgent forwards spawnPriority to the registry dial', async () => {
const desktop = installDesktop()
setPrimaryGateway({ connectionState: 'open' } as never, 'default')

await requestGatewayForAgent('homelab', 'research', 'session.create', {}, undefined, undefined, 'foreground')

const seen = priorities(desktop.getConnectionFor, args => (args[0] as { priority?: string }).priority)
expect(seen.length).toBeGreaterThanOrEqual(1)
expect(seen.every(priority => priority === 'foreground')).toBe(true)
})

it('requestGatewayForAgent without a priority never tags the registry dial as foreground', async () => {
const desktop = installDesktop()
setPrimaryGateway({ connectionState: 'open' } as never, 'default')

await requestGatewayForAgent('homelab', 'research', 'session.create', {})

const seen = priorities(desktop.getConnectionFor, args => (args[0] as { priority?: string }).priority)
expect(seen.length).toBeGreaterThanOrEqual(1)
expect(seen.every(priority => priority === undefined)).toBe(true)
})

it('retainGatewayForAgent forwards spawnPriority to the registry dial', async () => {
const desktop = installDesktop()
setPrimaryGateway({ connectionState: 'open' } as never, 'default')

const release = await retainGatewayForAgent('homelab', 'research', 'foreground')
release()

const seen = priorities(desktop.getConnectionFor, args => (args[0] as { priority?: string }).priority)
expect(seen.length).toBeGreaterThanOrEqual(1)
expect(seen.every(priority => priority === 'foreground')).toBe(true)
})

it('retainGatewayForAgent without a priority never tags the registry dial as foreground', async () => {
const desktop = installDesktop()
setPrimaryGateway({ connectionState: 'open' } as never, 'default')

const release = await retainGatewayForAgent('homelab', 'research')
release()

const seen = priorities(desktop.getConnectionFor, args => (args[0] as { priority?: string }).priority)
expect(seen.length).toBeGreaterThanOrEqual(1)
expect(seen.every(priority => priority === undefined)).toBe(true)
})
})
17 changes: 11 additions & 6 deletions apps/desktop/src/store/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,8 @@ export async function requestGatewayForAgent<T>(
method: string,
params: Record<string, unknown> = {},
timeoutMs?: number,
signal?: AbortSignal
signal?: AbortSignal,
spawnPriority: SpawnPriority = 'background'
): Promise<T> {
const key = normKey(profile)
const scope = registryBackendScopeKey(connectionId, key)
Expand All @@ -970,7 +971,7 @@ export async function requestGatewayForAgent<T>(
return requestGatewayForProfile<T>(key, method, params, timeoutMs, signal)
}

if (await isAttachedSharedRemote(connectionId, key)) {
if (await isAttachedSharedRemote(connectionId, key, spawnPriority)) {
return requestOnPrimaryGateway<T>(method, { ...params, profile: key }, timeoutMs, signal)
}

Expand All @@ -994,7 +995,7 @@ export async function requestGatewayForAgent<T>(

try {
if (!isOpen(entry.gateway)) {
await openSecondary(entry)
await openSecondary(entry, spawnPriority)
}

return await (timeoutMs === undefined && signal === undefined
Expand Down Expand Up @@ -1146,7 +1147,11 @@ export function retainGatewayForRelay(connectionId: null | string, profile: stri
* `finally`; the refcount keeps the socket (and the session it minted) alive
* for the whole sequence. Primary/shared-primary routes return a no-op release.
*/
export async function retainGatewayForAgent(connectionId: null | string, profile: string): Promise<() => void> {
export async function retainGatewayForAgent(
connectionId: null | string,
profile: string,
spawnPriority: SpawnPriority = 'background'
): Promise<() => void> {
const key = normKey(profile)
const scope = registryBackendScopeKey(connectionId, key)

Expand All @@ -1158,7 +1163,7 @@ export async function retainGatewayForAgent(connectionId: null | string, profile
return route.release
}

if (isPrimaryRegistryRoute(connectionId, key) || (await isAttachedSharedRemote(connectionId, key))) {
if (isPrimaryRegistryRoute(connectionId, key) || (await isAttachedSharedRemote(connectionId, key, spawnPriority))) {
// Primary socket stays open for the window lifetime — no secondary to hold.
return () => undefined
}
Expand Down Expand Up @@ -1214,7 +1219,7 @@ export async function retainGatewayForAgent(connectionId: null | string, profile

try {
if (!isOpen(entry.gateway)) {
await openSecondary(entry)
await openSecondary(entry, spawnPriority)
}
} catch (error) {
release()
Expand Down
Loading