Skip to content

[Bug]: Interrupted stop during update leaves orphaned pending-teardown receipts, causing subsequent ocx update runs to permanently fail-closed #4897

Description

@Vocllum

Client or integration

CLI / Updater / Process Control

Area

Installation or packaging

Summary

When ocx update attempts to stop the running proxy before replacing packages, it records a shared teardown claim via claimPendingTeardown (~/.opencodex/pending-teardown-<nonce>.json). If the stop or drain sequence is interrupted (e.g. timeout, service kill, watchdog kickstart, or external SIGTERM/SIGHUP), the process exits before reaching clearPendingTeardown(nonce).

Because bin/ocx.mjs checks hasPendingTeardownIn(readdirSync, configDir()) without validating process liveness (isProcessAlive(ownerPid)), any subsequent run of ocx update immediately aborts with:

opencodex: a shared teardown from an earlier stop is still outstanding and needs manual review; aborting the update.
opencodex: confirm no proxy is running, run 'ocx restore', then remove the pending-teardown file in the opencodex home.

If multiple attempts are made while the service is unstable, each attempt claims a new receipt and fails to clean it up. In our environment, this caused 6 orphaned pending-teardown-*.json receipts to accumulate, wedging the updater until files were manually deleted.

Reproduction

  1. Run opencodex as a managed service on macOS (gui/<uid>/com.opencodex.proxy).
  2. Trigger an update via ocx update --tag latest while the proxy is under load or handling long requests.
  3. Interrupt or kill the update worker during the stop phase before the final clearPendingTeardown() call.
  4. The worker process (ownerPid) exits. The receipt pending-teardown-<nonce>.json remains in ~/.opencodex/.
  5. Run ocx update again.
  6. Observe:
opencodex v2.56.0 (installed via npm, tag latest)
Verified @bitkyc08/opencodex@2.57.0 integrity metadata sha512-lKEPsKq7DYSg2AhA6…
opencodex: a shared teardown from an earlier stop is still outstanding and needs manual review; aborting the update.
opencodex: confirm no proxy is running, run 'ocx restore', then remove the pending-teardown file in the opencodex home.
  1. The updater refuses to proceed due to teardown-outstanding, even though the recorded ownerPid is dead and no proxy is running.

Version

2.56.0 -> 2.57.0

Operating system

macOS 27.0 (Darwin 27.0.0 arm64)

Provider and model

Not provider-specific

Logs or error output

opencodex: a shared teardown from an earlier stop is still outstanding and needs manual review; aborting the update.
opencodex: confirm no proxy is running, run 'ocx restore', then remove the pending-teardown file in the opencodex home.

In update-job.json:

{
  "currentVersion": "2.56.0",
  "latestVersion": "2.57.0",
  "exitCode": 1,
  "error": "update command failed (1)",
  "log": [
    "exit 1 · 505 bytes of output withheld (no recognized diagnostic fields)"
  ]
}

Orphaned receipt files in ~/.opencodex/:

pending-teardown-75280d7e4be191581141ca7863fbc7aa.json
pending-teardown-756b6d207bdfc07862a7969abb7b2c39.json
pending-teardown-a8d6f0e0c1d042ff281c49b7962265bb.json
pending-teardown-b13be77f97d4b87e0f0b6160279dc2b4.json
pending-teardown-b3bee9495dd4b13b3414c3c2013e1281.json
pending-teardown-f9f3728779188403ef2e9179778a0d04.json

Receipt payload example:

{
  "ownerPid": 90028,
  "nonce": "b3bee9495dd4b13b3414c3c2013e1281",
  "createdAt": "2026-09-17T08:52:45.393Z",
  "endpoint": { "hostname": "127.0.0.1", "port": 10100 },
  "endpointSource": "exact"
}

Root cause analysis

  1. Lack of exit/signal rollback: In src/cli/index.ts, claimPendingTeardown drops a durable receipt on disk before stopping the proxy. If stopProxy or eviction fails or is aborted, there is no signal handler or try...finally block to prune the uncommitted claim.
  2. Stat-only check in update launcher: hasPendingTeardownIn in bin/ocx.mjs only matches the filename pattern pending-teardown-*.json. It does not inspect the receipt payload or verify whether ownerPid is still alive via isProcessAlive(receipt.ownerPid).
  3. Deadlock accumulation: When bin/ocx.mjs sees an existing receipt, it invokes ocx stop to settle it, which claims yet another receipt before verifying the inherited ones. An interrupted chain easily snowballs into multiple orphaned files.

Suggested fix

  1. In bin/ocx.mjs / hasPendingTeardownIn: Parse existing receipts and check if ownerPid is still active. If isProcessAlive(ownerPid) is false and the target port is dead, the abandoned receipt should either be auto-recovered/cleared or safely ignored for the update gate.
  2. In src/cli/index.ts: Wrap stopWithDeferral with cleanup hooks so that if the process terminates unexpectedly before shared teardown completes, the uncommitted receipt is retracted.

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

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 workingcliCLI, config inject, packaging flagsinstallInstallation or packaging

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions