Skip to content

updater: in-place npm install races exec → ENOEXEC on launch, hang on retry #41495

Closed

Description

@kahlos

Summary

Launching opencode2 while the auto-updater is running npm install -g @opencode-ai/cli@<next> (or launching concurrently with a second instance's update) hits ENOEXEC: unknown error, posix_spawn '.../bin/opencode2.exe' because the updater replaces the live executable in place, non-atomically. A retry then hangs until the install finishes, and a killed install leaves the CLI unable to start at all until the next update cycle.

Environment

  • opencode version: 0.0.0-next-17082 at failure (current 0.0.0-next-17086; next channel advances several builds/hour)
  • OS: macOS Darwin 25.5.0 (arm64)
  • Terminal: ghostty (TERM=xterm-ghostty, COLORTERM=truecolor)
  • Shell: /bin/zsh
  • Install/channel: npm global install (/opt/homebrew/lib/node_modules/@opencode-ai/cli), next channel
  • Active plugins: ~/.config/opencode/plugins/notify.ts

Reproduction

  1. Install via npm install -g @opencode-ai/cli@next (auto-update active).
  2. While an update is in flight — either the updater's own background cycle (observed running on its own roughly every 13–20 min on the next channel) or a user-initiated npm install -g in another terminal — run opencode2.
  3. Observe immediate failure: Error: ENOEXEC: unknown error, posix_spawn '.../bin/opencode2.exe' (raw effect error, no hint that an update is in progress).
  4. Retry: process hangs ("Starting background server..." with no progress) until the install completes, then succeeds.

Alternative trigger with worse impact: kill the npm install mid-reify (SIGTERM/Ctrl-C). The package is left half-replaced; subsequent launches keep failing with ENOEXEC until the background updater happens to re-run and finish the install.

Expected Behavior

  • The updater should replace the executable atomically (install to a staging path then rename, or use versioned binary paths), so a spawn never observes a half-written binary.
  • The launcher should detect a transiently unexecutable target (file changing, ENOEXEC) and either retry briefly or print a clear "updating, retry in a moment" message instead of a raw error and a hang.

Actual Behavior

Timeline from npm debug logs and file mtimes (all times local):

  • 07:13:26 — auto-updater starts npm install -g @opencode-ai/[email protected]; process is terminated (SIGTERM) mid-reify (~/.npm/_logs/2026-08-10T06_13_26_476Z-debug-0.log:88), leaving the package half-moved.
  • 07:13:31 — user launches opencode2 → ENOEXEC: unknown error, posix_spawn '/opt/homebrew/lib/node_modules/@opencode-ai/cli/bin/opencode2.exe' (file present but not a valid executable).
  • 07:13:50 — updater re-runs (npm list then install); completes 07:13:51 (binary mtime = 155 MB valid arm64 Mach-O).
  • Second launch hung through this window; third succeeded.

Updater npm log pattern (every cycle: version check then exact-version install):

verbose title npm list @opencode-ai/cli
verbose title npm install @opencode-ai/[email protected]
error process terminated / signal SIGTERM   (first cycle, mid-reify)

Full first-failure stack trace:

ERROR (#1): Error: Failed to start server
    at service.ensure (src/services/server-connection.ts:66:53)
    at cli.server-connection.resolve (src/commands/handlers/default.ts:22:44)
  [cause]: Error: ENOEXEC: unknown error, posix_spawn '/opt/homebrew/lib/node_modules/@opencode-ai/cli/bin/opencode2.exe'
      at try (../client/src/effect/service.ts:71:23)

Additional Context

  • Occurred once here, but the race is inherent: any launch concurrent with an in-flight global install (another terminal's launch, the background poller, or a manual npm update) will hit it. The next channel makes this common because builds advance several times an hour.
  • Not related to network: registry fetches all returned HTTP 200 at the time of failure.
  • Workaround that helped: wait ~20–30 s and relaunch; if the CLI still fails with ENOEXEC, manually re-run npm install -g @opencode-ai/cli@next.
  • Root-cause sketch: npm's global reify replaces files in place (renames package dir to a staging name, extracts new one); the updater spawns/execs that same path while it is being replaced, and nothing retries on ENOEXEC.

Activity

  1. hanipcode commented on Aug 12, 2026

    @hanipcode

    I encountered the same failur on macOS arm64 with Node v22.20.0 through NVM and a global npm installation of @opencode-ai/cli.

    Additional Context

    At 09:42:36, starting opencode2 failed while launching the background service:

    timestamp=2026-08-12T02:42:15.307Z level=INFO run=0ffb7d01 message="cli starting" version=0.0.0-next-17155 channel=next local=false args=[] role=cli
    timestamp=2026-08-12T02:42:16.289Z level=INFO run=0ffb7d01 message="update check" current=0.0.0-next-17155 latest=0.0.0-next-17268 role=cli
    timestamp=2026-08-12T02:42:51.110Z level=INFO run=fd751eb5 message="cli starting" version=0.0.0-next-17155 channel=next local=false args=[] role=cli
    timestamp=2026-08-12T02:42:51.532Z level=INFO run=fd751eb5 message="update check" current=0.0.0-next-17155 latest=0.0.0-next-17268 role=cli
    timestamp=2026-08-12T02:42:54.843Z level=INFO run=29e672ba message="cli starting" version=0.0.0-next-17268 channel=next local=false args="[\"--version\"]" role=cli
    timestamp=2026-08-12T02:42:54.895Z level=INFO run=fd751eb5 message="updated OpenCode" from=0.0.0-next-17155 to=0.0.0-next-17268 method=npm role=cli
    timestamp=2026-08-12T02:42:56.595Z level=INFO run=693c20ad message="cli starting" version=0.0.0-next-17268 channel=next local=false args="[\"serve\",\"--service\"]" role=cli
    
    • Update check began: 09:42:16.289
    • ENOEXEC appeared: 09:42:36.867
    • Update completed: 09:42:54.895
    • Updated service launch began: 09:42:56.595
  2. bjorntechTobbe commented on Aug 28, 2026

    @bjorntechTobbe

    I can reproduce a self-update race in the global npm-installed opencode2 on macOS arm64.

    Environment

    • macOS arm64
    • NVM Node 24 + npm
    • global @opencode-ai/cli / opencode2.exe shim

    Repro

    1. Start the managed service.
    2. Launch multiple --standalone sessions at about the same time.
    3. Let them all hit the self-update path for the same global install.

    Timeline

    • 10:53:07 one CLI launch starts from $HOME/.nvm/versions/node/v24.15.0/bin/opencode2.exe and is already on 0.0.0-beta-18414.
    • 10:53:09-10:53:11 several server-side sessions (0.0.0-beta-18371, 0.0.0-beta-18381, 0.0.0-beta-18387) all detect 0.0.0-beta-18414 and spawn npm install --global @opencode-ai/[email protected] concurrently.
    • One updater succeeds (updated OpenCode from 0.0.0-beta-18387 to 0.0.0-beta-18414), while others fail.

    Sanitized error excerpts

    npm error ENOTEMPTY: directory not empty, rmdir '$HOME/.nvm/versions/node/v24.15.0/lib/node_modules/@opencode-ai/cli'
    npm error EEXIST: file already exists, symlink '../lib/node_modules/@opencode-ai/cli/bin/opencode2.exe' -> '$HOME/.nvm/versions/node/v24.15.0/bin/opencode2'
    npm error Binary not found at '$TMPDIR/opencode-install-*/node_modules/@opencode-ai/cli-darwin-arm64/bin/opencode2'
    

    Observed final state

    • After the race, the global package directory was empty and opencode2 no longer resolved, while already-running OpenCode processes remained alive.
    • Later checks report current=0.0.0-beta-18414 latest=0.0.0-beta-18414.
    • Some concurrent sessions fall back to automatic update skipped: installation method not found.

    Correlation / impact
    This looks like a race in the shared self-update path, not workspace-specific behavior. It makes updates nondeterministic and can remove the globally installed CLI while existing processes continue running.

  3. neriousy commented on Oct 22, 2026

    @neriousy
    Member

    Closing this, since most of it is fixed on dev:

    One gap remains: if you launch opencode while npm is replacing the binary, starting the service can still fail with a raw ENOEXEC. npm replaces global packages in place, so we can't make that swap atomic. If this still bites you on a current build, comment here and we'll reopen it.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions