fix(desktop): reuse primary WS for named profiles on attached shared remote (#96493)
Bot Mode group turns on a Windows Desktop attached to a remote gateway dialed a registry secondary per member profile. That second WebSocket accept/closed in ~30ms and never ran session.create. When the window primary is already that one-host-many-profiles source (sharedRemote), request/retain/open/ensure now reuse the primary socket and scope RPCs with profile=. Isolated SSH/pooled backends are unchanged.
This commit is contained in:
@@ -564,3 +564,75 @@ describe('retainGatewayForAgent (#93602)', () => {
|
||||
expect(secondaryGateways[0].close).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('attached shared-remote group turns (#96493)', () => {
|
||||
function installAttachedSharedRemote() {
|
||||
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
||||
getConnection: vi.fn(async (profile: null | string) => ({ port: 4242, profile, token: 't' })),
|
||||
getConnectionFor: vi.fn(async ({ connectionId, profile }: { connectionId: string; profile: string }) => ({
|
||||
connectionId,
|
||||
port: 9119,
|
||||
profile,
|
||||
sharedRemote: true
|
||||
})),
|
||||
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }: { connectionId: string; profile: string }) => ({
|
||||
ok: true as const,
|
||||
wsUrl: `ws://${connectionId}/${profile}`
|
||||
})),
|
||||
touchBackend: vi.fn(async () => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
it('reuses the primary socket for a named profile on the attached shared remote', async () => {
|
||||
const primary = makePrimary()
|
||||
setPrimaryGateway(primary as never, 'default')
|
||||
setPrimaryGatewayConnection({ connectionId: 'homelab' })
|
||||
installAttachedSharedRemote()
|
||||
await ensureGatewayForProfile('default')
|
||||
|
||||
const release = await retainGatewayForAgent('homelab', 'voter')
|
||||
await requestGatewayForAgent('homelab', 'voter', 'session.create', { title: 'Group: room' })
|
||||
await requestGatewayForAgent('homelab', 'voter', 'prompt.submit', { session_id: 'rt-1', text: 'hi' })
|
||||
|
||||
expect(secondaryGateways).toHaveLength(0)
|
||||
expect(primary.request).toHaveBeenCalledTimes(2)
|
||||
expect(primary.request).toHaveBeenNthCalledWith(1, 'session.create', {
|
||||
title: 'Group: room',
|
||||
profile: 'voter'
|
||||
})
|
||||
expect(primary.request).toHaveBeenNthCalledWith(2, 'prompt.submit', {
|
||||
session_id: 'rt-1',
|
||||
text: 'hi',
|
||||
profile: 'voter'
|
||||
})
|
||||
|
||||
release()
|
||||
expect(secondaryGateways).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('still dials a secondary when the attached source is not a shared remote', async () => {
|
||||
const primary = makePrimary()
|
||||
setPrimaryGateway(primary as never, 'default')
|
||||
setPrimaryGatewayConnection({ connectionId: 'homelab' })
|
||||
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
|
||||
getConnection: vi.fn(async (profile: null | string) => ({ port: 4242, profile, token: 't' })),
|
||||
getConnectionFor: vi.fn(async ({ connectionId, profile }: { connectionId: string; profile: string }) => ({
|
||||
connectionId,
|
||||
port: 5151,
|
||||
profile,
|
||||
sharedRemote: false
|
||||
})),
|
||||
getGatewayWsUrlFor: vi.fn(async ({ connectionId, profile }: { connectionId: string; profile: string }) => ({
|
||||
ok: true as const,
|
||||
wsUrl: `ws://${connectionId}/${profile}`
|
||||
})),
|
||||
touchBackend: vi.fn(async () => undefined)
|
||||
}
|
||||
await ensureGatewayForProfile('default')
|
||||
|
||||
await requestGatewayForAgent('homelab', 'voter', 'session.create', { title: 'g' })
|
||||
|
||||
expect(secondaryGateways).toHaveLength(1)
|
||||
expect(primary.request).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -296,6 +296,60 @@ function isPrimaryRegistryRoute(connectionId: null | string, profile: string): b
|
||||
)
|
||||
}
|
||||
|
||||
/** True when `connectionId` is the window's already-attached source AND that
|
||||
* source is a one-host-many-profiles remote (`sharedRemote`). Named member
|
||||
* profiles on that host must reuse the primary socket — a registry secondary
|
||||
* dials a second WebSocket at the same Tailscale URL, which accept/closes in
|
||||
* ~30ms (`messages=1`) and never runs `session.create` (#96493). Isolated
|
||||
* SSH/pooled backends (`sharedRemote: false`) still get their own secondary. */
|
||||
async function isAttachedSharedRemote(connectionId: null | string, profile: string): Promise<boolean> {
|
||||
const id = String(connectionId ?? '').trim()
|
||||
const key = normKey(profile)
|
||||
|
||||
if (!id || !g.primaryConnectionId || id !== g.primaryConnectionId) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isPrimaryRegistryRoute(id, key)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const desktop = window.hermesDesktop
|
||||
|
||||
if (!desktop?.getConnectionFor) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const conn = await withTimeout(
|
||||
desktop.getConnectionFor({ connectionId: id, profile: key }),
|
||||
RECONNECT_ATTEMPT_TIMEOUT_MS,
|
||||
`Timed out resolving shared-remote route for "${key}"`
|
||||
)
|
||||
|
||||
return Boolean(conn && typeof conn === 'object' && (conn as { sharedRemote?: boolean }).sharedRemote === true)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function requestOnPrimaryGateway<T>(
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
timeoutMs?: number,
|
||||
signal?: AbortSignal
|
||||
): Promise<T> {
|
||||
const gateway = g.primaryGateway
|
||||
|
||||
if (!gateway) {
|
||||
throw new Error('Hermes gateway unavailable')
|
||||
}
|
||||
|
||||
return timeoutMs === undefined && signal === undefined
|
||||
? gateway.request<T>(method, params)
|
||||
: gateway.request<T>(method, params, timeoutMs, signal)
|
||||
}
|
||||
|
||||
export function isActivePrimary(): boolean {
|
||||
return g.activeKey === g.primaryProfile
|
||||
}
|
||||
@@ -870,6 +924,10 @@ export async function requestGatewayForAgent<T>(
|
||||
return requestGatewayForProfile<T>(key, method, params, timeoutMs, signal)
|
||||
}
|
||||
|
||||
if (await isAttachedSharedRemote(connectionId, key)) {
|
||||
return requestOnPrimaryGateway<T>(method, { ...params, profile: key }, timeoutMs, signal)
|
||||
}
|
||||
|
||||
if (!window.hermesDesktop?.getConnectionFor) {
|
||||
throw new Error('This Desktop build cannot dial registry connections. Update Hermes Desktop.')
|
||||
}
|
||||
@@ -1054,6 +1112,11 @@ export async function retainGatewayForAgent(connectionId: null | string, profile
|
||||
return route.release
|
||||
}
|
||||
|
||||
if (isPrimaryRegistryRoute(connectionId, key) || (await isAttachedSharedRemote(connectionId, key))) {
|
||||
// Primary socket stays open for the window lifetime — no secondary to hold.
|
||||
return () => undefined
|
||||
}
|
||||
|
||||
if (!window.hermesDesktop?.getConnectionFor) {
|
||||
// No registry dialing in this build — nothing to hold; the request path
|
||||
// will throw its own actionable error.
|
||||
@@ -1268,6 +1331,10 @@ export async function openGatewayForAgent(
|
||||
return openGatewayForProfile(profile)
|
||||
}
|
||||
|
||||
if (await isAttachedSharedRemote(connectionId, profile)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!window.hermesDesktop?.getConnectionFor) {
|
||||
throw new Error('This Desktop build cannot dial registry connections. Update Hermes Desktop.')
|
||||
}
|
||||
@@ -1314,6 +1381,10 @@ export async function ensureGatewayForAgent(
|
||||
return !signal?.aborted
|
||||
}
|
||||
|
||||
if (await isAttachedSharedRemote(connectionId, profile)) {
|
||||
return !signal?.aborted
|
||||
}
|
||||
|
||||
if (!window.hermesDesktop?.getConnectionFor) {
|
||||
throw new Error('This Desktop build cannot dial registry connections. Update Hermes Desktop.')
|
||||
}
|
||||
|
||||
2
contributors/emails/paula@smfworks.com
Normal file
2
contributors/emails/paula@smfworks.com
Normal file
@@ -0,0 +1,2 @@
|
||||
smfworks
|
||||
# SMF Works contributor
|
||||
Reference in New Issue
Block a user