#265 Locally-sourced extension: source_mtime updates without regenerating stale bundle
Opened by bixu · 5/6/2026· Shipped 5/6/2026
Description
When a locally-registered extension source (swamp extension source add <path>) is modified on disk, swamp's catalog (_extension_catalog.db, table bundle_types) updates the row's source_mtime to match the new file but does not regenerate the cached JS bundle at bundle_path, nor refresh the version field. As a result, swamp model type describe, swamp model method run, etc. continue to load the stale pre-edit bundle indefinitely, even though the catalog "noticed" the change.
The user-visible symptom: methods you just added to your extension don't appear in swamp model type describe, and swamp model method run <model> <new-method> fails with Unknown method '<name>' for type '...'. Available methods: ....
Steps to reproduce
- Register a local source:
swamp extension source add /path/to/checkout. Confirmswamp model type describe @your/type --jsonreturns the expected method list. - Edit the model's TypeScript to add a new method (export a handler, register it in
model.methods.<newMethod>, bump the model'sversionfield) and save. - Run
swamp model type describe @your/type --json(or any command that consults the type).
Expected: catalog detects the source change, re-bundles, and the new method appears.
Actual: the catalog's source_mtime updates to match the new file, but bundle_path keeps pointing at the old .swamp/bundles/<hash>/.../*.js (which lacks the new method). The version column also stays at the prior value. describe shows the old method list, and method run <new> fails with Unknown method.
Real-world incident
Hit while iterating on @hivemq/mudroom (swamp-extensions/extensions/models/mudroom). Added ensureContainerCli to host_install.ts and registered it in mudroom.ts's model.methods. A host wrapper script then called swamp model method run <name> ensureContainerCli, which failed:
Unknown method 'ensureContainerCli' for type '@hivemq/mudroom'.
Available methods: prepareHost, up, exec, down, destroy, provisionGuest, ...Catalog state at the time:
sqlite> SELECT type_normalized, version, source_mtime, bundle_path
FROM bundle_types WHERE type_normalized = '@hivemq/mudroom';
@hivemq/mudroom|2026.05.06.1|2026-05-06T14:49:54.092Z|.../.swamp/bundles/6da0c2ac/mudroom/mudroom.jssource_mtime matched the latest source file mtime, but version was still 2026.05.06.1 (current source declared 2026.05.06.3), and bundle_path pointed at a bundle artifact whose mtime was hours older than the source. Inspecting the cached mudroom.js confirmed the new ensureContainerCli symbol was absent.
Workaround
Manually invalidate the catalog row and bundle:
sqlite3 <repo>/.swamp/_extension_catalog.db \
"DELETE FROM bundle_types WHERE type_normalized = '@your/type';"
rm -rf <repo>/.swamp/bundles/<stale-hash>The next swamp command re-bundles correctly and the new methods show up.
Suggested fix
When the indexer observes that source_mtime has advanced past the recorded value, it should treat that as bundle invalidation: recompute source_fingerprint, and if it differs from the stored value, regenerate the bundle and update bundle_path, version, and source_fingerprint atomically. As-is, partially updating only source_mtime silently masks the staleness — the row looks fresh by mtime but serves stale code.
Environment
- swamp version:
20260505.231643.0-sha.5a337b81(perswamp help) - macOS Darwin 25.4.0, Apple Silicon (arm64)
- Repo type: locally-registered extension source (not a pulled extension)
- Storage backend: default SQLite (
_extension_catalog.db)
Shipped
Click a lifecycle step above to view its details.
stack72 commented 5/6/2026, 10:01:27 PM
Root cause
The bug is a fingerprint poisoning issue in the warm-start rebundleAndUpdateCatalog → bundleWithCache flow.
When bundleWithCache cannot regenerate a bundle — either via the isExpectedBundleFailure fast-path (bare specifiers + no deno.json) or the error-fallback path (bundle build failed, cached bundle returned) — it returns the old cached JS. The caller rebundleAndUpdateCatalog then unconditionally writes the new source_fingerprint to the catalog. This poisons the freshness check: findStaleFiles compares computeSourceFingerprint(currentFile) against the catalog's stored fingerprint, finds them equal, and never retries the bundle. The extension is permanently stale with no user-visible indication.
Fix (PR #1327)
Added a kind-agnostic BundleResult type to bundle_freshness.ts. When bundleWithCache returns a cached bundle (fromCache: true), rebundleAndUpdateCatalog now preserves the catalog's stored fingerprint instead of writing the new one. This keeps the file stale so findStaleFiles retries on the next warm-start invocation.
A structured warning fires only on the fallback case (fingerprint actually differs from catalog), not on legitimate cache hits. Applied across all 5 extension loaders.
Verification
Before/after comparison confirmed: fingerprint no longer advances when the bundle isn't rebuilt. System retries every warm-start and self-heals when the user fixes the build issue.
Deferred work
- #270: warm-start state oscillation (Indexed vs BundleBuildFailed). Harmless but catalog lies. Deferred to W4.
- #271: sourceToRow empty source_mtime. Informational, not load-bearing. Deferred to post-W4.
Sign in to post a ripple.