Skip to content

[Bug]: npm launcher ignores OPENCODEX_BUN_PATH for direct starts and update fallbacks #721

Closed

Description

@luvs01

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

  1. Install OpenCodex 2.7.43 globally through npm.
  2. Download a Bun 1.4 Canary executable.
  3. Set a valid override and regenerate the service:
$env:OPENCODEX_BUN_PATH = "C:\path\to\bun-1.4-canary\bun.exe"
ocx service install
  1. Confirm service-state.json, the generated service wrapper, or service.log selects the Canary path and reports bun_source="override".
  2. Stop the running proxy/service so ocx gui must cold-start it, then run:
ocx gui
  1. Inspect the resulting proxy process or /api/system/memory. The proxy uses bundled Bun 1.3.14.
  2. 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 separate resolveBun() 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.14

Screenshots and supporting files

The shared runtime resolver honors the override, while the npm launcher maintains a separate bundled-only resolver:

  • 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;
  • opencodex/bin/ocx.mjs

    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" });
  • opencodex/src/cli/index.ts

    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();
  • opencodex/bin/ocx.mjs

    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

  1. github-actions commented on Jul 30, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: unknown.

  2. lidge-jun commented on Jul 31, 2026

    @lidge-jun
    Owner

    Fixed on dev — 9b5c864ff merge: PR #734 landed the launcher change (bin/ocx.mjs:316-346, src/lib/bun-runtime.ts:48-60), so OPENCODEX_BUN_PATH is honoured for direct starts and update fallbacks.

    The macOS test failure that delayed it is fixed too: f81e98aca realpaths the temp root before comparing against a post-chdir() path, since /var symlinks to /private/var. bun test tests/bun-runtime.test.ts is 8/8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions