#261 Local extension model bundles don't rebuild when source changes (no rebuild CLI; manual cache delete breaks the runner)
Opened by john Ā· 5/6/2026
Summary
Source-mode (local) extension models ā those loaded from extensions/models/<name>/ inside a swamp repo ā are bundled once into .swamp/bundles/<hash>/<name>/<entry>.js and never rebundled when the underlying TypeScript source changes. There is no CLI command to force a rebuild, and deleting the bundle directory by hand puts the runner into an unrecoverable state.
This makes the dev loop for authoring a local extension effectively broken: any non-trivial schema or method-signature change is invisible to the runtime until you find some way to invalidate the bundle (which the CLI doesn't expose).
Steps to reproduce
- Initialize a swamp repo:
swamp repo init. - Author a local extension model under
extensions/models/my-model/withmanifest.yamland an entry.tsthat exports amodelwith aglobalArgumentszod schema (e.g. one required fieldfoo: z.number()). - Create an instance:
swamp model create @scope/my-model my-model --json. - Run a workflow / model method that targets it once so swamp produces the cached bundle at
.swamp/bundles/<hash>/my-model/<entry>.js. - Edit the source: change
globalArgumentsfromz.object({ foo: z.number() })toz.object({})and movefooonto each method'sargumentsschema instead. Bumpversionin bothmanifest.yamland the exportedmodel.versionto force a fresh release identifier. - Re-run the workflow / method, passing
fooas a method argument (which is now where the schema says it lives).
Expected
The runtime picks up the new schema and validates foo against the method arguments. The bundle should be rebuilt automatically when source mtime is newer than bundle mtime, OR there should be an explicit swamp extension rebuild <path> / --force-rebundle flag.
Actual
- The cached bundle JS still contains the old schema definitions.
grep ConnectionArgsSchema .swamp/bundles/<hash>/my-model/<entry>.jsreturns0matches even after the source has been edited and the manifest version bumped. - Calling the method fails with
Model validation failed: Global arguments: Required at \"foo\"ā i.e. the executor is still using the oldglobalArgumentsschema baked into the stale bundle. swamp model type describe @scope/my-model --jsonreturns the newglobalArguments: {}shape (suggesting the type catalog is re-reading source) but the runtime executor disagrees with that view because it's loading the cached compiled JS.- Deleting the bundle directory manually (
rm -rf .swamp/bundles/<hash>) does not trigger a rebuild on the next run ā instead the runner errors withNo such file or directory (os error 2): readfile '/.../.swamp/bundles/<hash>/my-model/<entry>.js'and the workflow fails. So there is no rebuild-on-miss path. swamp extension --helpexposes only registry verbs (push,pull,install,rm,update,version,yank,unyank,trust,source,fmt,quality,search). There is norebuild,bundle,refresh, or equivalent. So neither implicit (mtime-driven) nor explicit (CLI-triggered) rebundling is available for source-mode extensions.
This is fine for registry-pulled extensions ā they're immutable per version and bundled at swamp extension push time. The gap is specifically in the local-source dev loop.
Workaround
Currently the only workaround I've found is to nuke the entire repo's swamp state (.swamp/) ā including unrelated workflow runs, vault data, and the SQLite extension catalog ā and re-create the model instance from scratch. That's destructive and not viable in a multi-extension repo.
Suggested fix
Either:
- Detect source mtime newer than
.swamp/bundles/<hash>/<file>.jsand rebundle on the next run, or - Expose
swamp extension rebuild <manifest-path>(or a--force-rebundleflag onswamp workflow run/swamp model method run) so the dev loop is explicit.
Option 1 is the better default for local sources since manifests already advertise themselves as a source: (vs. a registry-pulled lockfile entry).
Environment
- swamp
20260206.200442.0-sha. - macOS,
Darwin 22.6.0 arm64 - Local extension loaded from
extensions/models/<name>/(noswamp extension source addneeded ā theextensions/directory is the local-load path)
Closed
No activity in this phase yet.
john commented 5/6/2026, 12:11:26 PM
Re-checking on the latest binary ā bug still reproduces. Filing this update because the owner suggested a patch might already be in flight.
Versions
| Value | |
|---|---|
| Filed against | 20260206.200442.0-sha. (Feb 2026) |
| Tested now | 20260505.231643.0-sha.5a337b81 (May 2026) |
Repro
Same setup as the original report: a local extension model under extensions/models/issue-lifecycle/ inside a swamp repo init-ed directory, with one cached bundle at .swamp/bundles/<hash>/issue-lifecycle/issue-lifecycle.js from a previous run.
Steps:
- Edit the source TS file. I added the literal token
REBUNDLE_PROBE_TOKEN_2026to the leading docblock so agrepwould tell me whether the bundle was regenerated. - Source mtime advances:
source: May 6 13:09:16 2026. - Run something that exercises the executor ā I ran
swamp workflow run mandible-triage --input issueNumber=999999 --input mandibleBaseUrl=http://localhost:8000. The workflow ran the model'sstartmethod and produced an error message that came from the existing source (so the bundle was loaded), then fast-failed on the bogus issue number. - Inspect the bundle file:
bundle: May 6 11:37:32 2026ā mtime unchanged. grep -c "REBUNDLE_PROBE_TOKEN_2026" .swamp/bundles/<hash>/issue-lifecycle/issue-lifecycle.jsā0matches.
So the executor loaded the stale, cached bundle and ignored the source edit, just like before.
CLI surface
swamp extension --help still exposes only registry verbs:
push, fmt, quality, pull, install, rm, list, search, update, outdated, version, yank, unyank, trust, sourceNo rebuild, bundle, refresh, or equivalent. (outdated is new since the original report but it only flags out-of-date registry-pulled extensions ā it doesn't touch local-source bundles.)
Suggested fix (unchanged)
Either:
- Detect source mtime newer than
.swamp/bundles/<hash>/<file>.jsand rebundle on the next run, or - Expose
swamp extension rebuild <manifest-path>(or a--force-rebundleflag onswamp workflow run/swamp model method run) so the dev loop is explicit.
Happy to test a candidate fix locally if it'd help.
stack72 commented 5/6/2026, 10:52:08 PM
This is fixed on current main. I reproduced the exact steps from the issue (local extension with globalArguments: { foo: z.number() }, cached bundle, then edited source to move foo to method arguments with a version bump) and the bundle correctly rebuilt with the new schema on the next CLI invocation.
Three fixes landed since the reported version (20260206) that close the gap:
W3 (#1322) ā
ReconcileFromDisk+ freshness-as-aggregate-query.buildIndex()now always runs SHA-256 content-fingerprint checks viafindStaleFiles()beforeloadSingleType()can access the catalog. This is the core fix ā source changes are detected and rebundled in the warm-start path regardless of mtime.#265 (#1327) ā
BundleResult+fromCachefingerprint preservation. WhenbundleWithCachereturns a cached bundle (e.g. bare-specifier build failure),rebundleAndUpdateCatalognow preserves the old fingerprint instead of poisoning the catalog. This ensuresfindStaleFilesretries on the next invocation.#1288 ā
recoverMissingBundleENOENT fallback. Manual bundle deletion (rm -rf .swamp/bundles/<hash>) now triggers an on-demand rebundle from source instead of theNo such file or directoryerror reported in the issue.
The type describe vs runtime schema divergence is also gone ā both read from the same catalog entry after buildIndex rebundles.
Considered adding a regression test for the specific schema-migration scenario, but the existing integration test in bundle_cache_freshness_test.ts already covers the underlying mechanic (content-fingerprint invalidation with preserved mtime). W4's loader unification will reshape the test surface anyway.
Sign in to post a ripple.