Files
orca/src/shared/ai-vault-scan-error-message.ts
Brennan Benson 43461fca46 fix(ai-vault): replace scanner internals with actionable panel copy (#16229)
* fix(ai-vault): replace scanner internals with actionable panel copy

Agent Session History painted the scanner's supervision errors verbatim:
"AI Vault service restart circuit is open." and "AI Vault service timed
out after 130000ms." Neither tells a user what happened or what to do.

Add a shared mapper that rewrites the supervision family into copy tied
to an action, passing anything unrecognized through so scanner-authored
messages (host name, remote path, cap) keep their own wording. It also
strips Electron's `Error invoking remote method` wrapper, which this
path never handled. Applied at both surfaces the panel paints: the local
leg's scan-issue row and the thrown-rejection banner. Humanizing in main
covers remote clients too; the raw text moves to the main log.

Also make "Refresh to try again" true. The relay path lets a forced
refresh reopen the restart circuit, but on the local path `force` stopped
at the cache layer and never reached the supervisor, so the refresh
button was inert for the full 60s fault window.

The client file sat at 299/300 lines, so extract the child-listener and
init-frame wiring into `attachAiVaultServiceChild`, next to the
ready-waiter and retirement helpers it belongs with, rather than bumping
the max-lines ceiling.

* fix(ai-vault): humanize runtime scanner errors

* fix(ai-vault): preserve runtime error metadata

* fix(ai-vault): normalize wrapped scan errors

* fix(ai-vault): preserve non-scanner relay errors

* fix(ai-vault): make forced retries cancel backoff
2026-08-24 13:00:03 -07:00

51 lines
2.2 KiB
TypeScript

/**
* Maps the session scanner's internal supervision errors onto copy a user can
* act on. Applied at both surfaces the panel paints: the local leg's scan-issue
* row and the thrown-rejection banner. Anything unrecognized passes through, so
* scanner-authored messages (host name, remote path, cap) keep their own wording.
*/
const RETRY = 'Refresh to try again.'
/** Electron wraps every rejected `ipcMain.handle` before the renderer sees it. */
const IPC_INVOKE_PREFIX = /^Error invoking remote method '[^']*': (?:\w*Error: )?/
/** Relay-hosted scanner errors carry a transport-only `Relay ` prefix. */
const RELAY_PREFIX = /^Relay (?=AI Vault )/
/** Both the fork-based service and the legacy worker thread emit this family. */
const SCANNER = String.raw`AI Vault (?:service|scanner worker)`
function toSeconds(ms: string): number {
return Math.max(1, Math.round(Number(ms) / 1000))
}
function humanize(text: string): string | null {
const sshTimeout = /^Agent Session History scan timed out after (\d+)ms on this SSH host\.$/.exec(
text
)
if (sshTimeout) {
return `Session scan timed out after ${toSeconds(sshTimeout[1]!)}s on this SSH host. ${RETRY}`
}
const timeout = new RegExp(String.raw`^${SCANNER} timed out after (\d+)ms\.$`).exec(text)
if (timeout) {
return `Session scan timed out after ${toSeconds(timeout[1]!)}s — the machine may be busy. ${RETRY}`
}
if (text === 'AI Vault service restart circuit is open.') {
return `Session scanning paused after repeated failures. ${RETRY}`
}
if (new RegExp(String.raw`^${SCANNER} (?:client )?queue is full\.$`).test(text)) {
return 'Too many session scans in flight. Wait a moment, then refresh.'
}
if (new RegExp(String.raw`^${SCANNER} entry not found: `).test(text)) {
return 'The session scanner is missing from this Orca install. Reinstalling Orca restores it.'
}
if (new RegExp(String.raw`^${SCANNER}\b`).test(text)) {
return `The session scanner stopped unexpectedly. ${RETRY}`
}
return null
}
/** Returns user-facing copy, or the original text when it is already meaningful. */
export function describeAiVaultScanError(raw: string): string {
const text = raw.replace(IPC_INVOKE_PREFIX, '').replace(RELAY_PREFIX, '')
return humanize(text) ?? text
}