Relationships
#2563 Partial extension catalog saves delete rows for sources mounted from outside the repo
Opened by stack72 · 9/26/2026
Description
Found while working on swamp-club#2355. ExtensionRepository.saveAll always runs ExtensionCatalogStore.pruneUnreachableSources (src/infrastructure/persistence/extension_catalog_store.ts). It deletes every non-tombstoned catalog row whose source path is outside the repo root, except rows belonging to extensions in that same save.
Extensions mounted through .swamp-sources.yaml can live outside the repo root (a sibling checkout, or the git main worktree when the repo is a worktree). Any save that does not include their aggregate therefore deletes their catalog rows. Today that covers every partial save: extension pull (InstallExtensionService), extension rm (RemoveExtensionService) and upgrade (UpgradeExtensionService). The full cold-start reconcile is safe because it saves every aggregate.
Confirmed with a probe during #2355. A row for a live source outside the repo root was present before a partial saveAll and gone after it.
The in-memory registry of a running process is unaffected. The rows stay missing until something re-indexes those sources. That should be the next buildIndex or full reconcile, but it is not verified.
#2355 avoids triggering this from its new serve-reload path: its scoped reconcile calls saveAll with pruneUnreachable false. The pull, rm and upgrade paths still prune.
Why the obvious fix is wrong
Changing the prune to keep an out-of-root row while its source file still exists was tried in #2355 and reverted. The prune also repairs a copied or moved repo. If a repo at path A is copied to path B and A still exists, the rows for A would no longer be pruned by partial saves. resolveOriginConflicts treats paths outside the pulled-extensions dir as local, and local wins, so the stale A rows would clear the type on B's real pulled rows.
Suggested direction
Make the prune mount-aware. Keep an out-of-root row only if its source is under a root that the current .swamp-sources.yaml resolves to (expandSourcePaths and resolveSourceExtensionDirs) and the source still exists. Every ExtensionRepository caller that saves (pull, rm, upgrade, reconcile, serve reload) would then need those mount roots, for example passed at construction.
Steps to reproduce
- In a swamp repo, mount an extension directory from outside the repo root through .swamp-sources.yaml, and let a command catalogue it.
- Run swamp extension pull for any extension.
- Query the extension catalog (.swamp/_extension_catalog.db, table bundle_types): the mounted source's row is gone.
Open
No activity in this phase yet.
Sign in to post a ripple.