Repository navigation
[Bug][Windows] ocx update is not transactional: a failed update deletes the working install with no rollback #1942
Description
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.
Activity
coderabbitai commentedon Aug 18, 2026
🔗 Related PRs
#306 - feat(windows): add tray controls and restart-safety diagnostics [merged]
#533 - fix(update): preserve proxy on npm cache failures [open]
#557 - fix(update): harden npm cache recovery preflight logs [open]
#803 - Codex/pr533 update recovery hardening [open]
#1877 - fix(windows): stop service loop on missing install [open]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
docs(devlog): record the WP5 outcome for lidge-jun#1849 and lidge-jun…
docs(devlog): fold the WP5 audit corrections
docs(devlog): map where the split work now lives
lidge-jun commentedon Aug 18, 2026
Design for the remaining transactional half is now written to diff-level: devlog/_plan/260817_windows_stability_program/090_transactional_update_rollback.md (stage-to-side install, post-install verification manifest, atomic swap with rollback window, boot-time recovery probe, fault-injection test plan). It builds on the landed foundation (#1944-#1947, d09c752). Implementation is the next scheduled cycle; this issue closes when the boot probe + swap land.
docs(devlog): wp10 follow-up designs + campaign phase records
fix(update): stage, verify, and swap npm self-updates with rollback (#…
lidge-jun commentedon Aug 19, 2026
Fixed on
devby #2079 (1ad131a):ocx update(npm installs) now stages the new version into a sibling directory, verifies a manifest inside the stage (package.json version, launcher integrity, bundled-Bun size gate, sentinel deps), and only then swaps live aside to a sibling backup and the staged tree in — with reverse-rename rollback, a bounded Windows lock-retry, a.ocx-recovery.jsonmarker on double fault, an in-launcher boot probe, and a service-wrapper restore that recovers the newest backup even when the live tree (and launcher) are gone. A failed update can no longer delete a working install.docs(devlog): record the WP5 outcome for lidge-jun#1849 and lidge-jun…
docs(devlog): fold the WP5 audit corrections
docs(devlog): map where the split work now lives
docs(devlog): wp10 follow-up designs + campaign phase records
fix(update): stage, verify, and swap npm self-updates with rollback (l…