Repository navigation
[Bug]: npm launcher ignores OPENCODEX_BUN_PATH for direct starts and update fallbacks #721
Description
Client or integration
Codex CLI
Area
Service lifecycle
Summary
With a valid OPENCODEX_BUN_PATH pointing to Bun 1.4 Canary, ocx service install correctly bakes that runtime into the Windows service wrapper. However, the npm ocx launcher independently resolves only the bundled Bun dependency and does not read the override.
As a result, a cold ocx gui start and the direct-proxy fallback after an npm service-refresh failure can run the actual proxy under bundled Bun 1.3.14, despite the service state and wrapper selecting Bun 1.4.
Expected: a valid OPENCODEX_BUN_PATH should be the effective runtime for the npm launcher and every direct proxy start/recovery path, consistent with durableBunRuntime() and the documented override behavior.
Actual: durable service artifacts use the override, but npm-launched CLI/direct fallback processes use bundled Bun 1.3.14.
This is practically significant on Windows because 2.7.43 bundles Bun 1.3.14, while the project documents the current multi-gigabyte memory-growth problem and recommends a Bun 1.4 Canary override as the experimental escape hatch.
Reproduction
- Install OpenCodex 2.7.43 globally through npm.
- Download a Bun 1.4 Canary executable.
- Set a valid override and regenerate the service:
$env:OPENCODEX_BUN_PATH = "C:\path\to\bun-1.4-canary\bun.exe"
ocx service install- Confirm
service-state.json, the generated service wrapper, orservice.logselects the Canary path and reportsbun_source="override". - Stop the running proxy/service so
ocx guimust cold-start it, then run:
ocx gui- Inspect the resulting proxy process or
/api/system/memory. The proxy uses bundled Bun 1.3.14. - The same downgrade occurs during an npm update when service reinstall fails and OpenCodex takes the direct-proxy fallback. The fallback goes through Node
bin/ocx.mjs, whose separateresolveBun()selects bundled Bun.
The gap also affects other direct starts that inherit process.execPath; the durable service path itself selects the override correctly.
A focused regression should cover override → npm launcher → direct proxy start and assert the effective proxy runtime, not only the helper selection or generated service wrapper.
Version
2.7.43 (v2.7.43, commit d1f544bbc22d25b9b2bd3c8e776fb8e3242b4ed5)
Operating system
Windows 11 Pro 25H2, build 26200.8894, x64
Provider and model
Not provider-specific
Logs or error output
# Durable service artifact before fallback
bun="C:\...\bun-1.4.0-canary...\bun.exe"
bun_source="override"
# Expected proxy runtime
Bun 1.4.0 Canary
# Actual proxy after ocx gui cold-start / npm direct fallback
Bun 1.3.14Screenshots and supporting files
The shared runtime resolver honors the override, while the npm launcher maintains a separate bundled-only resolver:
opencodex/src/lib/bun-runtime.ts
Lines 63 to 84 in d1f544b
export function overrideBunPath(): string | null { const value = process.env[BUN_OVERRIDE_ENV]?.trim(); if (!value) return null; return isRealBunBinary(value) ? value : null; } export function durableBunRuntime(): DurableBunRuntime { const override = overrideBunPath(); if (override) return { path: override, source: "override", overrideEnv: BUN_OVERRIDE_ENV }; const bundled = bundledBunPath(); if (bundled) return { path: bundled, source: "bundled", overrideEnv: BUN_OVERRIDE_ENV }; return { path: process.execPath, source: "process", overrideEnv: BUN_OVERRIDE_ENV }; } /** * Bun path to bake into durable artifacts (launchd/systemd/Task Scheduler and * the Codex auto-start shim). Prefer the bundled binary — it lives under the * npm global prefix and survives across `ocx update` — and fall back to the * current runtime, which is Bun when launched normally. */ export function durableBunPath(): string { return durableBunRuntime().path; Lines 319 to 364 in d1f544b
function resolveBun() { let bunDir; try { bunDir = bunBinDir(); } catch { fail("the `bun` dependency is not installed."); } let bin = findBunBinary(bunDir); if (bin) return bin; // Lazy fallback: --ignore-scripts (or a failed postinstall) leaves the // ~450-byte placeholder stub. Run the bun package's own installer once. const installJs = join(bunDir, "install.js"); if (existsSync(installJs)) { const r = spawnSync(process.execPath, [installJs], { stdio: "inherit" }); if (r.status === 0) bin = findBunBinary(bunDir); } if (!bin) fail("Bun binary missing after install attempt."); return bin; } // `ocx update --help` prints usage and exits WITHOUT side effects. The npm launcher // intercepts `update` before the Bun CLI starts, so the help short-circuit must live // here too — otherwise --help runs the real self-update, stops the proxy, and drops // in-flight routed streams (issue #168). const updateHelpRequested = process.argv[2] === "update" && process.argv.slice(3).some(a => a === "--help" || a === "-h" || a === "help"); if (updateHelpRequested) { console.log("Usage: ocx update [--tag latest|preview]\n\nUpdate opencodex. Preview installs stay on the preview tag unless overridden."); process.exit(0); } if (process.argv[2] === "update" && isNodeModulesInstall() && !isBunGlobalInstall()) { runNpmSelfUpdate(); } const bun = resolveBun(); // Run the Bun child asynchronously and FORWARD termination signals to it, then wait // for its graceful shutdown before this launcher exits. The previous blocking // spawnSync() could not run JS signal handlers and did not forward signals, so a // signal delivered only to this launcher (Codex app, IDE terminal, service wrapper, // or `kill -INT <launcherPid>`) killed the launcher and ORPHANED the Bun proxy — // port left bound, pid/runtime-port files left behind, Codex config not restored. const child = spawn(bun, [cliPath, ...process.argv.slice(2)], { stdio: "inherit" }); Lines 848 to 863 in d1f544b
case "gui": { const cfg = await import("../config"); const config = cfg.loadConfig(); // Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port // proxy and waits until the spawned one actually answers before opening the browser. let live = await findLiveProxy(); if (!live) { console.log("Proxy not running. Starting..."); const child = spawn(process.execPath, startArgv((config.port ?? 10100) > 0 ? (config.port ?? 10100) : undefined), { detached: true, stdio: "ignore", windowsHide: true, env: process.env, }); child.unref(); live = await waitForProxy(); Lines 243 to 267 in d1f544b
// The stop above unloaded any managed service; reinstall via the freshly-installed // launcher so the new files write the baked paths and the service restarts. if (serviceWasInstalled) { console.log("Reinstalling the background service with the updated files..."); const prevBake = process.env.OCX_BAKE_PORT; process.env.OCX_BAKE_PORT = String(bakePort); try { const svcArgs = serviceReinstallArgs(); const svc = spawnSync(process.execPath, svcArgs, { stdio: "inherit", windowsHide: true }); if (svc.status !== 0) { // On Windows, schtasks /create requires elevation. The launcher inherits the // user's (non-admin) token, so the service reinstall can fail with access // denied. Fall back to a direct detached proxy start so the update never // leaves the user without a running proxy. console.warn("opencodex: service refresh failed — starting the proxy directly instead."); console.warn(" Run 'ocx service install' as administrator to refresh the background service."); const env = { ...process.env }; delete env.OCX_SERVICE; const child = spawn(process.execPath, [launcher, "start", "--port", String(bakePort)], { detached: true, stdio: "ignore", windowsHide: true, env, }); child.unref(); ## Your options 1. **Wait for a bundled runtime update.** Once a Bun release verifiably carries the fixes, opencodex will bump the bundled runtime and the safer stream path turns on automatically. 2. **Run a Bun runtime you trust with `OPENCODEX_BUN_PATH`.** This is unvalidated territory — you are running opencodex on a runtime we have not tested; at your own risk. Important for service installs: the override is read **when the service artifact is generated**, not at service start. Set the environment variable, then re-run `ocx service install` from that same shell so the path is baked into the durable service definition. Setting the env alone does nothing for an already-installed service.
I found memory-symptom reports (#314, #509) and service-install work (#408, #591), but no issue covering a valid runtime override being ignored by the npm launcher/direct fallback.
Redacted configuration
The only relevant setting is a valid OPENCODEX_BUN_PATH that points to a Bun 1.4 Canary executable. Its local path is redacted.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Jul 30, 2026
Automated translation bookkeeping — detected language: unknown.
lidge-jun commentedon Jul 31, 2026
Fixed on
dev—9b5c864ff merge: PR #734landed the launcher change (bin/ocx.mjs:316-346,src/lib/bun-runtime.ts:48-60), soOPENCODEX_BUN_PATHis honoured for direct starts and update fallbacks.The macOS test failure that delayed it is fixed too:
f81e98acarealpaths the temp root before comparing against a post-chdir()path, since/varsymlinks to/private/var.bun test tests/bun-runtime.test.tsis 8/8.