From 8e9690468a82ec7a65ed1269a749d52a6e86e4bb Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:27:39 -0700 Subject: [PATCH] fix(orcad): ship computer use with the server and say when it is unavailable (#31958) * fix(orcad): ship computer use with the server and say when it is unavailable * test(computer): drop the type assertion in the sidecar app-environment mock * fix(cli): carry the server reprovision hint into --json computer failures --- .../build-plugins/main-build-entry-points.ts | 2 +- config/knip.json | 2 +- config/scripts/build-orcad.mjs | 5 ++ config/scripts/orcad-entry-build.mjs | 3 +- native/computer-use-linux/runtime.py | 9 +++- src/cli/cli-error.ts | 26 ++++++++- src/cli/format-recovery.test.ts | 35 ++++++++++++ ...puter-provider-unavailable-message.test.ts | 7 +++ .../computer-provider-unavailable-message.ts | 4 +- .../{sidecar-entry.ts => computer-sidecar.ts} | 0 .../desktop-script-provider-bridge.test.ts | 14 +++++ .../desktop-script-provider-bridge.ts | 11 +++- .../desktop-script-provider-paths.test.ts | 26 +++++++++ .../computer/desktop-script-provider-paths.ts | 4 +- src/main/computer/sidecar-client.test.ts | 53 +++++++++++++++++-- src/main/computer/sidecar-client.ts | 37 ++++++------- ...decar-provider-reaping.integration.test.ts | 2 +- src/shared/computer-use-error-recovery.ts | 7 +++ src/shared/orcad-artifacts.ts | 10 ++++ 19 files changed, 222 insertions(+), 35 deletions(-) rename src/main/computer/{sidecar-entry.ts => computer-sidecar.ts} (100%) create mode 100644 src/main/computer/desktop-script-provider-paths.test.ts diff --git a/config/build-plugins/main-build-entry-points.ts b/config/build-plugins/main-build-entry-points.ts index f190b71313d..35375ee9d93 100644 --- a/config/build-plugins/main-build-entry-points.ts +++ b/config/build-plugins/main-build-entry-points.ts @@ -10,7 +10,7 @@ export const MAIN_BUILD_ENTRY_POINTS: Record = { 'doc-preview-link-preload': resolve('src/preload/doc-preview-link.ts'), 'daemon-entry': resolve('src/main/daemon/daemon-entry.ts'), 'plugin-host-entry': resolve('src/main/plugins/plugin-host-entry.ts'), - 'computer-sidecar': resolve('src/main/computer/sidecar-entry.ts'), + 'computer-sidecar': resolve('src/main/computer/computer-sidecar.ts'), 'stt-worker': resolve('src/main/speech/stt-worker.ts'), 'warp-theme-parser-worker': resolve('src/main/warp-themes/warp-theme-parser-worker.ts'), 'foreign-sqlite-reader-entry': resolve( diff --git a/config/knip.json b/config/knip.json index 596bb7812b6..a18c838b60f 100644 --- a/config/knip.json +++ b/config/knip.json @@ -6,7 +6,7 @@ "src/preload/browser-window-close.ts", "src/main/daemon/daemon-entry.ts", "src/main/plugins/plugin-host-entry.ts", - "src/main/computer/sidecar-entry.ts", + "src/main/computer/computer-sidecar.ts", "src/main/speech/stt-worker.ts", "src/main/warp-themes/warp-theme-parser-worker.ts", "src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry.ts", diff --git a/config/scripts/build-orcad.mjs b/config/scripts/build-orcad.mjs index 057636d8a11..d40d8e13fdc 100644 --- a/config/scripts/build-orcad.mjs +++ b/config/scripts/build-orcad.mjs @@ -40,6 +40,7 @@ import { ORCAD_EMOJI_SHORTCODE_DATASET, ORCAD_CLI_ENTRY_FILENAME, ORCAD_CLI_PACKAGE_FILENAME, + ORCAD_COMPUTER_USE_RUNTIME_ARTIFACTS, ORCAD_LAUNCHER_FILENAME, ORCAD_SERVER_ENTRY_FILENAME, ORCAD_NODE_PTY_DIR, @@ -210,6 +211,10 @@ cpSync(join(ROOT, 'resources', 'licenses', 'ripgrep'), join(OUT_DIR, 'ripgrep', cpSync(join(ROOT, 'resources', 'native-chat-visuals'), join(OUT_DIR, 'native-chat-visuals'), { recursive: true }) +for (const artifact of ORCAD_COMPUTER_USE_RUNTIME_ARTIFACTS) { + mkdirSync(dirname(join(OUT_DIR, artifact)), { recursive: true }) + copyFileSync(join(ROOT, 'native', ...artifact.split('/')), join(OUT_DIR, ...artifact.split('/'))) +} stageOrcadMobileWebBundle(OUT_DIR) /** Why one call per child and not one `outdir` build: esbuild mirrors each entry's source diff --git a/config/scripts/orcad-entry-build.mjs b/config/scripts/orcad-entry-build.mjs index 20be80b2d50..b0620abc439 100644 --- a/config/scripts/orcad-entry-build.mjs +++ b/config/scripts/orcad-entry-build.mjs @@ -21,7 +21,8 @@ export const ORCAD_CHILD_ENTRY_POINTS = { workerThreadChildProcess: 'src/main/worker-thread-child-process/worker-thread-child-process-entry.ts', claudeProfileSetup: 'src/main/claude-accounts/claude-profile-setup-worker-entry.ts', - sttWorker: 'src/main/speech/stt-worker.ts' + sttWorker: 'src/main/speech/stt-worker.ts', + computerSidecar: 'src/main/computer/computer-sidecar.ts' } /** Each child ships flat beside orcad.js under its source basename; the runtime resolvers look there. */ diff --git a/native/computer-use-linux/runtime.py b/native/computer-use-linux/runtime.py index 464235b206a..903289b2696 100644 --- a/native/computer-use-linux/runtime.py +++ b/native/computer-use-linux/runtime.py @@ -18,9 +18,14 @@ import time import uuid from dataclasses import dataclass -import gi +try: + import gi -gi.require_version("Atspi", "2.0") + gi.require_version("Atspi", "2.0") +except (ImportError, ValueError) as error: + # Why name it: PATH often puts a pyenv/Nix python3 without gi ahead of the system one. + sys.stderr.write(f"PyGObject unavailable in interpreter: {sys.executable}\n{error}\n") + sys.exit(1) try: gi.require_version("Gdk", "3.0") gi.require_version("GdkPixbuf", "2.0") diff --git a/src/cli/cli-error.ts b/src/cli/cli-error.ts index e76dcf4d71e..15491801247 100644 --- a/src/cli/cli-error.ts +++ b/src/cli/cli-error.ts @@ -84,6 +84,10 @@ export function formatCliError(error: unknown, context: CliErrorContext = {}): s if (conflict) { return formatMessageWithNextSteps(stripAutomationOwnerConflictCode(message), conflict.nextSteps) } + const sidecarExit = computerSidecarExitRecovery(error, context) + if (sidecarExit) { + return formatMessageWithNextSteps(message, sidecarExit.nextSteps) + } if (error instanceof RuntimeClientError) { const nextSteps = nextStepsFromData(error.data) if (nextSteps.length > 0) { @@ -120,7 +124,10 @@ export function reportCliError(error: unknown, json: boolean, context: CliErrorC const selector = contextRecovery(errorCode(error), context) if (json) { if (error instanceof RuntimeRpcFailureError) { - const response = withAutomationOwnerConflictRecovery(error.response) + const sidecarExit = computerSidecarExitRecovery(error, context) + const response = sidecarExit + ? { ...error.response, error: { ...error.response.error, data: sidecarExit } } + : withAutomationOwnerConflictRecovery(error.response) console.log( JSON.stringify( selector @@ -217,7 +224,24 @@ function nextStepsFromData(data: unknown): string[] { return [] } +/** Read here, not from the error's data: the servers that send this predate the hint (#31777). */ +function computerSidecarExitRecovery(error: unknown, context: CliErrorContext) { + if ( + context.commandPath?.[0] !== 'computer' || + errorCode(error) !== 'accessibility_error' || + !(error instanceof Error) || + !error.message.startsWith('computer sidecar exited with code') + ) { + return undefined + } + return computerUseErrorRecoveryData('accessibility_error', error.message) +} + function localCliErrorData(error: unknown, context: CliErrorContext): unknown { + const sidecarExit = computerSidecarExitRecovery(error, context) + if (sidecarExit) { + return sidecarExit + } const selector = contextRecovery(errorCode(error), context) // Why: error-specific recovery must win over the generic computer fallback. if (error instanceof RuntimeClientError && error.data !== undefined) { diff --git a/src/cli/format-recovery.test.ts b/src/cli/format-recovery.test.ts index 055d0a23cd3..a448f0f074c 100644 --- a/src/cli/format-recovery.test.ts +++ b/src/cli/format-recovery.test.ts @@ -217,6 +217,41 @@ describe('CLI error recovery', () => { expect(output).toContain('Fix the command flags or RPC params') }) + it('tells an older server to reprovision when its computer sidecar is missing', () => { + // An older orcad answers with the generic provider steps, which never name the server install. + const error = new RuntimeRpcFailureError({ + id: 'req_sidecar_exit', + ok: false, + error: { + code: 'accessibility_error', + message: 'computer sidecar exited with code 1', + data: { + nextSteps: [ + 'Run `orca computer capabilities --json` to confirm the provider is available before retrying.' + ] + } + }, + _meta: { runtimeId: 'runtime_orcad' } + }) + + const output = formatCliError(error, { commandPath: ['computer', 'list-apps'] }) + expect(output).toContain('update or reprovision the server') + + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + reportCliError(error, true, { commandPath: ['computer', 'list-apps'] }) + expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({ + error: { + code: 'accessibility_error', + data: { + nextSteps: expect.arrayContaining([ + expect.stringContaining('update or reprovision the server') + ]) + } + } + }) + log.mockRestore() + }) + it('does not replace mutation recovery with generic runtime startup advice', () => { const error = new RuntimeClientError( 'runtime_unavailable', diff --git a/src/main/computer/computer-provider-unavailable-message.test.ts b/src/main/computer/computer-provider-unavailable-message.test.ts index d3255eb8a91..4cd4710ea22 100644 --- a/src/main/computer/computer-provider-unavailable-message.test.ts +++ b/src/main/computer/computer-provider-unavailable-message.test.ts @@ -11,6 +11,13 @@ describe('computerProviderUnavailableMessage', () => { ) }) + it('names the runtime file a Linux or Windows install is missing', () => { + expect(computerProviderUnavailableMessage('linux')).toContain('computer-use-linux/runtime.py') + expect(computerProviderUnavailableMessage('win32')).toContain( + 'computer-use-windows/runtime.ps1' + ) + }) + it('keeps unsupported platforms explicit', () => { expect(computerProviderUnavailableMessage('freebsd')).toBe( 'computer-use has no native provider for freebsd' diff --git a/src/main/computer/computer-provider-unavailable-message.ts b/src/main/computer/computer-provider-unavailable-message.ts index 9255d8ca91e..9c25cee876d 100644 --- a/src/main/computer/computer-provider-unavailable-message.ts +++ b/src/main/computer/computer-provider-unavailable-message.ts @@ -6,7 +6,9 @@ export function computerProviderUnavailableMessage(platform: NodeJS.Platform): s ].join(' ') } if (platform === 'linux' || platform === 'win32') { - return `computer-use has no native provider for ${platform}; the platform runtime file was not found` + const runtime = + platform === 'linux' ? 'computer-use-linux/runtime.py' : 'computer-use-windows/runtime.ps1' + return `Computer use isn't available on this host: its Orca install has no ${runtime}. On an Orca server installed before computer use shipped with it, update or reprovision the server.` } return `computer-use has no native provider for ${platform}` } diff --git a/src/main/computer/sidecar-entry.ts b/src/main/computer/computer-sidecar.ts similarity index 100% rename from src/main/computer/sidecar-entry.ts rename to src/main/computer/computer-sidecar.ts diff --git a/src/main/computer/desktop-script-provider-bridge.test.ts b/src/main/computer/desktop-script-provider-bridge.test.ts index fac67f47d91..ddfecf738db 100644 --- a/src/main/computer/desktop-script-provider-bridge.test.ts +++ b/src/main/computer/desktop-script-provider-bridge.test.ts @@ -18,4 +18,18 @@ describe('mapBridgeError', () => { expect(mapBridgeError('unknown element_index').code).toBe('element_not_found') expect(mapBridgeError('element metadata unavailable').code).toBe('accessibility_error') }) + + it('names the python3 that could not load PyGObject', () => { + const error = mapBridgeError( + "PyGObject unavailable in interpreter: /home/me/.pyenv/shims/python3\nNo module named 'gi'" + ) + expect(error.code).toBe('unsupported_capability') + expect(error.message).toContain('(/home/me/.pyenv/shims/python3)') + }) + + it('says computer use is unavailable when no python3 is on PATH', () => { + const error = mapBridgeError('spawn python3 ENOENT') + expect(error.code).toBe('unsupported_capability') + expect(error.message).toContain("Computer use isn't available on this host") + }) }) diff --git a/src/main/computer/desktop-script-provider-bridge.ts b/src/main/computer/desktop-script-provider-bridge.ts index 3dc08eb9bc1..08c4af353eb 100644 --- a/src/main/computer/desktop-script-provider-bridge.ts +++ b/src/main/computer/desktop-script-provider-bridge.ts @@ -195,9 +195,18 @@ export function mapBridgeError(message: string): RuntimeClientError { return new RuntimeClientError('invalid_argument', text) } if (/ModuleNotFoundError: No module named 'gi'|PyGObject|python3-gi/i.test(text)) { + const interpreter = /PyGObject unavailable in interpreter: (.+)/.exec(text)?.[1]?.trim() return new RuntimeClientError( 'unsupported_capability', - 'Linux Computer Use requires python3-gi and AT-SPI packages. Install python3-gi gir1.2-atspi-2.0 at-spi2-core, then retry.' + interpreter + ? `Linux Computer Use requires python3-gi and AT-SPI packages, which the python3 it ran (${interpreter}) cannot load. Install python3-gi gir1.2-atspi-2.0 at-spi2-core for that interpreter, or put a python3 that has them (usually /usr/bin/python3) first on PATH, then retry.` + : 'Linux Computer Use requires python3-gi and AT-SPI packages. Install python3-gi gir1.2-atspi-2.0 at-spi2-core, then retry.' + ) + } + if (/spawn python3 ENOENT/.test(text)) { + return new RuntimeClientError( + 'unsupported_capability', + "Computer use isn't available on this host: Linux Computer Use runs python3, and none is on PATH. Install python3 with python3-gi gir1.2-atspi-2.0 at-spi2-core, then retry." ) } if (/not a valid secondary action|action.*not supported/i.test(text)) { diff --git a/src/main/computer/desktop-script-provider-paths.test.ts b/src/main/computer/desktop-script-provider-paths.test.ts new file mode 100644 index 00000000000..2b79dc2910c --- /dev/null +++ b/src/main/computer/desktop-script-provider-paths.test.ts @@ -0,0 +1,26 @@ +import type * as NodeFs from 'node:fs' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' + +const { existsSyncMock } = vi.hoisted(() => ({ existsSyncMock: vi.fn((_path: string) => false) })) + +vi.mock('node:fs', async (importOriginal) => ({ + ...(await importOriginal()), + existsSync: existsSyncMock +})) + +import { resolveDesktopScriptProviderPath } from './desktop-script-provider-paths' + +describe('resolveDesktopScriptProviderPath', () => { + afterEach(() => { + existsSyncMock.mockReset() + }) + + it('finds the runtime orcad ships beside its sidecar when there is no resourcesPath', () => { + // orcad's sidecar bundle is flat at the install root, so its runtimes sit beside it. + const bundled = join(__dirname, 'computer-use-linux', 'runtime.py') + existsSyncMock.mockImplementation((path) => path === bundled) + + expect(resolveDesktopScriptProviderPath('linux')).toBe(bundled) + }) +}) diff --git a/src/main/computer/desktop-script-provider-paths.ts b/src/main/computer/desktop-script-provider-paths.ts index 9de0f23b0f1..dad4e930149 100644 --- a/src/main/computer/desktop-script-provider-paths.ts +++ b/src/main/computer/desktop-script-provider-paths.ts @@ -29,11 +29,13 @@ export function resolveDesktopScriptProviderPath( const sourceDirectory = platform === 'windows' ? 'native/computer-use-windows' : 'native/computer-use-linux' const packaged = [join(process.resourcesPath ?? '', directory, filename)] + // orcad has no resourcesPath; its bundled sidecar (this module's __dirname) holds the runtimes. + const bundled = join(__dirname, directory, filename) const dev = [ join(process.cwd(), sourceDirectory, filename), resolve(__dirname, '../../', sourceDirectory, filename) ] - const candidates = process.resourcesPath ? [...packaged, ...dev] : dev + const candidates = process.resourcesPath ? [...packaged, bundled, ...dev] : [bundled, ...dev] return candidates.find((candidate) => candidate && existsSync(candidate)) ?? null } diff --git a/src/main/computer/sidecar-client.test.ts b/src/main/computer/sidecar-client.test.ts index fb3b28bf444..11ca60c3a5c 100644 --- a/src/main/computer/sidecar-client.test.ts +++ b/src/main/computer/sidecar-client.test.ts @@ -1,4 +1,6 @@ +import type * as NodeFs from 'node:fs' import { EventEmitter } from 'node:events' +import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS, @@ -11,14 +13,31 @@ import { resetComputerSidecarForTest } from './sidecar-client' -const { forkMock } = vi.hoisted(() => ({ - forkMock: vi.fn() -})) +type TestAppEnvironment = { getAppPath(): string; isPackaged(): boolean } + +const { forkMock, existsSyncMock, appEnvironment } = vi.hoisted(() => { + const environment: { current: TestAppEnvironment | null } = { current: null } + return { + forkMock: vi.fn(), + existsSyncMock: vi.fn((_path: string) => true), + appEnvironment: environment + } +}) vi.mock('child_process', () => ({ fork: forkMock })) +vi.mock('node:fs', async (importOriginal) => ({ + ...(await importOriginal()), + existsSync: existsSyncMock +})) + +vi.mock('../../shared/app-environment', () => ({ + hasAppEnvironment: () => appEnvironment.current !== null, + getAppEnvironment: () => appEnvironment.current +})) + type SentRequest = { id: number method: string @@ -72,9 +91,37 @@ describe('computer sidecar client', () => { afterEach(() => { resetComputerSidecarForTest() forkMock.mockReset() + existsSyncMock.mockImplementation(() => true) + appEnvironment.current = null vi.useRealTimers() }) + it('forks the sidecar orcad ships beside orcad.js, not a desktop out/main path', async () => { + appEnvironment.current = { getAppPath: () => '/srv/orcad', isPackaged: () => true } + existsSyncMock.mockImplementation((path) => path === join('/srv/orcad', 'computer-sidecar.js')) + + const call = callComputerSidecarCapabilities() + children[0]!.emit('message', { id: children[0]!.sent[0]!.id, ok: true, result: {} }) + + await expect(call).resolves.toEqual({}) + expect(forkMock).toHaveBeenCalledWith( + join('/srv/orcad', 'computer-sidecar.js'), + [], + expect.anything() + ) + }) + + it('says computer use is unavailable when an older server install has no sidecar', async () => { + appEnvironment.current = { getAppPath: () => '/srv/orcad', isPackaged: () => true } + existsSyncMock.mockImplementation(() => false) + + await expect(callComputerSidecarCapabilities()).rejects.toMatchObject({ + code: 'unsupported_capability', + message: expect.stringContaining("Computer use isn't available on this host") + }) + expect(forkMock).not.toHaveBeenCalled() + }) + it('ignores stale child exit and error after a replacement sidecar starts', async () => { const firstCall = callComputerSidecarCapabilities() const firstRejection = expect(firstCall).rejects.toThrow( diff --git a/src/main/computer/sidecar-client.ts b/src/main/computer/sidecar-client.ts index 23af1aa603b..a56ba95a833 100644 --- a/src/main/computer/sidecar-client.ts +++ b/src/main/computer/sidecar-client.ts @@ -1,6 +1,5 @@ import { fork, type ChildProcess } from 'node:child_process' -import { getAppEnvironment, hasAppEnvironment } from '../../shared/app-environment' -import { join } from 'node:path' +import { existsSync } from 'node:fs' import type { ComputerActionResult, ComputerListAppsResult, @@ -12,6 +11,10 @@ import { normalizeComputerActionResult } from './computer-action-verification-no import { isComputerSidecarDiagnostic, logComputerDiagnostic } from './computer-sidecar-diagnostics' import { validateComputerSidecarPasteText } from './computer-sidecar-paste-validation' import { RuntimeClientError } from './runtime-client-error' +import { getForkedChildEntryPath } from '../forked-child-entry-path' + +// orcad ships it beside orcad.js (ORCAD_CHILD_ENTRY_POINTS); the desktop under out/main. +export const COMPUTER_SIDECAR_ENTRY_FILENAME = 'computer-sidecar.js' type ComputerSidecarMethod = | 'capabilities' @@ -94,30 +97,13 @@ export function resetComputerSidecarForTest(): void { function getComputerSidecar(): ComputerSidecarProcess { if (!sidecar) { - sidecar = new ComputerSidecarProcess(getComputerSidecarEntryPath()) + sidecar = new ComputerSidecarProcess(getForkedChildEntryPath(COMPUTER_SIDECAR_ENTRY_FILENAME)) } return sidecar } -function getComputerSidecarEntryPath(): string { - const app = loadElectronApp() - const appPath = app?.getAppPath() ?? process.cwd() - const isPackaged = app?.isPackaged ?? false - // Why: packaged sidecars must be forked from app.asar.unpacked because - // ELECTRON_RUN_AS_NODE bypasses Electron's asar require integration. - const basePath = isPackaged ? appPath.replace('app.asar', 'app.asar.unpacked') : appPath - return join(basePath, 'out', 'main', 'computer-sidecar.js') -} - -// Why the port and not require('electron'): the literal text fails the plain-Node -// entry guard even inside a try/catch, and hasAppEnvironment() gives the same -// "no app root here" answer without it. -function loadElectronApp(): { getAppPath(): string; isPackaged: boolean } | null { - if (!hasAppEnvironment()) { - return null - } - const environment = getAppEnvironment() - return { getAppPath: () => environment.getAppPath(), isPackaged: environment.isPackaged() } +export function computerSidecarMissingMessage(entryPath: string): string { + return `Computer use isn't available on this host: its Orca install has no computer-use sidecar (${entryPath}). On an Orca server installed before computer use shipped with it, update or reprovision the server.` } class ComputerSidecarProcess { @@ -211,6 +197,13 @@ class ComputerSidecarProcess { } this.cleanupActiveChildListeners() this.child = null + // Why before fork: a missing entry only surfaces as "exited with code 1", which names nothing. + if (!existsSync(this.entryPath)) { + throw new RuntimeClientError( + 'unsupported_capability', + computerSidecarMissingMessage(this.entryPath) + ) + } const child = fork(this.entryPath, [], { stdio: ['ignore', 'pipe', 'pipe', 'ipc'], diff --git a/src/main/computer/sidecar-provider-reaping.integration.test.ts b/src/main/computer/sidecar-provider-reaping.integration.test.ts index 3ed45c4de62..0b5598081ca 100644 --- a/src/main/computer/sidecar-provider-reaping.integration.test.ts +++ b/src/main/computer/sidecar-provider-reaping.integration.test.ts @@ -14,7 +14,7 @@ describe.skipIf(process.platform === 'win32')('real sidecar exit reaping', () => directory = await mkdtemp(join(tmpdir(), 'orca-sidecar-reaping-')) entry = join(directory, 'sidecar.cjs') await build({ - entryPoints: [join(__dirname, 'sidecar-entry.ts')], + entryPoints: [join(__dirname, 'computer-sidecar.ts')], outfile: entry, bundle: true, platform: 'node', diff --git a/src/shared/computer-use-error-recovery.ts b/src/shared/computer-use-error-recovery.ts index 7803738fb1a..b30d08f07dc 100644 --- a/src/shared/computer-use-error-recovery.ts +++ b/src/shared/computer-use-error-recovery.ts @@ -92,6 +92,13 @@ export function computerUseErrorRecoveryData( 'If the message mentions the payload cap, target a smaller/current window or use `--no-screenshot`.' ) case 'accessibility_error': + // Why: an Orca server installed before it bundled computer use reports only this exit (#31777). + if (message?.startsWith('computer sidecar exited with code')) { + return recoverWith( + 'If this targets an Orca server (`orca serve`, SSH or WSL), its install may not include computer use: update or reprovision the server, then retry.', + 'Run `orca computer capabilities --json` to confirm the provider is available before retrying.' + ) + } return recoverWith( 'Run `orca computer capabilities --json` to confirm the provider is available before retrying.', 'If the message mentions permissions, run `orca computer permissions --id accessibility --json` and grant access.', diff --git a/src/shared/orcad-artifacts.ts b/src/shared/orcad-artifacts.ts index 63271c86b9d..17465851301 100644 --- a/src/shared/orcad-artifacts.ts +++ b/src/shared/orcad-artifacts.ts @@ -120,6 +120,13 @@ export const ORCAD_FOREIGN_SQLITE_READER_ENTRY = 'foreign-sqlite-reader-entry.js export const ORCAD_PORT_SCAN_COMMAND_WORKER_ENTRY = 'port-scan-command-worker-entry.js' // Equals AI_VAULT_SERVICE_ENTRY_FILENAME: the forked child that lists agent sessions. export const ORCAD_SESSION_SCANNER_SERVICE_ENTRY = 'session-scanner-service-entry.js' +// Equals COMPUTER_SIDECAR_ENTRY_FILENAME, and the desktop's out/main entry name. +export const ORCAD_COMPUTER_SIDECAR_ENTRY = 'computer-sidecar.js' +/** Plain scripts, one copy for every target: each platform's sidecar runs only its own. */ +export const ORCAD_COMPUTER_USE_RUNTIME_ARTIFACTS = [ + 'computer-use-linux/runtime.py', + 'computer-use-windows/runtime.ps1' +] as const // Kept here because build-orcad.mjs imports this manifest directly under Node type stripping. export const ORCAD_RIPGREP_ARTIFACTS = [ @@ -248,6 +255,9 @@ export const ORCAD_ARTIFACTS: readonly OrcadArtifact[] = [ { filename: 'claude-profile-setup-worker-entry.js' }, // Dictation's worker thread; it loads the recognizer `orca serve` names (#31174). { filename: 'stt-worker.js' }, + // `orca computer` forks it; it finds the platform runtimes below beside itself (#31777). + { filename: ORCAD_COMPUTER_SIDECAR_ENTRY }, + ...ORCAD_COMPUTER_USE_RUNTIME_ARTIFACTS.map((filename) => ({ filename })), // Target-specific even when the JavaScript bundle is shared across packaged slots. { filename: ORCAD_SERVER_TARGET_FILENAME }, // orcad never depends on a host runtime or host-installed native module.