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

#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

  1. Register a local source: swamp extension source add /path/to/checkout. Confirm swamp model type describe @your/type --json returns the expected method list.
  2. Edit the model's TypeScript to add a new method (export a handler, register it in model.methods.<newMethod>, bump the model's version field) and save.
  3. 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.js

source_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 (per swamp 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)
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 10 MOREREVIEW+ 1 MOREIMPLEMENTATIONCOMPLETE

Shipped

5/6/2026, 9:43:03 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack725/6/2026, 7:56:50 PM
Editable. Press Enter to edit.

stack72 commented 5/6/2026, 10:01:27 PM

Root cause

The bug is a fingerprint poisoning issue in the warm-start rebundleAndUpdateCatalogbundleWithCache 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.