Files
orca/src/main/agent-cli-version-probe.ts
T
Brennan Benson 349e3d19b9 Agents need only a minimum version for chat; newer releases and prereleases keep working (#30393)
* fix(native-chat): agents need only a minimum version for chat; newer releases and prereleases run

OpenCode chats accepted only the 1.x and 2.x lines and Pi only 0.x and 1.x,
every prerelease was refused, and a slow or failed --version run counted as
unsupported. A new major would open as a terminal and existing chats said
"Orca can't run this agent in a chat here."

Each agent now lists ascending minimum versions; a version must reach the
latest one on or below its own major line, compared by semver. The check
answers ready, too old, unknown (slow, failed or wordless run) or missing
(did not start). Only too old refuses a launch, as Codex already did, with
"<Agent> X is too old for chats. Update to Y or newer." carried in a new
optional refusal fact agentInstallation; Codex keeps sending
codexInstallation for older clients. Codex's reader and error fold into the
same check.

* refactor(native-chat): a launch check declares its minimum versions; create support and the agent health snapshot run the one shared check

* fix(native-chat): read an agent's version from stderr too, so Pi 0.73 is known too old instead of unreadable
2026-10-21 17:57:10 -07:00

97 lines
3.9 KiB
TypeScript

import { compareAppVersions, parseCliVersion } from '../shared/app-version'
import { runProcess } from '@orca/process-host'
import type { ProcessSpec } from '@orca/process-host/process-spec'
/** One line naming why a probe said no; the binary's own words, bounded. */
function warnRefused(program: string, why: string, output = ''): void {
const said = output.trim().replace(/\s+/g, ' ').slice(0, 200)
console.warn(`[agent-cli-version] ${program} --version: ${why}${said ? `: ${said}` : ''}`)
}
/** What one `--version` run says of the installed release. `unknown` (a slow, failed or wordless
* run) is no verdict, so a chat runs and any real start failure reports itself; `missing` is a
* program that did not start at all. */
export type AgentCliVersionCheck =
| { status: 'ready'; version: string }
| { status: 'tooOld'; version: string; minimumVersion: string }
| { status: 'unknown' | 'missing'; version: null }
/** Reads a launch binary's `--version`; injectable so tests name the release. */
export type AgentCliVersionReader = (
input: Pick<ProcessSpec, 'program' | 'cwd' | 'env'>
) => Promise<{ version: string | null; started?: false }>
/** Runs `<program> --version` on this host and holds what it prints to `minimumVersions`. */
export async function checkAgentCliVersion(
input: Pick<ProcessSpec, 'program' | 'cwd' | 'env'>,
minimumVersions: readonly string[],
read: AgentCliVersionReader = readAgentCliVersion
): Promise<AgentCliVersionCheck> {
const { version, started } = await read(input)
if (version === null) {
return { status: started === false ? 'missing' : 'unknown', version: null }
}
const minimumVersion = agentCliMinimumVersion(version, minimumVersions)
if (minimumVersion !== null && compareAppVersions(version, minimumVersion) < 0) {
warnRefused(input.program, `${version} is older than ${minimumVersion}`)
return { status: 'tooOld', version, minimumVersion }
}
console.info(`[agent-cli-version] ${input.program} --version: ${version} is supported`)
return { status: 'ready', version }
}
/** Create support's answer: only a too-old binary or one that does not start keeps the terminal. */
export function launchableRelease(check: AgentCliVersionCheck): boolean {
return check.status === 'ready' || check.status === 'unknown'
}
/** The release `version` must reach: the latest of the ascending `minimumVersions` on or below its
* major line, so a later major needs no entry (and one major holds one floor); the first below
* them all; none when none is listed. */
export function agentCliMinimumVersion(
version: string,
minimumVersions: readonly string[]
): string | null {
const major = (value: string): number => Number(value.split('.')[0])
return (
minimumVersions.findLast((minimum) => major(minimum) <= major(version)) ??
minimumVersions[0] ??
null
)
}
export async function readAgentCliVersion(
input: Pick<ProcessSpec, 'program' | 'cwd' | 'env'>,
parseVersion: typeof parseCliVersion = parseCliVersion
): Promise<{ version: string | null; started?: false }> {
let result
try {
result = await runProcess({
...input,
args: ['--version'],
timeoutMs: 5_000,
maxOutputBytes: 4_096,
killOnOutputLimit: true
})
} catch (error) {
warnRefused(input.program, `did not start (${String(error)})`)
return { version: null, started: false }
}
const output = `${result.stdout}\n${result.stderr}`
if (result.timedOut || result.outputTruncated || result.code !== 0) {
const why = result.timedOut
? 'timed out'
: result.outputTruncated
? 'printed too much'
: `exited ${result.code ?? result.signal}`
warnRefused(input.program, why, output)
return { version: null }
}
const version = parseVersion(output)
if (version === null) {
warnRefused(input.program, 'printed no version', output)
return { version: null }
}
return { version }
}