feat(acp): chat agents can declare a managed runtime that a start runs but never installs (#28460)

* feat(acp): ACP agents can declare a managed runtime that a start runs but never installs

A start resolves PATH and the agent's install folders first, then the newest whole release under
Orca's data folder (its completion receipt and member sizes checked); with none it refuses with
runtimeNotInstalled instead of downloading.

* fix(runtime): tell an unreadable runtime release from a broken one, and point a not-installed chat at the chat itself

A transient read error (EIO, EMFILE) no longer counts a working release as incomplete; only
definite absence or a mismatch does. The not-installed refusal no longer sends a remote chat's
user to this computer's Settings.

* fix(acp): a start runs Orca's installed release before a copy on PATH, and a copy elsewhere only beside every file it needs
This commit is contained in:
Brennan Benson
2026-10-15 14:17:32 -07:00
committed by GitHub
parent 36e36f2dd6
commit 76735edf31
29 changed files with 352 additions and 26 deletions
+4
View File
@@ -15,6 +15,7 @@ import { isStableCliVersionFrom, isStableCliVersionOnLine } from '../agent-cli-v
import type { TuiAgent } from '../../shared/tui-agent'
import type { AgentSessionModelOption } from '../../shared/agent-session-wire'
import type { InitializeResponse } from './generated/acp-protocol.generated'
import type { ManagedRuntime } from '../node-runtime/managed-runtime-release'
import { readGrokModelCatalog } from './acp-dialects/grok-model-catalog'
import {
OPENCODE_MODEL_LISTING_ARGS,
@@ -88,6 +89,9 @@ export type AcpLaunchSpec = {
supportsVersion?(version: string): boolean
/** Where the agent installs its own binary, searched after PATH. */
installDirectories(input: { env: Readonly<Record<string, string>>; homePath: string }): string[]
/** A pinned copy Orca installs under its data folder when the person asks, searched last; a
* start never installs it. A spec with one names no `supportsVersion`. */
managedRuntime?: ManagedRuntime
/** Images go to the agent when it also advertises them; off sends text prompts only. */
imagePrompts?: true
/** The agent compacts its conversation when sent `/compact` as a prompt; off hides `/compact`. */
@@ -69,6 +69,7 @@ function scriptedGrok(initializeMeta: Record<string, unknown>): {
closed: () => closes,
deps: {
resolveEnvironment: async () => ({ PATH: '/usr/bin', HOME: '/home/user' }),
dataDirectory: '/orca-data',
resolveLaunchEnv: () => ({ XAI_API_KEY: 'from-settings' }),
resolveCommand: () => '/opt/grok/bin/grok',
homePath: '/home/user',
@@ -155,6 +156,7 @@ describe('ACP model catalog probes', () => {
const connect = vi.fn()
const probe = createAcpModelCatalogProbe(OPENCODE, {
resolveEnvironment: async () => ({ PATH: '/usr/bin', HOME: '/home/user' }),
dataDirectory: '/orca-data',
resolveCommand: () => '/opt/opencode/bin/opencode',
probeVersion: async () => true,
homePath: '/home/user',
@@ -180,6 +182,7 @@ describe('ACP model catalog probes', () => {
const runListing = vi.fn()
const probe = createAcpModelCatalogProbe(OPENCODE, {
resolveEnvironment: async () => ({ PATH: '/usr/bin' }),
dataDirectory: '/orca-data',
resolveCommand: () => '/opt/opencode/bin/opencode',
probeVersion: async () => false,
runListing
+2 -7
View File
@@ -27,7 +27,7 @@ import type { AcpStructuredConnection } from './acp-structured-connection'
import { ACP_HANDLE_TRANSPORT, acpAgentName } from './acp-structured-agent-definitions'
import { AcpStructuredLane, acpLaneChildWorkDelivery } from './acp-structured-lane'
import { NO_ACP_CHILD_STOPS, probeAcpChildStops } from './acp-structured-child-stop'
import type { AcpStructuredLaunch } from './acp-structured-launch-resolution'
import { prepareAcpStructuredLaunch } from './acp-structured-launch-preparation'
import { AcpStructuredOptions, restoreAcpSessionOptions } from './acp-structured-options'
import { AcpStructuredPrompts } from './acp-structured-prompts'
import {
@@ -77,12 +77,7 @@ export async function acquireAcpStructuredSession(input: {
}
}
closedBeforeSpawn()
const launch: AcpStructuredLaunch = await deps
.resolveLaunch({ identity: acquire.identity, spawnToken: acquire.spawnToken })
.catch((error: unknown) => {
throw new AgentSessionPreSpawnError(error)
})
closedBeforeSpawn()
const launch = await prepareAcpStructuredLaunch(deps, acquire, closedBeforeSpawn)
let session: AcpStructuredSession | null = null
// A slot rather than a `let`: closures read it, and control-flow narrowing cannot see them write.
const slot: { lane: AcpStructuredLane | null; reattaching: boolean } = {
@@ -62,6 +62,7 @@ afterEach(async () => {
function grokProbe() {
return createAcpModelCatalogProbe(GROK, {
resolveEnvironment: async () => ({ PATH: '/usr/bin' }),
dataDirectory: '/orca-data',
resolveCommand: () => '/opt/grok/bin/grok',
homePath: '/home/user',
connect: () => {
@@ -0,0 +1,32 @@
// What a start checks on this host before it spawns the agent. A runtime Orca manages is installed
// only when the person asks, so a start that finds no copy refuses rather than downloading one.
import {
AgentSessionPreSpawnError,
type StructuredAgentSessionAcquireInput
} from '../native-chat/agent-session-wire/structured-agent-session-adapter'
import type { AcpStructuredLaunch } from './acp-structured-launch-resolution'
import type { AcpStructuredSessionAdapterDeps } from './acp-structured-session-adapter-deps'
/** The launch a start spawns; `closedBeforeSpawn` throws once the start was abandoned. */
export async function prepareAcpStructuredLaunch(
deps: Pick<AcpStructuredSessionAdapterDeps, 'spec' | 'resolveLaunch'>,
acquire: Pick<StructuredAgentSessionAcquireInput, 'identity' | 'spawnToken'>,
closedBeforeSpawn: () => void
): Promise<AcpStructuredLaunch> {
const { spec } = deps
const launch = await deps
.resolveLaunch({ identity: acquire.identity, spawnToken: acquire.spawnToken })
.catch((error: unknown) => {
throw new AgentSessionPreSpawnError(error)
})
closedBeforeSpawn()
// A missed lookup returns the bare name; a selected Command setting resolves or refuses.
if (spec.managedRuntime && launch.command === spec.command) {
throw new AgentSessionPreSpawnError(
new Error(`${spec.managedRuntime.label} is not installed on this host`),
{ reason: 'runtimeNotInstalled' }
)
}
return launch
}
@@ -21,6 +21,11 @@ import {
type StructuredAgentCommandSettings
} from '../native-chat/structured-agent-command-resolution'
import { probeAgentCliVersion } from '../agent-cli-version-probe'
import {
copyBesidePinnedMembers,
resolveInstalledRuntimeRelease
} from '../node-runtime/managed-runtime-release'
import { hasExplicitTuiLaunchCommand } from '../../shared/tui-agent-launch-command-override'
import { readAgentJournalTurn } from '../../shared/agent-session-turn-record'
import type { JournalLoad } from '../native-chat/agent-session-journal/journal-open'
import {
@@ -73,6 +78,8 @@ export type AcpStructuredLaunchPartDeps = {
/** The environment the child process inherits at spawn; this process's own by default. */
inheritedEnv?: NodeJS.ProcessEnv
probeVersion?: typeof probeAgentCliVersion
/** Orca's data folder on this host, which holds the spec's managed runtime. */
dataDirectory: string
} & Pick<AcpLaunchVisualsDeps, 'logger'>
/** What a launch's binary and environment are read from; a catalog probe reads the same. */
@@ -87,11 +94,16 @@ export type AcpLaunchInvocationDeps = {
refreshEnvironmentAfterMiss?: (askedAt: number) => Promise<void>
/** Where the program starts, for a PATH entry relative to it; Orca's own when absent. */
resolveWorkingDirectory?: () => Promise<string> | string
} & Pick<AcpStructuredLaunchPartDeps, 'resolveCommand' | 'homePath' | 'inheritedEnv'>
} & Pick<
AcpStructuredLaunchPartDeps,
'resolveCommand' | 'homePath' | 'inheritedEnv' | 'dataDirectory'
>
/** The binary a launch with `env` spawns: the user's Command setting when set, else the stock
* binary from PATH, then the agent's own install directories. A create's version check resolves
* through here too, so it asks about the same file. */
* binary from PATH, then the agent's own install directories. A spec with a managed runtime runs
* its installed release before those, and a copy from them only beside every pinned member; the
* bare name when none qualifies. A create's version check resolves through here too, so it asks
* about the same file. A null `dataDirectory` skips the installed release. */
export function resolveAcpLaunchCommand(
spec: AcpLaunchSpec,
env: Readonly<Record<string, string>>,
@@ -99,21 +111,36 @@ export function resolveAcpLaunchCommand(
commandSettings?: StructuredAgentCommandSettings
resolveCommand?: typeof resolveCliCommand
homePath?: string
} = {}
dataDirectory: string | null
}
): string {
const homePath = env.HOME ?? env.USERPROFILE ?? options.homePath ?? homedir()
const pathEnv = [env.PATH ?? env.Path, ...spec.installDirectories({ env, homePath })]
.filter((entry): entry is string => Boolean(entry))
.join(delimiter)
return resolveStructuredAgentCommand(
spec.agent,
options.commandSettings ?? {},
{ pathEnv, homePath },
{
command: spec.command,
...(options.resolveCommand ? { resolve: options.resolveCommand } : {})
}
)
const settings = options.commandSettings ?? {}
const resolve = (searchPath: string) =>
resolveStructuredAgentCommand(
spec.agent,
settings,
{ pathEnv: searchPath, homePath },
{
command: spec.command,
...(options.resolveCommand ? { resolve: options.resolveCommand } : {})
}
)
const { managedRuntime } = spec
if (!managedRuntime || hasExplicitTuiLaunchCommand(settings, spec.agent)) {
return resolve(pathEnv)
}
const pin = managedRuntime.pin()
const root = options.dataDirectory && managedRuntime.root(options.dataDirectory)
const release = root ? resolveInstalledRuntimeRelease(root, pin).release : null
if (release) {
return resolve(release.folder)
}
const found = resolve(pathEnv)
return (found !== spec.command && copyBesidePinnedMembers(found, pin)) || spec.command
}
/** Whether `command` is a release the spec runs a structured chat on (true when it names none),
@@ -202,7 +229,8 @@ export function acpStructuredLaunchPart(
resolveWorkingDirectory: basis.launchDirectory,
...(deps.resolveCommand ? { resolveCommand: deps.resolveCommand } : {}),
...(deps.homePath ? { homePath: deps.homePath } : {}),
...(deps.inheritedEnv ? { inheritedEnv: deps.inheritedEnv } : {})
...(deps.inheritedEnv ? { inheritedEnv: deps.inheritedEnv } : {}),
dataDirectory: deps.dataDirectory
}
)
const cwd = await basis.launchDirectory()
@@ -0,0 +1,80 @@
import { mkdir, mkdtemp, realpath, rm, truncate, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
import { agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture'
import { closeProviderTimelineRigs } from '../native-chat/agent-session-timeline/provider-timeline-assembler-test-support'
import { RUNTIME_RELEASE_RECEIPT } from '../node-runtime/managed-runtime-release'
import { createAcpStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support'
import { GROK, openAcpAdapterRig } from './acp-structured-adapter.test-support'
afterEach(() => closeProviderTimelineRigs())
it('starts the installed release first, else a copy beside every pinned member', async () => {
const dataDirectory = await mkdtemp(join(tmpdir(), 'orca-acp-managed-'))
const homePath = join(dataDirectory, 'home')
const command = `orca-managed-${basename(dataDirectory)}`
const root = join(dataDirectory, 'agent-runtimes', 'grok')
const pin = {
version: '1.0',
url: 'https://example.test/grok-1.0.zip',
archiveSha256: 'a'.repeat(64),
archiveBytes: 10,
members: [
{ path: command, sha256: 'b'.repeat(64), bytes: 6 },
{ path: 'harness', sha256: 'c'.repeat(64), bytes: 7 }
]
}
const spec = {
...GROK,
command,
managedRuntime: { label: 'Grok', root: () => root, pin: () => pin }
}
const spawn = async () => {
const resolveLaunch = createAcpStructuredLaunchResolver(spec, {
store: {
getRecord: () => ({
...agentSessionRecordFixture(),
provider: 'grok',
providerHandleChain: [],
accountHome: { variable: 'GROK_HOME', path: join(dataDirectory, '.grok') }
}),
pinLaunchDirectory: vi.fn()
},
readJournal: () => null,
resolveEnvironment: async () => ({ PATH: '', HOME: homePath }),
dataDirectory
})
const rig = await openAcpAdapterRig({ spec, deps: { resolveLaunch } })
await rig.acquire()
return rig.child().launch.command
}
await expect(spawn()).rejects.toMatchObject({ reason: 'runtimeNotInstalled' })
// A server without its companion beside it is no copy.
const localBin = join(homePath, '.local', 'bin')
await mkdir(localBin, { recursive: true })
await writeFile(join(localBin, command), '', { mode: 0o755 })
await expect(spawn()).rejects.toMatchObject({ reason: 'runtimeNotInstalled' })
const release = join(root, '1.0')
await mkdir(release, { recursive: true })
await writeFile(join(release, command), 'server', { mode: 0o755 })
await writeFile(join(release, 'harness'), 'harness', { mode: 0o755 })
await writeFile(
join(release, RUNTIME_RELEASE_RECEIPT),
JSON.stringify({
releaseId: pin.archiveSha256,
version: '1.0',
platform: process.platform,
arch: process.arch,
members: pin.members.map(({ path, bytes }) => ({ path, bytes }))
})
)
await writeFile(join(localBin, 'harness'), '', { mode: 0o755 })
expect(await spawn()).toBe(join(release, command))
await truncate(join(release, command), 2)
expect(await spawn()).toBe(await realpath(join(localBin, command)))
await rm(dataDirectory, { recursive: true, force: true })
})
@@ -135,6 +135,7 @@ export type AgentSessionPreSpawnReason = Extract<
| 'claudeAccountFolderMissing'
| 'claudeAccountSetupFailed'
| 'agentCommandNotRunnable'
| 'runtimeNotInstalled'
>
/** Acquisition failed with first-hand proof that no provider process existed. */
@@ -36,7 +36,8 @@ const TYPED_START_REFUSALS = [
'historyInOtherAccount',
'claudeAccountFolderMissing',
'claudeAccountSetupFailed',
'agentCommandNotRunnable'
'agentCommandNotRunnable',
'runtimeNotInstalled'
] as const satisfies readonly (AgentSessionFailureKind &
AgentSessionRefusalReason<'agent_session_operation_invalid'>)[]
@@ -410,7 +410,8 @@ describe('adopting an OpenCode CLI conversation', () => {
const launch = await resolveAcpLaunchInvocation(spec, unmanaged, {
resolveEnvironment: async () => baseEnvironment,
resolveLaunchEnv: () => launchEnv,
resolveCommand: () => '/test/opencode'
resolveCommand: () => '/test/opencode',
dataDirectory: root
})
expect(adoption.transcriptPath).toBe(configuredDb)
@@ -0,0 +1,149 @@
// The installed releases of a runtime Orca downloads for an agent: `<root>/<version>/` holding the
// pinned members and a receipt written before the folder was renamed into place. Read-only, and
// cheap enough for every launch: a full hash is only ever taken by an install.
import { lstatSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
import { basename, dirname, join } from 'node:path'
import { z } from 'zod'
import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence'
export type ManagedRuntimePin = {
version: string
url: string
archiveSha256: string
/** The decoded archive's size. */
archiveBytes: number
/** Archive-relative path, sha256 and size of every executable the release must hold. */
members: readonly { path: string; sha256: string; bytes: number }[]
}
/** A runtime Orca installs only when the person asks; a launch reads what is installed. */
export type ManagedRuntime = {
/** Shown to the person, e.g. "Antigravity". */
label: string
/** The folder that holds only this runtime's releases. */
root(dataDirectory: string): string
/** This host's pinned release; null where none is published. */
pin(): ManagedRuntimePin | null
}
export const RUNTIME_RELEASE_RECEIPT = '.install-complete.json'
const RECEIPT_MAX_BYTES = 8 * 1024
const VERSION_ORDER = new Intl.Collator(undefined, { numeric: true })
const runtimeReleaseReceiptSchema = z.object({
releaseId: z.string().regex(/^[a-f0-9]{64}$/),
version: z.string().min(1),
platform: z.string(),
arch: z.string(),
members: z.array(z.object({ path: z.string().min(1), bytes: z.number().int().positive() })).min(1)
})
export type InstalledRuntimeRelease = {
folder: string
version: string
/** The pinned release itself, rather than an older one kept runnable. */
current: boolean
}
/** What a release folder read proved: whole, definitively not (absent, mismatched or partial), or
* unknown because the read itself failed. */
type ReleaseRead = { releaseId: string } | 'broken' | 'unreadable'
export type InstalledRuntimeReleases = {
/** The release a launch runs: the pinned one when installed whole, else the newest other whole
* one; null when none is. Staging, links and anything incomplete never count. */
release: InstalledRuntimeRelease | null
/** Release folders that are definitively not whole, e.g. a member deleted or truncated. */
broken: string[]
/** Some folder could not be read, so whether it is whole is unknown. */
unreadable: boolean
}
export function resolveInstalledRuntimeRelease(
root: string,
pin: ManagedRuntimePin | null
): InstalledRuntimeReleases {
let names: string[]
try {
names = readdirSync(root)
} catch (error) {
return { release: null, broken: [], unreadable: !isDefinitiveAbsence(error) }
}
const reads = names
.filter((name) => !name.startsWith('.'))
.map((name) => ({ name, read: readRelease(join(root, name), name) }))
const releases = reads.flatMap(({ name, read }) =>
typeof read === 'object' ? [{ name, releaseId: read.releaseId }] : []
)
const pinned = releases.find(
(release) => release.name === pin?.version && release.releaseId === pin.archiveSha256
)
const chosen = pinned ?? releases.sort((a, b) => VERSION_ORDER.compare(b.name, a.name))[0]
return {
release: chosen
? { folder: join(root, chosen.name), version: chosen.name, current: chosen === pinned }
: null,
broken: reads.filter(({ read }) => read === 'broken').map(({ name }) => name),
unreadable: reads.some(({ read }) => read === 'unreadable')
}
}
/** `command`'s real path when every pinned member sits beside it, as the server expects its
* companions; null otherwise, so a lone server file found elsewhere is never started. */
export function copyBesidePinnedMembers(
command: string,
pin: ManagedRuntimePin | null
): string | null {
try {
const real = realpathSync(command)
const whole = (pin?.members ?? []).every((member) => {
const file = statSync(join(dirname(real), basename(member.path)))
return file.isFile() && (process.platform === 'win32' || (file.mode & 0o111) !== 0)
})
return whole ? real : null
} catch {
return null
}
}
function readRelease(folder: string, version: string): ReleaseRead {
try {
const receiptPath = join(folder, RUNTIME_RELEASE_RECEIPT)
const stat = lstatSync(receiptPath)
if (!lstatSync(folder).isDirectory() || !stat.isFile() || stat.size > RECEIPT_MAX_BYTES) {
return 'broken'
}
const receipt = runtimeReleaseReceiptSchema.safeParse(
parseJson(readFileSync(receiptPath, 'utf8'))
)
const whole =
receipt.success &&
receipt.data.version === version &&
receipt.data.platform === process.platform &&
receipt.data.arch === process.arch &&
receipt.data.members.every((member) => {
const segments = member.path.split('/')
if (segments.some((segment) => !segment || segment === '.' || segment === '..')) {
return false
}
const file = lstatSync(join(folder, ...segments))
return (
file.isFile() &&
file.size === member.bytes &&
(process.platform === 'win32' || (file.mode & 0o111) !== 0)
)
})
return whole ? { releaseId: receipt.data.releaseId } : 'broken'
} catch (error) {
return isDefinitiveAbsence(error) ? 'broken' : 'unreadable'
}
}
function parseJson(text: string): unknown {
try {
return JSON.parse(text)
} catch {
return null
}
}
@@ -110,12 +110,14 @@ export function createCodexStructuredLaunchResolver(
/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */
export function createAcpStructuredLaunchResolver(
spec: AcpLaunchSpec,
deps: LaunchTestSources & AcpStructuredLaunchPartDeps
deps: LaunchTestSources &
Omit<AcpStructuredLaunchPartDeps, 'dataDirectory'> &
Partial<Pick<AcpStructuredLaunchPartDeps, 'dataDirectory'>>
) {
return withTestSpawnToken(
acpStructuredAgentDefinition(spec),
launchSources(deps),
acpStructuredLaunchPart(spec, deps),
acpStructuredLaunchPart(spec, { dataDirectory: '/orca-data', ...deps }),
null
)
}
@@ -70,6 +70,7 @@ export function acpModelCatalogDiscovery(
probe: createAcpModelCatalogProbe(spec, {
resolveEnvironment: environment.resolveBaseEnvironment,
refreshEnvironmentAfterMiss: environment.refreshAfterMissingProgram,
dataDirectory: deps.stateDirectory,
...(deps.resolveAgentLaunchEnv ? { resolveLaunchEnv: deps.resolveAgentLaunchEnv } : {}),
...(deps.resolveAgentCommandSettings
? { resolveCommandSettings: deps.resolveAgentCommandSettings }
@@ -42,7 +42,11 @@ function acpMetadata(spec: AcpLaunchSpec): StructuredAgentRuntimeMetadata {
launchCheck: {
resolveLaunch: (shellEnv, commandSettings) => {
const env = { ...shellEnv, ...spec.env }
return { command: resolveAcpLaunchCommand(spec, env, { commandSettings }), env }
const command = resolveAcpLaunchCommand(spec, env, {
commandSettings,
dataDirectory: null
})
return { command, env }
},
supportsVersion: (launch) => acpLaunchVersionSupported(spec, launch)
}
@@ -117,6 +117,7 @@ function createAcpAdapter(
const part = acpStructuredLaunchPart(spec, {
readJournal,
...(deps.resolveAgentFullAccess ? { resolveFullAccess: deps.resolveAgentFullAccess } : {}),
dataDirectory: deps.stateDirectory,
logger: deps.logger
})
return new AcpStructuredSessionAdapter({
@@ -135,6 +135,12 @@ const PIECES: Record<AgentSessionFailureCopyId, (values: AgentSessionFailureCopy
COPY.agentCommandNotRunnable,
values
),
runtimeNotInstalled: (values) =>
translate(
'components.native-chat.failureWords.runtimeNotInstalled',
COPY.runtimeNotInstalled,
values
),
chooseClaudeAccount: () =>
translate(
'components.native-chat.failureWords.chooseClaudeAccount',
+1
View File
@@ -2849,6 +2849,7 @@
"failureWords": {
"accountSwitchInProgress": "A Claude account switch is in progress. Try again after it finishes.",
"agentCommandNotRunnable": "{{agent}}'s Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it.",
"runtimeNotInstalled": "{{agent}} isn't installed on the computer running this chat. Install it from the chat in a current version of Orca.",
"agentCommandNotSignedIn": "Sign in to {{agent}} with `{{loginCommand}}` on the computer running this chat.",
"agentNotSignedIn": "Sign in to {{agent}}.",
"answerUnconfirmed": "Your answer was recorded but the agent did not confirm it.",
+1
View File
@@ -18194,6 +18194,7 @@
"claudeAccountFolderMissing": "The selected Claude account's folder is missing. Sign in to it again or choose another account.",
"claudeAccountSetupFailed": "The selected Claude account could not be set up. Try again or choose another account.",
"agentCommandNotRunnable": "{{agent}}'s Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it.",
"runtimeNotInstalled": "{{agent}} isn't installed on the computer running this chat. Install it from the chat in a current version of Orca.",
"chooseClaudeAccount": "Choose or add one in Claude Accounts settings.",
"chooseClaudeAccountThenRunCommand": "Choose or add one in Claude Accounts settings, then run /{{command}} again.",
"chooseClaudeAccountThenSend": "Choose or add one in Claude Accounts settings, then send your message again.",
+1
View File
@@ -17974,6 +17974,7 @@
"claudeAccountFolderMissing": "Falta la carpeta de la cuenta de Claude seleccionada. Vuelva a iniciar sesión en ella o elija otra cuenta.",
"claudeAccountSetupFailed": "No se pudo configurar la cuenta de Claude seleccionada. Vuelva a intentarlo o elija otra cuenta.",
"agentCommandNotRunnable": "{{agent}}: el Comando en Ajustes → Agentes debe ser la ruta o el nombre de un programa que Orca pueda encontrar, sin argumentos ni variables. Cámbielo o restablézcalo con Reiniciar.",
"runtimeNotInstalled": "{{agent}} no está instalado en el equipo que ejecuta este chat. Instálelo desde el chat en una versión actual de Orca.",
"chooseClaudeAccount": "Elija o agregue una en la configuración de Cuentas de Claude.",
"chooseClaudeAccountThenRunCommand": "Elija o agregue una en la configuración de Cuentas de Claude y vuelva a ejecutar /{{command}}.",
"chooseClaudeAccountThenSend": "Elija o agregue una en la configuración de Cuentas de Claude y vuelva a enviar su mensaje.",
+1
View File
@@ -18078,6 +18078,7 @@
"claudeAccountFolderMissing": "Le dossier du compte Claude sélectionné est introuvable. Reconnectez-vous à ce compte ou choisissez-en un autre.",
"claudeAccountSetupFailed": "Le compte Claude sélectionné n'a pas pu être configuré. Réessayez ou choisissez un autre compte.",
"agentCommandNotRunnable": "{{agent}} : la Commande dans Paramètres → Agents doit être le chemin ou le nom d'un programme qu'Orca peut trouver, sans arguments ni variables. Modifiez-la ou réinitialisez-la.",
"runtimeNotInstalled": "{{agent}} n'est pas installé sur l'ordinateur qui exécute ce chat. Installez-le depuis le chat dans une version récente d'Orca.",
"chooseClaudeAccount": "Choisissez-en un ou ajoutez-en un dans les paramètres Comptes Claude.",
"chooseClaudeAccountThenRunCommand": "Choisissez-en un ou ajoutez-en un dans les paramètres Comptes Claude, puis relancez /{{command}}.",
"chooseClaudeAccountThenSend": "Choisissez-en un ou ajoutez-en un dans les paramètres Comptes Claude, puis renvoyez votre message.",
+1
View File
@@ -18009,6 +18009,7 @@
"claudeAccountFolderMissing": "選択した Claude アカウントのフォルダーが見つかりません。もう一度サインインするか、別のアカウントを選択してください。",
"claudeAccountSetupFailed": "選択した Claude アカウントを設定できませんでした。もう一度試すか、別のアカウントを選択してください。",
"agentCommandNotRunnable": "[設定] → [Agent] にある {{agent}} の [コマンド] には、Orca が見つけられるプログラムのパスまたは名前を、引数や変数なしで指定する必要があります。変更するか、[リセット] してください。",
"runtimeNotInstalled": "{{agent}} はこのチャットを実行しているコンピューターにインストールされていません。最新バージョンの Orca でチャットからインストールしてください。",
"chooseClaudeAccount": "Claude アカウントの設定で選択または追加してください。",
"chooseClaudeAccountThenRunCommand": "Claude アカウントの設定で選択または追加してから、/{{command}} をもう一度実行してください。",
"chooseClaudeAccountThenSend": "Claude アカウントの設定で選択または追加してから、メッセージをもう一度送信してください。",
+1
View File
@@ -18009,6 +18009,7 @@
"claudeAccountFolderMissing": "선택한 Claude 계정의 폴더가 없습니다. 다시 로그인하거나 다른 계정을 선택하세요.",
"claudeAccountSetupFailed": "선택한 Claude 계정을 설정할 수 없습니다. 다시 시도하거나 다른 계정을 선택하세요.",
"agentCommandNotRunnable": "{{agent}}의 설정 → 에이전트 → 명령은 Orca가 찾을 수 있는 프로그램 경로나 이름이어야 하며, 인수나 변수를 포함할 수 없습니다. 변경하거나 재설정하세요.",
"runtimeNotInstalled": "{{agent}}이(가) 이 채팅을 실행하는 컴퓨터에 설치되어 있지 않습니다. 최신 버전의 Orca에서 채팅을 통해 설치하세요.",
"chooseClaudeAccount": "Claude 계정 설정에서 선택하거나 추가하세요.",
"chooseClaudeAccountThenRunCommand": "Claude 계정 설정에서 선택하거나 추가한 다음 /{{command}}을(를) 다시 실행하세요.",
"chooseClaudeAccountThenSend": "Claude 계정 설정에서 선택하거나 추가한 다음 메시지를 다시 보내세요.",
+1
View File
@@ -17974,6 +17974,7 @@
"claudeAccountFolderMissing": "所选 Claude 账户的文件夹不存在。请重新登录该账户或选择其他账户。",
"claudeAccountSetupFailed": "无法设置所选 Claude 账户。请重试或选择其他账户。",
"agentCommandNotRunnable": "{{agent}} 在设置 → 智能体中的命令必须是 Orca 能找到的程序路径或名称,且不能包含参数或变量。请修改或重置。",
"runtimeNotInstalled": "运行此聊天的计算机上未安装 {{agent}}。请在最新版本的 Orca 中从聊天里安装。",
"chooseClaudeAccount": "请在 Claude 账户设置中选择或添加一个。",
"chooseClaudeAccountThenRunCommand": "请在 Claude 账户设置中选择或添加一个,然后重新运行 /{{command}}。",
"chooseClaudeAccountThenSend": "请在 Claude 账户设置中选择或添加一个,然后重新发送您的消息。",
+2
View File
@@ -71,6 +71,8 @@ export const AGENT_SESSION_FAILURE_COPY = {
claudeAccountSetupFailed: CLAUDE_PROFILE_SETUP_FAILED_MESSAGE,
agentCommandNotRunnable:
"{{agent}}'s Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it.",
runtimeNotInstalled:
"{{agent}} isn't installed on the computer running this chat. Install it from the chat in a current version of Orca.",
chooseClaudeAccount: 'Choose or add one in Claude Accounts settings.',
chooseClaudeAccountThenRunCommand:
'Choose or add one in Claude Accounts settings, then run /{{command}} again.',
@@ -208,6 +208,8 @@ const FAILURE_SENTENCES = {
claudeAccountSetupFailed: (_context, _fact, _surface, say) => say('claudeAccountSetupFailed'),
agentCommandNotRunnable: (context, _fact, _surface, say) =>
say('agentCommandNotRunnable', agent(say, context)),
runtimeNotInstalled: (context, _fact, _surface, say) =>
say('runtimeNotInstalled', agent(say, context)),
managedAccountUnsupported: (context, _fact, _surface, say) =>
joinSentences([
say('managedAccountUnsupported'),
+1
View File
@@ -37,6 +37,7 @@ export const AGENT_SESSION_FAILURE_KINDS = [
'claudeAccountFolderMissing',
'claudeAccountSetupFailed',
'agentCommandNotRunnable',
'runtimeNotInstalled',
'providerExited',
'restartFailed',
'providerRejected',
@@ -84,6 +84,8 @@ export const AGENT_SESSION_REFUSAL_REASONS = {
'claudeAccountSetupFailed',
/** Settings → Agents → Command names no program this host can run. */
'agentCommandNotRunnable',
/** The runtime Orca installs for the agent is not installed on this host. */
'runtimeNotInstalled',
/** The agent started, then Orca could not open the chat's conversation for it. */
'attachFailed'
],
@@ -118,6 +118,7 @@ const REASON_WORDS = {
claudeAccountFolderMissing: { fact: 'claudeAccountFolderMissing', action: 'actFirst' },
claudeAccountSetupFailed: { fact: 'claudeAccountSetupFailed', action: 'retry' },
agentCommandNotRunnable: { fact: 'agentCommandNotRunnable', action: 'actFirst' },
runtimeNotInstalled: { fact: 'runtimeNotInstalled', action: 'actFirst' },
attachFailed: codeWords('retry')
},
agent_session_ownership_unknown: {
@@ -96,6 +96,7 @@ const KIND_CATEGORY = {
claudeAccountFolderMissing: 'startFailed',
claudeAccountSetupFailed: 'startFailed',
agentCommandNotRunnable: 'startFailed',
runtimeNotInstalled: 'startFailed',
restartFailed: 'startFailed',
providerRejected: 'content',
attachmentInvalid: 'content',
@@ -127,6 +128,7 @@ const KIND_VERDICT = {
claudeAccountFolderMissing: 'failure',
claudeAccountSetupFailed: 'failure',
agentCommandNotRunnable: 'failure',
runtimeNotInstalled: 'failure',
restartFailed: 'failure',
providerRejected: 'failure',
attachmentInvalid: 'failure',