From 026c45e9e359f86ae318bb9db042b7871fbf86c9 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:11:11 -0700 Subject: [PATCH] fix(ai-vault): give Orca servers reached over SSH the 15 s all-hosts scan budget (#31139) (#31226) All hosts gave every Orca server a 3 s scan, including the managed servers Orca reaches through an SSH tunnel, which now serve every SSH host. A healthy SSH host walking a large home timed out and showed up empty. The saved environment's connectionDependency now picks the 15 s budget for tunnelled servers; direct LAN/Tailscale servers keep 3 s. Reported, with the proposed approach, by @quzard in #31139. --- .../ai-vault/runtime-session-scanner.test.ts | 8 +++++-- src/main/ai-vault/runtime-session-scanner.ts | 11 +++++----- src/main/ipc/ai-vault-all-host-timeouts.ts | 21 +++++++++++++------ src/main/ipc/ai-vault-runtime-scan.ts | 2 ++ src/main/ipc/ai-vault.test.ts | 21 +++++++++++++++++++ src/main/ipc/ai-vault.ts | 4 ++-- 6 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/main/ai-vault/runtime-session-scanner.test.ts b/src/main/ai-vault/runtime-session-scanner.test.ts index e431038ccff..8f866c23c02 100644 --- a/src/main/ai-vault/runtime-session-scanner.test.ts +++ b/src/main/ai-vault/runtime-session-scanner.test.ts @@ -33,12 +33,16 @@ describe('runtime AI Vault session scanner', () => { it('lists saved runtime environments as runtime execution hosts', () => { mocks.listEnvironments.mockReturnValue([ { id: 'env-1', name: 'Server 1' }, - { id: 'env-2', name: 'Server 2' } + { id: 'env-2', name: 'Server 2', connectionDependency: 'ssh-tunnel' } ]) expect(getSavedRuntimeAiVaultHostInfos('/user-data')).toEqual([ { environmentId: 'env-1', executionHostId: 'runtime:env-1' }, - { environmentId: 'env-2', executionHostId: 'runtime:env-2' } + { + environmentId: 'env-2', + executionHostId: 'runtime:env-2', + connectionDependency: 'ssh-tunnel' + } ]) }) diff --git a/src/main/ai-vault/runtime-session-scanner.ts b/src/main/ai-vault/runtime-session-scanner.ts index 9e3fe411932..3436c07eae1 100644 --- a/src/main/ai-vault/runtime-session-scanner.ts +++ b/src/main/ai-vault/runtime-session-scanner.ts @@ -21,11 +21,7 @@ import type { import { parseAiVaultListResult } from './session-list-result-validation' import { parseAiVaultSessionTitlesResult } from './session-title-result-validation' import { isGatedOffWslHost } from '../wsl/wsl-host-gate' - -export type RuntimeAiVaultHostInfo = { - environmentId: string - executionHostId: `runtime:${string}` -} +import type { RuntimeAiVaultHostInfo } from '../ipc/ai-vault-runtime-scan' export type RuntimeAiVaultScanOptions = { timeoutMs?: number @@ -53,7 +49,10 @@ export function getSavedRuntimeAiVaultHostInfos( .filter((environment) => !isGatedOffWslHost(environment)) .map((environment) => ({ environmentId: environment.id, - executionHostId: toRuntimeExecutionHostId(environment.id) + executionHostId: toRuntimeExecutionHostId(environment.id), + ...(environment.connectionDependency + ? { connectionDependency: environment.connectionDependency } + : {}) })) } diff --git a/src/main/ipc/ai-vault-all-host-timeouts.ts b/src/main/ipc/ai-vault-all-host-timeouts.ts index 42a05c99b3f..a9fff53725f 100644 --- a/src/main/ipc/ai-vault-all-host-timeouts.ts +++ b/src/main/ipc/ai-vault-all-host-timeouts.ts @@ -1,13 +1,22 @@ +import type { RuntimeAiVaultHostInfo } from './ai-vault-runtime-scan' + // Per-leg bounds for the all-hosts fan-outs, so one slow host cannot hold a merge open. export const AI_VAULT_ALL_HOST_TIMEOUT_MS = { runtimeScan: 3_000, // Why: a remote home with many agent roots routinely needs seconds to walk, // stat and parse. The old shared 3s bound emptied healthy SSH hosts in the - // all-hosts view; the relay gets a real scan budget and the whole leg (relay - // attempt plus any legacy crawl) stays bounded. - sshScanRelay: 15_000, - sshScan: 20_000, - // A search reads an index rather than walking a home, but it shares the relay - // with the scans, so it gets the relay budget rather than one of its own. + // all-hosts view; a server reached over an SSH tunnel walks the same home + // plus the tunnel's latency, so it gets a real scan budget. + sshTunneledRuntimeScan: 15_000, + // A search reads an index rather than walking a home, but it shares the + // connection with the scans, so it gets the SSH scan budget. search: 15_000 } as const + +export function allHostsRuntimeScanTimeoutMs( + hostInfo: Pick +): number { + return hostInfo.connectionDependency === 'ssh-tunnel' + ? AI_VAULT_ALL_HOST_TIMEOUT_MS.sshTunneledRuntimeScan + : AI_VAULT_ALL_HOST_TIMEOUT_MS.runtimeScan +} diff --git a/src/main/ipc/ai-vault-runtime-scan.ts b/src/main/ipc/ai-vault-runtime-scan.ts index 7485415c555..1cb6863366b 100644 --- a/src/main/ipc/ai-vault-runtime-scan.ts +++ b/src/main/ipc/ai-vault-runtime-scan.ts @@ -12,6 +12,8 @@ import { aiVaultScanIssueResult } from '../ai-vault/session-list-results' export type RuntimeAiVaultHostInfo = { environmentId: string executionHostId: `runtime:${string}` + /** The saved environment's own record of how it is reached; picks the all-hosts scan budget. */ + connectionDependency?: 'ssh-tunnel' } export type RuntimeAiVaultScanOptions = { diff --git a/src/main/ipc/ai-vault.test.ts b/src/main/ipc/ai-vault.test.ts index 51ef45a3049..4f669e82a78 100644 --- a/src/main/ipc/ai-vault.test.ts +++ b/src/main/ipc/ai-vault.test.ts @@ -96,6 +96,27 @@ describe('listAiVaultSessions host routing', () => { ) }) + it('gives servers reached over an SSH tunnel the longer all-hosts scan budget', async () => { + registerAiVaultHandlers({ + getActiveRuntimeAiVaultHostInfos: () => [ + { environmentId: 'lan-server', executionHostId: 'runtime:lan-server' }, + { + environmentId: 'ssh-server', + executionHostId: 'runtime:ssh-server', + connectionDependency: 'ssh-tunnel' + } + ], + scanRuntimeAiVaultSessions: mocks.scanRuntimeAiVaultSessions + }) + + await _internals.listAiVaultSessions({ executionHostScope: 'all', force: true }) + + const timeoutFor = (environmentId: string): unknown => + mocks.scanRuntimeAiVaultSessions.mock.calls.find(([id]) => id === environmentId)?.[2] + expect(timeoutFor('lan-server')).toEqual({ timeoutMs: 3_000 }) + expect(timeoutFor('ssh-server')).toEqual({ timeoutMs: 15_000 }) + }) + it('keeps direct runtime host scans on the normal runtime timeout', async () => { registerAiVaultHandlers({ getActiveRuntimeAiVaultHostInfos: () => [], diff --git a/src/main/ipc/ai-vault.ts b/src/main/ipc/ai-vault.ts index afd183fa860..53dddff9daa 100644 --- a/src/main/ipc/ai-vault.ts +++ b/src/main/ipc/ai-vault.ts @@ -59,7 +59,7 @@ import { OPENS_EVERY_STRUCTURED_CHAT, projectStructuredAiVaultSessions } from '../ai-vault/structured-session-ownership' -import { AI_VAULT_ALL_HOST_TIMEOUT_MS } from './ai-vault-all-host-timeouts' +import { allHostsRuntimeScanTimeoutMs } from './ai-vault-all-host-timeouts' import { registerAiVaultScanProgressBroadcast } from './ai-vault-scan-progress-broadcast' type AiVaultHandlerOptions = AiVaultSessionSources & @@ -156,7 +156,7 @@ async function scanAiVaultSessionsByHostScope( listArgs: args ? { ...args, afterFingerprint: undefined } : undefined, options: { signal, - timeoutMs: AI_VAULT_ALL_HOST_TIMEOUT_MS.runtimeScan + timeoutMs: allHostsRuntimeScanTimeoutMs(hostInfo) } }) })