From 26b63141acc36f46d4af43f95e23755fec703263 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:47:50 -0700 Subject: [PATCH] fix(ai-vault): humanize runtime scanner errors --- src/main/runtime/rpc/methods/ai-vault.test.ts | 19 +++++++++++++++++++ src/main/runtime/rpc/methods/ai-vault.ts | 19 +++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/runtime/rpc/methods/ai-vault.test.ts b/src/main/runtime/rpc/methods/ai-vault.test.ts index 0eb9535a097..8b76e72663d 100644 --- a/src/main/runtime/rpc/methods/ai-vault.test.ts +++ b/src/main/runtime/rpc/methods/ai-vault.test.ts @@ -79,6 +79,14 @@ function makeDispatcher(): RpcDispatcher { return new RpcDispatcher({ runtime, methods: AI_VAULT_METHODS }) } +function makeFailingDispatcher(error: Error): RpcDispatcher { + const runtime = { + getRuntimeId: () => 'test-runtime', + listAiVaultSessions: vi.fn().mockRejectedValue(error) + } as unknown as OrcaRuntimeService + return new RpcDispatcher({ runtime, methods: AI_VAULT_METHODS }) +} + describe('aiVault.resolveSessionTitles handler', () => { beforeEach(() => { resolveAiVaultSessionTitlesInWorker.mockReset() @@ -229,6 +237,17 @@ describe('aiVault.listSessions handler + shared cache', () => { expect(response).toMatchObject({ ok: true, result: makeResult() }) }) + it('humanizes scanner supervision errors for remote clients', async () => { + const dispatcher = makeFailingDispatcher(new Error('AI Vault service restart circuit is open.')) + + await expect( + dispatcher.dispatch(makeRequest('aiVault.listSessions', { limit: 500 })) + ).resolves.toMatchObject({ + ok: false, + error: { message: 'Session scanning paused after repeated failures. Refresh to try again.' } + }) + }) + it('passes only the first 64 scopePaths to the scanner when a request exceeds the cap', async () => { const dispatcher = makeDispatcher() const scopePaths = Array.from({ length: 65 }, (_, index) => `/p/${index}`) diff --git a/src/main/runtime/rpc/methods/ai-vault.ts b/src/main/runtime/rpc/methods/ai-vault.ts index 9578c903eb2..a129422629e 100644 --- a/src/main/runtime/rpc/methods/ai-vault.ts +++ b/src/main/runtime/rpc/methods/ai-vault.ts @@ -5,6 +5,7 @@ import { restampAiVaultListResult } from '../../../ai-vault/session-list-results import { AI_VAULT_AGENTS, AI_VAULT_SCOPE_PATHS_MAX_COUNT } from '../../../../shared/ai-vault-types' import { AI_VAULT_SESSION_TITLE_REQUEST_MAX_COUNT } from '../../../../shared/ai-vault-session-title' import { LOCAL_EXECUTION_HOST_ID, parseExecutionHostId } from '../../../../shared/execution-host' +import { describeAiVaultScanError } from '../../../../shared/ai-vault-scan-error-message' // Why: bound limit + scopePaths so a client cannot force an unbounded scan. // Each scopePath is a host-local match prefix (validated/capped, never used for @@ -83,12 +84,18 @@ export const AI_VAULT_METHODS: RpcMethod[] = [ name: 'aiVault.listSessions', params: AiVaultListSessionsParams, handler: async (params, { runtime }) => { - const result = await runtime.listAiVaultSessions({ - limit: params.unlimited ? undefined : params.limit, - unlimited: params.unlimited, - force: params.force, - scopePaths: params.scopePaths - }) + let result + try { + result = await runtime.listAiVaultSessions({ + limit: params.unlimited ? undefined : params.limit, + unlimited: params.unlimited, + force: params.force, + scopePaths: params.scopePaths + }) + } catch (error) { + const raw = error instanceof Error ? error.message : String(error) + throw new Error(describeAiVaultScanError(raw)) + } // Why: web clients consume this response directly (no parent-side retag), // so sessions must come back stamped as the runtime host they addressed. return params.executionHostId