Skip to content
56 changes: 48 additions & 8 deletions packages/opencode/src/altimate/bridge/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import type { BridgeMethod, BridgeMethods } from "./protocol"
import { Telemetry } from "../telemetry"
import { Log } from "../../util/log"

/** Platform-aware path to the python binary inside a venv directory. */
function venvPythonBin(venvDir: string): string {
return process.platform === "win32"
? path.join(venvDir, "Scripts", "python.exe")
: path.join(venvDir, "bin", "python")
}

/** Resolve the Python interpreter to use for the engine sidecar.
* Exported for testing — not part of the public API. */
export function resolvePython(): string {
Expand All @@ -22,17 +29,20 @@ export function resolvePython(): string {

// 2. Check for .venv relative to altimate-engine package (local dev)
const engineDir = path.resolve(__dirname, "..", "..", "..", "altimate-engine")
const venvPython = path.join(engineDir, ".venv", "bin", "python")
const venvPython = venvPythonBin(path.join(engineDir, ".venv"))
if (existsSync(venvPython)) return venvPython

// 3. Check for .venv in cwd
const cwdVenv = path.join(process.cwd(), ".venv", "bin", "python")
if (existsSync(cwdVenv)) return cwdVenv

// 4. Check the managed engine venv (created by ensureEngine)
// 3. Check the managed engine venv (created by ensureEngine)
// This must come before the CWD venv check — ensureEngine() installs
// altimate-engine here, so an unrelated .venv in the user's project
// directory must not shadow it.
const managedPython = enginePythonPath()
if (existsSync(managedPython)) return managedPython

// 4. Check for .venv in cwd
const cwdVenv = venvPythonBin(path.join(process.cwd(), ".venv"))
if (existsSync(cwdVenv)) return cwdVenv

// 5. Fallback
return "python3"
}
Expand All @@ -45,6 +55,8 @@ export namespace Bridge {
const CALL_TIMEOUT_MS = 30_000
const pending = new Map<number, { resolve: (value: any) => void; reject: (reason: any) => void }>()
let buffer = ""
// Mutex to prevent concurrent start() calls from spawning duplicate processes
let pendingStart: Promise<void> | null = null

export async function call<M extends BridgeMethod>(
method: M,
Expand All @@ -53,7 +65,20 @@ export namespace Bridge {
const startTime = Date.now()
if (!child || child.exitCode !== null) {
if (restartCount >= MAX_RESTARTS) throw new Error("Python bridge failed after max restarts")
await start()
if (pendingStart) {
await pendingStart
// Re-check: the process may have died between startup and now
if (!child || child.exitCode !== null) {
throw new Error("Bridge process died during startup")
}
} else {
pendingStart = start()
try {
await pendingStart
} finally {
pendingStart = null
}
}
}
const id = ++requestId
const request = JSON.stringify({ jsonrpc: "2.0", method, params, id })
Expand Down Expand Up @@ -141,8 +166,18 @@ export namespace Bridge {
if (msg) Log.Default.error("altimate-engine stderr", { message: msg })
})

child.on("error", (err) => {
Log.Default.error("altimate-engine spawn error", { error: String(err) })
restartCount++
for (const [id, p] of pending) {
p.reject(new Error(`Bridge process failed to spawn: ${err}`))
pending.delete(id)
}
child = undefined
})

child.on("exit", (code) => {
if (code !== 0) restartCount++
if (code !== null && code !== 0) restartCount++
for (const [id, p] of pending) {
p.reject(new Error(`Bridge process exited (code ${code})`))
pending.delete(id)
Expand All @@ -154,6 +189,11 @@ export namespace Bridge {
try {
await call("ping", {} as any)
} catch (e) {
// Clean up the spawned process so subsequent call() invocations
// correctly detect !child and trigger a restart instead of writing
// to a non-functional process and hanging until timeout.
child?.kill()
child = undefined
throw new Error(`Failed to start Python bridge: ${e}`)
}
}
Expand Down
23 changes: 19 additions & 4 deletions packages/opencode/src/altimate/bridge/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@ declare const OPENCODE_VERSION: string
// Mutex to prevent concurrent ensureEngine/ensureUv calls from corrupting state
let pendingEnsure: Promise<void> | null = null

/** Pip extras spec for altimate-engine (e.g. "warehouses" → altimate-engine[warehouses]).
* Used in ensureEngine install command and recorded in manifest for upgrade detection. */
export const ENGINE_INSTALL_SPEC = "warehouses"

interface Manifest {
engine_version: string
python_version: string
uv_version: string
cli_version: string
installed_at: string
/** Comma-separated extras that were installed (e.g. "warehouses") */
extras?: string
}

/** Returns path to the engine directory */
Expand Down Expand Up @@ -158,7 +164,12 @@ export async function ensureEngine(): Promise<void> {
async function ensureEngineImpl(): Promise<void> {
const manifest = await readManifest()
const isUpgrade = manifest !== null
if (manifest && manifest.engine_version === ALTIMATE_ENGINE_VERSION) return

// Validate both version AND filesystem state — a matching version in the
// manifest is not enough if the venv or Python binary was deleted.
const pythonExists = existsSync(enginePythonPath())
const extrasMatch = (manifest?.extras ?? "") === ENGINE_INSTALL_SPEC
if (manifest && manifest.engine_version === ALTIMATE_ENGINE_VERSION && pythonExists && extrasMatch) return

const startTime = Date.now()

Expand All @@ -168,8 +179,9 @@ async function ensureEngineImpl(): Promise<void> {
const dir = engineDir()
const venvDir = path.join(dir, "venv")

// Create venv if it doesn't exist
if (!existsSync(venvDir)) {
// Create venv if it doesn't exist, or recreate if the Python binary is missing
// (e.g. user deleted the binary but left the venv directory intact)
if (!existsSync(venvDir) || !pythonExists) {
Log.Default.info("creating python environment")
try {
execFileSync(uv, ["venv", "--python", "3.12", venvDir], { stdio: "pipe" })
Expand All @@ -189,7 +201,8 @@ async function ensureEngineImpl(): Promise<void> {
const pythonPath = enginePythonPath()
Log.Default.info("installing altimate-engine", { version: ALTIMATE_ENGINE_VERSION })
try {
execFileSync(uv, ["pip", "install", "--python", pythonPath, `altimate-engine==${ALTIMATE_ENGINE_VERSION}`], { stdio: "pipe" })
const spec = `altimate-engine[${ENGINE_INSTALL_SPEC}]==${ALTIMATE_ENGINE_VERSION}`
execFileSync(uv, ["pip", "install", "--python", pythonPath, spec], { stdio: "pipe" })
} catch (e: any) {
Telemetry.track({
type: "engine_error",
Expand All @@ -212,6 +225,7 @@ async function ensureEngineImpl(): Promise<void> {
uv_version: uvVersion,
cli_version: typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local",
installed_at: new Date().toISOString(),
extras: ENGINE_INSTALL_SPEC,
})

Telemetry.track({
Expand All @@ -220,6 +234,7 @@ async function ensureEngineImpl(): Promise<void> {
session_id: Telemetry.getContext().sessionId,
engine_version: ALTIMATE_ENGINE_VERSION,
python_version: pyVersion,
extras: ENGINE_INSTALL_SPEC,
status: isUpgrade ? "upgraded" : "started",
duration_ms: Date.now() - startTime,
})
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export namespace Telemetry {
session_id: string
engine_version: string
python_version: string
extras?: string
status: "started" | "restarted" | "upgraded"
duration_ms: number
}
Expand Down
Loading
Loading