CodeNomad/packages/server/src/opencode/runtime-support.ts
Pascal André 8e8ca15934
chore(opencode): qualify runtime 2.0.29 and align client/plugin pins (#948)
## Summary

- Recommend and pin OpenCode **2.0.29** (`@opencode/client`,
`@opencode/plugin`); the technical minimum stays **2.0.27** since
2.0.28/2.0.29 keep Effect 4.0.1.
- Add 2.0.29 as a reviewed contract profile beside 2.0.27; 2.0.28 and
future/custom labels keep authenticated negotiation.
- Test fixture only: the recurring Missions host declares the
`credential` domain that the 2.0.29 Promise adapter reads eagerly (the
managed owner spreads `ctx`, dropping the Proxy fallback). No product
code change.
- Two new isolated fixtures for upstream behavior changes: bounded
output-length continuation, and rejected Promise tools / native denial.
- Compatibility docs, CONTRIBUTING, AGENTS and architecture guide
updated.

## Upstream review

- OpenAPI: same 118 paths; additive schema changes (config `policies`,
provider/model `compatibility`, `encoding`).
- Private LocationServiceMap/Form/Permission/queue contracts used by the
pending snapshot: unchanged.
- Effect stays 4.0.1.

## Validation (Windows, isolated CLIs/databases, never the shared
daemon)

- 2.0.29: plugin runtime, automation, pruning/history/proxy, Missions
contract/family control/full journey, session environment, fork, aside,
prompt skills, Git degraded/init, provider usage/accounts, temporary
workspaces, blank-session cleanup, output-limit continuation, Promise
tool failure, system settings discovery, native upgrade 2.0.15→2.0.29.
- 2.0.27 (minimum): plugin runtime, automation, pruning, Missions
contract/family, session environment, fork, aside, prompt skills, blank
cleanup.
- Server: 2235 passed / 7 skipped / 0 failed. UI: 1524 passed. Tauri
crate 268/268, updater handoff 1/1, UI updater flow 13/13 (dev base
includes #786).

## Known, not caused by this change

`scripts/test-permission-receipts-native.mjs` times out identically on
2.0.27 with the previous pins; tracked separately.
2026-10-14 15:06:35 +02:00

43 lines
2.4 KiB
TypeScript

/**
* Native plugins share the daemon's Effect/SDK rather than embedding a second
* runtime. 2.0.27 supplies Effect 4.0.1 and Schema.isStartingWith consumed by the
* current schemas; 2.0.26 fails the isolated plugin-loading regression.
* See dev-docs/OPENCODE_V2_COMPATIBILITY.md for the demonstrated ABI boundary.
*/
export const MINIMUM_OPENCODE_VERSION = "2.0.27"
/** Qualification target, independent of the technically required minimum. */
export const RECOMMENDED_OPENCODE_VERSION = "2.0.29"
export const OPENCODE_UPDATE_REQUIRED = "opencode_update_required"
export function isBelowOpenCodeMinimum(version: string): boolean {
// These historical V2 beta publications predate the stable event contract.
if (/^0\.0\.0-beta-\d+$/.test(version)) return true
const match = /^(\d+)\.(\d+)\.(\d+)(?:\+[\w.-]+)?$/.exec(version)
// Custom/prerelease labels are unverified, not automatically incompatible.
// Their authenticated API contract is inspected before functional requests.
if (!match) return false
const [major, minor, patch] = match.slice(1).map(Number)
const [minimumMajor, minimumMinor, minimumPatch] = MINIMUM_OPENCODE_VERSION.split(".").map(Number)
return major! < minimumMajor! || (major === minimumMajor && (minor! < minimumMinor!
|| (minor === minimumMinor && patch! < minimumPatch!)))
}
export type OpenCodeIncompatibility = "plugin_runtime" | "step_timestamp" | "canonical_api" | "session_environment"
export class UnsupportedOpenCodeError extends Error {
readonly code = OPENCODE_UPDATE_REQUIRED
readonly statusCode = 426
readonly minimumVersion = MINIMUM_OPENCODE_VERSION
constructor(readonly actualVersion: string, readonly reason: OpenCodeIncompatibility = "plugin_runtime") {
super(`${OPENCODE_UPDATE_REQUIRED}: OpenCode ${actualVersion}; ${reason === "plugin_runtime"
? `requires the native Effect 4.0.1/Schema plugin runtime introduced in ${MINIMUM_OPENCODE_VERSION}`
: reason === "step_timestamp" ? "requires the native session.step.started.data.started timestamp introduced in 2.0.7"
: reason === "session_environment" ? "requires PUT /api/session/{sessionID}/environment with variables"
: "does not expose the canonical session, permission, Form and inbox API contract"}`)
this.name = "UnsupportedOpenCodeError"
}
}
export function assertSupportedOpenCode(version: string): void {
if (isBelowOpenCodeMinimum(version)) throw new UnsupportedOpenCodeError(version)
}