Files
orca/src/main/runtime/rpc/errors.ts
T

157 lines
5.2 KiB
TypeScript

// Why: every RPC response needs the same runtimeId envelope, and the
// runtime/browser error allowlists define the contract the CLI relies on to
// format human-facing messages. Centralizing this mapping keeps the allowlist
// auditable in one place instead of spread across per-method branches.
import type { RpcEnvelopeMeta, RpcFailure, RpcSuccess } from './core'
import { computerUseErrorRecoveryData } from '../../../shared/computer-use-error-recovery'
import { COMPUTER_ERROR_CODES } from '../../../shared/runtime-types'
import { LINEAR_ERROR_CODES } from '../../../shared/linear-agent-access'
import { AGENT_SESSION_RPC_ERROR_CODES } from '../../../shared/agent-session-host-authority'
export function successResponse(id: string, meta: RpcEnvelopeMeta, result: unknown): RpcSuccess {
return {
id,
ok: true,
result,
_meta: meta
}
}
export function errorResponse(
id: string,
meta: RpcEnvelopeMeta,
code: string,
message: string,
data?: unknown
): RpcFailure {
return {
id,
ok: false,
error: data === undefined ? { code, message } : { code, message, data },
_meta: meta
}
}
// Why: the OrcaRuntimeService throws plain Error objects whose `message` is
// actually a stable error code. This allowlist is the contract the CLI relies
// on — expanding or renaming entries without updating the CLI would silently
// change user-visible error codes.
const RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
'runtime_unavailable',
'selector_not_found',
'selector_ambiguous',
'terminal_handle_stale',
'terminal_not_writable',
'terminal_exited',
'terminal_gone',
'terminal_tab_close_timeout',
'terminal_tab_not_found',
'terminal_tab_pinned',
'no_active_terminal',
'repo_not_found',
'timeout',
'invalid_limit',
'remote_update_manual_required',
'remote_update_not_available',
'remote_update_not_downloaded',
...AGENT_SESSION_RPC_ERROR_CODES
])
const COMPUTER_PASSTHROUGH_CODES: ReadonlySet<string> = new Set(Object.values(COMPUTER_ERROR_CODES))
const LINEAR_PASSTHROUGH_CODES: ReadonlySet<string> = new Set(LINEAR_ERROR_CODES)
const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
'worktree_id_requires_full_path'
])
export function mapRuntimeError(id: string, meta: RpcEnvelopeMeta, error: unknown): RpcFailure {
const message = error instanceof Error ? error.message : String(error)
if (
error instanceof Error &&
'code' in error &&
typeof (error as { code: unknown }).code === 'string' &&
COMPUTER_PASSTHROUGH_CODES.has((error as { code: string }).code)
) {
const code = (error as { code: string }).code
return errorResponse(id, meta, code, message, computerErrorData(code))
}
if (
error instanceof Error &&
'code' in error &&
typeof (error as { code: unknown }).code === 'string' &&
(error as { code: string }).code.startsWith('LINEAGE_')
) {
return errorResponse(
id,
meta,
(error as { code: string }).code,
message,
(error as { data?: unknown }).data
)
}
if (
error instanceof Error &&
'code' in error &&
typeof (error as { code: unknown }).code === 'string' &&
LINEAR_PASSTHROUGH_CODES.has((error as { code: string }).code)
) {
return errorResponse(
id,
meta,
(error as { code: string }).code,
message,
(error as { data?: unknown }).data
)
}
if (
error instanceof Error &&
'code' in error &&
typeof (error as { code: unknown }).code === 'string' &&
STRUCTURED_RUNTIME_PASSTHROUGH_CODES.has((error as { code: string }).code)
) {
return errorResponse(
id,
meta,
(error as { code: string }).code,
message,
(error as { data?: unknown }).data
)
}
if (RUNTIME_PASSTHROUGH_CODES.has(message)) {
return errorResponse(id, meta, message, message)
}
if (message === 'invalid_terminal_send') {
return errorResponse(id, meta, 'invalid_argument', 'Missing terminal send payload')
}
return errorResponse(id, meta, 'runtime_error', message)
}
export const computerErrorData = computerUseErrorRecoveryData
// Why: browser errors carry a structured .code property (BrowserError from
// cdp-bridge.ts) that maps directly to agent-facing error codes. We forward
// that code rather than falling back to the runtime allowlist, because the
// browser surface area uses its own code namespace (browser_no_tab, etc.).
export function mapBrowserError(id: string, meta: RpcEnvelopeMeta, error: unknown): RpcFailure {
if (
error instanceof Error &&
'code' in error &&
typeof (error as { code: unknown }).code === 'string'
) {
return errorResponse(id, meta, (error as { code: string }).code, error.message)
}
return mapRuntimeError(id, meta, error)
}
// Why: same as browser — emulator errors (EmulatorError) carry .code (emulator_no_active etc.)
// so we forward the structured code instead of generic runtime_error.
export function mapEmulatorError(id: string, meta: RpcEnvelopeMeta, error: unknown): RpcFailure {
if (
error instanceof Error &&
'code' in error &&
typeof (error as { code: unknown }).code === 'string'
) {
return errorResponse(id, meta, (error as { code: string }).code, error.message)
}
return mapRuntimeError(id, meta, error)
}