fix(lock): load proper-lockfile lazily so a Worker can import this package - #97
Merged
Conversation
…ckage `proper-lockfile` pulls in `graceful-fs`, which patches Node's `fs` at module scope (`fs.close = ...`, `fs.closeSync = ...`). workerd exposes those as getter-only accessors, so a static import made the assignment throw while Cloudflare ran startup validation on an uploaded Worker: Uncaught TypeError: Cannot set property close of #<Object> which has only a getter [code: 10021] The whole Worker is rejected — no request frame, nothing the app can catch — even for bundles that never take a lock. Every downstream consumer inherits it: `@tangle-network/agent-runtime` pins this package and re-exports the knowledge subsystem from its ROOT barrel, which is where a product's chat turn imports `runToolLoop` from. Confirmed by uploading a Worker whose only import is that root barrel. Both call sites are already async, so deferring the module scope to the first lock is behaviour-preserving. `LockOptions` stays a type import. `verify:package` now walks the packed dist and fails on any static import of a module that patches a Node builtin. `wrangler deploy --dry-run` only bundles — it never executes module scope — so it is structurally blind to this class, which is why the regression shipped green.
tangletools
approved these changes
Jul 28, 2026
tangletools
left a comment
Contributor
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 235def35
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-28T19:12:34Z
This was referenced Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every Cloudflare Worker importing this package is undeployable
graceful-fs@4.2.11patches Node'sfsat module scope:workerd exposes
node:fs'sclose/closeSyncas getter-only accessors, so that assignment throws while Cloudflare runs startup validation on upload. The entire Worker is rejected — no request frames, nothing the app can catch:The chain is a static top-level import at every edge:
That root barrel is the documented home of
runToolLoop/streamToolLoop, which is exactly what a product's chat turn imports — so any Worker onagent-runtime@0.108+that imports the root barrel cannot deploy. It already took down one product's production deploys, which were unblocked with a per-app bundler alias. This is the source fix.Change
proper-lockfilemoves to a dynamicimport()inside the two functions that take a lock. Both are alreadyasync, so deferring the module scope to the first lock is behaviour-preserving;LockOptionsstays a type-only import. The built bundle now carries onlylockfileModule ??= import("proper-lockfile").The check CI was missing
wrangler deploy --dry-runonly bundles. It never uploads, so it never executes module scope, and it is structurally blind to this entire class — which is why the breaking change went green.verify:package(already run in CI afterbuild) now walks the packed dist and fails on any static import of a module that patches a Node builtin.Proof — the guard fails on the real regression
Reverting
src/mutation-lock.tsto the static import and rebuilding:inspect-D5iarJc2.jsis the exact chunk hash published in 6.1.8, so the revert reproduces the shipped bytes.Restoring the fix:
Proof — the real Worker upload
A minimal Worker whose only import is the agent-runtime root barrel, built with
@cloudflare/vite-plugin(the build products use) and uploaded withwrangler versions upload(same startup validation as a deploy, zero traffic).Before, on
agent-knowledge@6.1.8:The after-upload is run against the published
6.1.9once this merges and releases, and posted here.Verification
pnpm lint— clean (188 files)pnpm typecheck— clean (src + contracts)pnpm test—468 passed | 12 skipped (480)node scripts/verify-package.mjs— passes on the packed tarball