Client or integration
CLI (ocx update), npm global install
Area
Platform (Windows) / CLI
Summary
Split out of #1849, which reported two distinct defects that need different fixes and
different closure conditions. The service-restart half shipped; this is the root cause,
and it is still open.
| Half |
Defect |
Status |
| A |
Service wrapper restart-loops against a missing install (1,009 × child exited with code 3) |
Fixed by #1877, released in v2.24.0 |
| B |
ocx update can destroy a working install and leave an unbootable skeleton |
this issue |
#1877 stopped the symptom: the wrapper no longer hammers a package that is not there.
It does nothing about how the package came to be missing.
Current behaviour
The reporter's failed update left the global package directory with 107 directories and
zero files — no package.json, no bin/ocx.mjs, no bundled bun.exe, and every
launcher (ocx, ocx.cmd, ocx.ps1, and the opencodex siblings) removed from
%APPDATA%\npm. npm created the tree, unpacked no content, and did not roll back. The
documented recovery commands were themselves unreachable, because the launchers that would
run them were the files that had been deleted.
The update path delegates to npm install -g, which is destructive-in-place: it removes
the existing install before the replacement is verified. When it fails midway there is no
prior state left to fall back to.
Verification does exist and is not the gap — checkUpdatePackageIntegrity fails closed
before the proxy is stopped, isRealBunBinary rejects the postinstall stub, and
awaitRestartedProxyHealthy runs an identity-checked /healthz probe with a version match
and a stability window. The problem is narrower and worse than missing verification: it is
detect-only, and it runs after the old install is already gone, so a failed probe has
nothing left to restore.
Expected behaviour
An update that fails leaves the previous working install in place. The user should never
reach a state where the tool cannot repair itself.
Suggested implementation notes
A transactional shape, where the existing install is only released after the replacement
is proven:
download to staging
→ verify executable/assets/version in staging
→ snapshot the existing install
→ atomic directory or launcher switch
→ health probe the new install
→ success: drop the snapshot | failure: restore the snapshot
The ordering constraint is the whole point: never delete the existing install before the
replacement is verified. "download → delete → copy" cannot be made safe by adding retries
to it.
Worth deciding explicitly, since it shapes the fix: whether to keep delegating to
npm install -g (and stage around it, e.g. install to a temp prefix and switch), or to own
the download and unpack directly. The former is less invasive; the latter is the only way to
get a truly atomic switch on Windows, where an in-use file cannot always be replaced.
Verification this needs
Fault injection at every boundary, each asserting a launchable install survives:
- after download, before verification
- after verification, before the snapshot
- mid-switch (the Windows case where a partially replaced directory is the failure mode)
- after the switch, before the health probe
- health probe fails → snapshot restored and the old version still runs
tests/update-transaction.test.ts does not exist yet and would be new.
Additional context
Original report, environment details and the full 1,009-restart evidence are in #1849.
That issue stays open for this defect; #1877 is linked there as the mitigation half.
Client or integration
CLI (
ocx update), npm global installArea
Platform (Windows) / CLI
Summary
Split out of #1849, which reported two distinct defects that need different fixes and
different closure conditions. The service-restart half shipped; this is the root cause,
and it is still open.
child exited with code 3)ocx updatecan destroy a working install and leave an unbootable skeleton#1877 stopped the symptom: the wrapper no longer hammers a package that is not there.
It does nothing about how the package came to be missing.
Current behaviour
The reporter's failed update left the global package directory with 107 directories and
zero files — no
package.json, nobin/ocx.mjs, no bundledbun.exe, and everylauncher (
ocx,ocx.cmd,ocx.ps1, and theopencodexsiblings) removed from%APPDATA%\npm. npm created the tree, unpacked no content, and did not roll back. Thedocumented recovery commands were themselves unreachable, because the launchers that would
run them were the files that had been deleted.
The update path delegates to
npm install -g, which is destructive-in-place: it removesthe existing install before the replacement is verified. When it fails midway there is no
prior state left to fall back to.
Verification does exist and is not the gap —
checkUpdatePackageIntegrityfails closedbefore the proxy is stopped,
isRealBunBinaryrejects the postinstall stub, andawaitRestartedProxyHealthyruns an identity-checked/healthzprobe with a version matchand a stability window. The problem is narrower and worse than missing verification: it is
detect-only, and it runs after the old install is already gone, so a failed probe has
nothing left to restore.
Expected behaviour
An update that fails leaves the previous working install in place. The user should never
reach a state where the tool cannot repair itself.
Suggested implementation notes
A transactional shape, where the existing install is only released after the replacement
is proven:
The ordering constraint is the whole point: never delete the existing install before the
replacement is verified. "download → delete → copy" cannot be made safe by adding retries
to it.
Worth deciding explicitly, since it shapes the fix: whether to keep delegating to
npm install -g(and stage around it, e.g. install to a temp prefix and switch), or to ownthe download and unpack directly. The former is less invasive; the latter is the only way to
get a truly atomic switch on Windows, where an in-use file cannot always be replaced.
Verification this needs
Fault injection at every boundary, each asserting a launchable install survives:
tests/update-transaction.test.tsdoes not exist yet and would be new.Additional context
Original report, environment details and the full 1,009-restart evidence are in #1849.
That issue stays open for this defect; #1877 is linked there as the mitigation half.