Relationships
#2724 An extension upgrade that hits a type collision is rolled back to no version: roll back to the prior version with a commit/rollback install handle
Opened by hammz · 9/29/2026· Shipped 9/30/2026
Summary
When an extension upgrade hits a DuplicateTypeError in the catalog save, the rollback leaves no version installed. InstallExtensionService.rollbackOnCollision (src/libswamp/extensions/install_extension_service.ts:372) deletes createdPaths and restores the prior lockfile entry. But createdPaths includes every file written into the extension root, including files that overwrote v1's (see InstallResult.createdPaths, pull.ts ~139). And v1-only files were already pruned as orphans. The lockfile then says v1 is installed while neither v1's nor v2's files are on disk, and the same holds for dependencies installed with it.
With stage and swap (swamp-club#2723), the old roots are kept until commit, so a rollback can put the prior version back exactly.
This is the second half of unit U4 of the swamp-club#2612 split (see the planning ripples there). It affects every repo and ships on its own. #2612 needs it because archives are stored, and superseded archives are deleted, only in commit(). Depends on swamp-club#2723.
Scope
PendingInstallhandle returned byapplyInstallin place of #2723's commit at the end of apply. It covers the top-level install and every dependency installed with it.commit(): delete each staging folder, includingold/, and mark the journal committed. #2612 later adds storing the archive and deleting the superseded archive here.rollback(): reverse each swap, dependencies first and then the top level, in reverse install order. For each entry whosewriteEntryran, restore the prior lockfile entry with every field (include,checksum,filesChecksum,serverUrl,channel,pulledAt), or remove the entry if there was none. Skills fall back to deletingcreatedPathsas today.- Both are idempotent. The journal records
committed/rolled-backso #2723's recovery treats them as finished.
installExtension(the direct caller, e.g. the auto-resolver atsrc/cli/auto_resolver_adapters.ts:309) commits right after apply, as in #2723.InstallExtensionService.executeholds the handle across phase 8, under the #2709 lock:saveAllsucceeds →commit().DuplicateTypeError→rollback(), then the existing user error.rollbackOnCollisionis deleted.- Other phase-8 faults (bundling in
buildExtensionFromDisk, or a genericsaveAllfailure) →commit()then rethrow with today's "Install partially applied" guidance, as the #2612 plan decided. While implementing, reconsider the bundling failure, which happens before the catalog is touched: rolling back there looks strictly safer. If you change it, say so in the PR.
- Lockfile: rollback re-reads with
refresh()(#2709) before restoring, and never removes an entry this install did not write. - Upgrade path:
UpgradeExtensionService,update, and serve's install handler inherit this throughInstallExtensionService. Confirm with a test for each.
Tests
- Unit: a v1→v2 upgrade whose catalog save throws
DuplicateTypeErrorends with v1's exact tree, bundles and lockfile entry (includingchannelandpulledAt). This test fails onmaintoday. - Unit: the same with a dependency installed in the same call. The dependency's files and entry are removed; a dependency that was already installed is untouched.
- Unit: first install plus collision → no files and no entry remain, as today.
- Unit:
commit()androllback()are idempotent, and recovery (#2723) treats committed and rolled-back journals as finished. - Unit: a generic phase-8 fault commits and keeps today's message.
- Integration: a collision during
extension updatein a temp repo leaves the repo exactly as it was before the command.
Docs
design/primitives/extensions.md: replace the "FS rollback on DuplicateTypeError" description and the crash-state posture with the commit/rollback handle.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.