Skip to content

fix(packages): install opencode node_modules with its exact pinned Bun - #287

Merged
gkze merged 10 commits into
mainfrom
cursor/opencode-exact-bun-b086
Oct 24, 2026
Merged

gkze merged 10 commits into
mainfrom
cursor/opencode-exact-bun-b086

Conversation

@gkze

@gkze gkze commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Update run 40086686747 (main @ a5f02837) failed prepare-x86 / native. In Phase 2/4, the opencode and opencode-desktop node_modules FODs at fork rev d5ea4ab ran bun install v1.4.2 --frozen-lockfile. Both stopped with error: lockfile had changes, but lockfile is frozen. electron-runtimes then failed only as their prerequisite.

Root cause

The fork's lockfile was generated by one Bun release and checked by Nix with another.

  • Upstream bumped the Bun pin. Upstream set packageManager: [email protected] on 2026-10-22 (anomalyco/opencode#58087). nixcfg's currently locked fork rev a6fde022 still declared [email protected].
  • The fork now regenerates bun.lock itself. Since 5deeb3a0 and afa1bb25 (2026-10-24), the fork's nightly no longer replays its lockfile. It runs bun install --lockfile-only with the Bun pinned in packageManager (1.4.4), and verifies the result with --frozen-lockfile under 1.4.4 only. d5ea4ab is that regenerated lockfile.
  • Nix installs with nixpkgs Bun. The node_modules FOD lives in the fork's nix/node_modules.nix and uses pkgs.bun from nixcfg's nixpkgs. That is 1.4.2, and it is still 1.4.2 on every channel, including master.
  • The trigger is one fork dependency. The fork's root "vite": "catalog:" devDependency (fork commit 05743a66) puts vite 7.3.6 at the root, while astro, vitest, and app pull vite 8.2.2. Bun 1.4.2 resolves that split differently from the 1.4.4-written lockfile, so it refuses the frozen install. Upstream's own lockfile has no root vite, and 1.4.2 still accepts it.

Evidence:

Check Bun 1.4.2 Bun 1.4.4
Fork d5ea4ab source as the FOD sees it (fileset, flags, filters) fails passes
The same, on all three --cpu/--os targets × both filter sets (CLI and desktop) 6/6 fail 6/6 pass
Upstream dev (f20760b8) passes —
Upstream plus only the root vite devDependency, relocked by 1.4.4 fails passes
Upstream plus only the packages/ui devDependencies, relocked by 1.4.4 passes passes
nixcfg's real opencode.node_modules derivation with --override-input opencode …/d5ea4ab (baseline) fails, same error as CI —

Fix

The input's packageManager decides which Bun installs the dependencies. That matches how the lockfile is generated and verified.

  • Exact Bun for opencode. overlays/opencode builds the fork's node_modules and CLI with lib/exact-bun.nix at the input's packageManager version, the same mechanism mux and superset use. opencode-desktop inherits it through opencode's node_modules and buildBun.
  • No more packageManager rewrite. sync_package_manager_bun_version.py is removed. exact-bun.nix now asserts that the pinned Bun equals packageManager, and only the root manifest declares one.
  • Updater owns the pin. OpencodeUpdater derives pins.bunVersion from the locked commit's package.json and hashes the Bun release archives. The exact-Bun updater mechanics that only mux had (the pin, runtime hashing, _is_latest, the pins-only finalize, and duplicate guards) now live in a shared ExactBunNodeModulesHashUpdater base, and mux uses it too.
  • Desktop reads opencode's in-run pin. opencode-desktop now declares companion_of = "opencode". FlakeInputHashUpdater probes now include the companion parent's result from the current run, as the source-backed updaters already do. Without this, a future packageManager bump would make desktop evaluate opencode's stale workspace pin, and exact-bun.nix would reject it.
  • Candidate handoff fix. ResolvedVersion.restore listed only direct MappingMetadata subclasses, so MuxSourceMetadata would have failed the pins-to-native candidate handoff. It now walks the whole subclass tree, in definition order.
  • sources.json. The updaters regenerated both files for the still-locked a6fde022 ([email protected]). That pin uses the same release archives nixpkgs fetches, so the carried-forward node_modules hashes still build. Update moves the input to d5ea4ab (Bun 1.4.4) after merge.
  • Docs and kick. docs/update/README.md and runtime.md are updated. One .github/update-kick line is added, so merging starts exactly one Update run.

Nothing is weakened. --frozen-lockfile, platform coverage, and every validation stay as they were, and no package is skipped.

Fork follow-up (gkze/opencode)

nixcfg no longer depends on this, but it is still a gap in the fork. The fork's relock and replay steps verify bun.lock only with the packageManager Bun. The fork's nix-hashes workflow runs only on pushes to dev, never on gkze/patches. So the fork's own nix build .#opencode (which uses nixpkgs Bun 1.4.2) is broken at d5ea4ab, and nothing on the fork catches it. Two options: have nix/node_modules.nix use a Bun that matches packageManager, or run the Nix node_modules build as part of nightly validation.

Separately, the fork's "Publish fork nightly" run 40086226766 is failing with Every new commit needs provenance. That is unrelated to this hash failure.

Validation

  • CI stages run locally, x86_64-linux. nixcfg ci update prepare --pins-only -- opencode opencode-desktop resolved d5ea4ab with bunVersion 1.4.4. The native prepare --previous then succeeded for both, with success: true and no errors. Computed hashes:
    • opencode nodeModulesHash x86_64-linux: sha256-Q4r9RztFAcEw+cxlxFRtXmUk5bwonBgZgz9JrACocS4=
    • opencode-desktop nodeModulesHash x86_64-linux: sha256-eJTns72ExUwqzZg6+joA34C/fDNt0d2i8G1e9kttEBs=
  • Locked input. At a6fde022, the x86_64-linux opencode and desktop node_modules FODs build with the exact Bun 1.4.2 and match the committed hashes. Both packages evaluate on x86_64-linux, aarch64-linux, and aarch64-darwin.
  • PR checks. PR update prepare maps this diff to mux opencode opencode-desktop t3code-workspace and runs the native prepare stages on macos-15, ubuntu-arm, and ubuntu x86. That computes and proves the aarch64-linux and aarch64-darwin hashes.
  • Local gates.
    • prek run -a passes. It needed a reflow of a pre-existing 114-column comment line from fix(update): bound the Cachix flush and recover a wedged daemon #286.
    • ruff and ty pass. The certify-python set passes (697 tests).
    • Touched modules have 100% line and branch coverage.
    • The Nix checks test-nix-opencode-desktop, test-nix-package-helpers, and test-nix-repo-checks pass.
    • The full pytest run shows no new failures versus origin/main. Those 58 failures are environment-specific to this VM: macOS tooling, Node, and one timing-flaky test.
    • nix fmt's shellcheck step fails on the same unrelated zsh and shell files on main.

Merge with main after 36e573f

main moved to 36e573f5 (92e3ee5, engineering-discipline remediation), which conflicted with this branch. main was merged in, not rebased, so the history above is unchanged.

Re-validated on the merged tree:

  • Main bumped nixpkgs from 25f53830 to 02c087f3. On x86_64-linux, the opencode.node_modules, opencode-desktop.node_modules, and mux.offlineCache FODs build from scratch, match the committed hashes, and reproduce under nix build --rebuild.
  • prek run -a passes, and so do ruff format --check and the touched and certify-python tests (816 passed).
  • The full pytest run shows no new failures versus origin/main. Its 50 failures are macOS host tools missing from this Linux VM.
Open in Web Open in Cursor 

cursoragent and others added 6 commits October 24, 2026 11:41
Mux owned the only implementation of the exact-Bun contract: pin the
manifest's packageManager Bun, hash its release archives, keep them through
the pins-only stage, and refuse duplicate runtime entries. Move that into
ExactBunNodeModulesHashUpdater so another Bun-locked source can reuse it.

Candidate handoff restored only direct MappingMetadata subclasses, which
would have dropped MuxSourceMetadata once it specialized the shared base.
Walk the whole subclass tree.

Co-authored-by: George Kontridze <[email protected]>
…result

Flake-input hash probes overrode only the package's own sources entry, so a
companion read its parent's pre-update workspace file. opencode-desktop's
node_modules is opencode's derivation: once opencode pins an exact Bun, a
packageManager bump would make desktop evaluate the stale pin and fail.

Pass the companion parent's in-run entry into the probe overrides, as the
source-backed updaters already do, and declare opencode-desktop a companion
of opencode.

Co-authored-by: George Kontridze <[email protected]>
Update run 40086686747 failed opencode and opencode-desktop on
x86_64-linux: their node_modules FODs ran nixpkgs bun 1.4.2 against fork rev
d5ea4ab, whose bun.lock the fork now regenerates with its packageManager
Bun ([email protected], bumped upstream in anomalyco/opencode#58087). With the
fork's root `vite: catalog:` devDependency, 1.4.2 re-resolves the vite 7/8
split and rejects --frozen-lockfile ("lockfile had changes"); 1.4.4 accepts
the same tree for both filter sets on every target. nixpkgs ships 1.4.2 on
every channel.

Build opencode's node_modules and CLI with lib/exact-bun.nix at the input's
packageManager version, maintained by the updater like mux and superset.
Desktop inherits it through opencode's node_modules and buildBun. Drop the
packageManager rewrite: exact-bun.nix now asserts that equality instead.

sources.json is regenerated by the updaters for the still-locked a6fde022
([email protected]): the same release archives nixpkgs fetches, so the carried
node_modules hashes still build (verified on x86_64-linux).

Co-authored-by: George Kontridze <[email protected]>
The subclass-tree walk visited classes last-defined-first, so a reloaded
updater module's stale metadata class shadowed the live one and restored
metadata failed isinstance checks. Visit in definition order, as the
direct-subclass comprehension did.

Co-authored-by: George Kontridze <[email protected]>
cachix-flush-proof.yml (#286) carried a 114-column comment line that fails
lint-yaml-yamllint, and nix fmt marks opencode-desktop's unused node_modules
override argument. Both already fail on main.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T12:04:06.866467Z a24873e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Update run 40089810663 failed prepare-pins at bootstrap with exit 1 and
no diagnostic. 92e3ee5 added a `nix eval` of
`(builtins.getFlake "git+file://<checkout>").inputs.nixpkgs.outPath`.
Actions checks out with fetch-depth 1, and Determinate Nix (3.22.5 on
the runners) rejects an explicit git+file URL to a clean shallow clone:
"'<checkout>' is a shallow Git repository, so 'revCount' is not
available". The `.` flake reference used by the preceding `nix build`
and `nix develop` adds `shallow=1` implicitly, so only the eval failed.

Bootstrap now takes its flake URL from the shared `local_flake_url`,
which opts into `shallow=1`. That also covers the updaters that build
the same URL and only avoided the failure because the tree was dirty or
the devshell's Nix 2.34.9 tolerates it.

The job launcher's entrypoint now reports a failed child's command and
the tail of its captured stderr before exiting with its status, so a
captured failure can no longer exit silently.
update_bun_lock.py is a standalone template that cannot import
lib.update.paths, so it carried its own copy of the git+file URL without
the shallow=1 opt-in. Its test now asserts that the installable equals
local_flake_url, and the neutils resolution-event test derives its
expected URL from the same helper.
Resolve the two conflicts with 92e3ee5's engineering-discipline
remediation:

- packages/mux/updater.py: keep this branch's move onto
  ExactBunNodeModulesHashUpdater and adopt main's shared
  require_exact_semantic_version in place of mux's private regex.
- .github/workflows/cachix-flush-proof.yml: take main's rewritten
  header comment; this branch only rewrapped the old one for yamllint.
PR update prepare runs bootstrap on a fetch-depth 1 checkout, which
fails on main until #288 lands. Merging the same commits keeps #287
and #288 conflict-free in either merge order.
@gkze
gkze merged commit f51da84 into main Oct 24, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants