Skip to main content
← Back to list
01Issue
BugClosedSwamp CLI
Assigneesstack72

#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

  1. Initialize a swamp repo: swamp repo init.
  2. Author a local extension model under extensions/models/my-model/ with manifest.yaml and an entry .ts that exports a model with a globalArguments zod schema (e.g. one required field foo: z.number()).
  3. Create an instance: swamp model create @scope/my-model my-model --json.
  4. Run a workflow / model method that targets it once so swamp produces the cached bundle at .swamp/bundles/<hash>/my-model/<entry>.js.
  5. Edit the source: change globalArguments from z.object({ foo: z.number() }) to z.object({}) and move foo onto each method's arguments schema instead. Bump version in both manifest.yaml and the exported model.version to force a fresh release identifier.
  6. Re-run the workflow / method, passing foo as 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>.js returns 0 matches 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 old globalArguments schema baked into the stale bundle.
  • swamp model type describe @scope/my-model --json returns the new globalArguments: {} 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 with No 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 --help exposes only registry verbs (push, pull, install, rm, update, version, yank, unyank, trust, source, fmt, quality, search). There is no rebuild, 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:

  1. Detect source mtime newer than .swamp/bundles/<hash>/<file>.js and rebundle on the next run, or
  2. Expose swamp extension rebuild <manifest-path> (or a --force-rebundle flag on swamp 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>/ (no swamp extension source add needed — the extensions/ directory is the local-load path)
02Bog Flow
āœ“OPENāœ“TRIAGEDāœ“IN PROGRESSā—‰CLOSED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 1 MOREIMPLEMENTATION

Closed

5/6/2026, 10:52:34 PM

No activity in this phase yet.

03Sludge Pulse
stack72 assigned stack725/6/2026, 10:32:39 PM
Editable. Press Enter to edit.

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:

  1. Edit the source TS file. I added the literal token REBUNDLE_PROBE_TOKEN_2026 to the leading docblock so a grep would tell me whether the bundle was regenerated.
  2. Source mtime advances: source: May 6 13:09:16 2026.
  3. 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's start method and produced an error message that came from the existing source (so the bundle was loaded), then fast-failed on the bogus issue number.
  4. Inspect the bundle file: bundle: May 6 11:37:32 2026 — mtime unchanged.
  5. grep -c "REBUNDLE_PROBE_TOKEN_2026" .swamp/bundles/<hash>/issue-lifecycle/issue-lifecycle.js → 0 matches.

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, source

No 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:

  1. Detect source mtime newer than .swamp/bundles/<hash>/<file>.js and rebundle on the next run, or
  2. Expose swamp extension rebuild <manifest-path> (or a --force-rebundle flag on swamp 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:

  1. W3 (#1322) — ReconcileFromDisk + freshness-as-aggregate-query. buildIndex() now always runs SHA-256 content-fingerprint checks via findStaleFiles() before loadSingleType() can access the catalog. This is the core fix — source changes are detected and rebundled in the warm-start path regardless of mtime.

  2. #265 (#1327) — BundleResult + fromCache fingerprint preservation. When bundleWithCache returns a cached bundle (e.g. bare-specifier build failure), rebundleAndUpdateCatalog now preserves the old fingerprint instead of poisoning the catalog. This ensures findStaleFiles retries on the next invocation.

  3. #1288 — recoverMissingBundle ENOENT fallback. Manual bundle deletion (rm -rf .swamp/bundles/<hash>) now triggers an on-demand rebundle from source instead of the No such file or directory error 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.