Relationships
#2562 extensions: no defined winner when two extensions add the same method to a type; pulled beats local
Opened by stack72 · 9/26/2026· Shipped 9/27/2026
Summary
When two export const extension files add the same method name to one model type, only one of them can attach. The other is refused with method '<name>' already exists on '<type>'; method not registered. Nothing defines which one wins:
- Between two extensions of the same origin, the winner follows catalog write order (first written wins), not path order. It survives restarts and even a catalog rebuild, so it is stable but arbitrary.
- Between a local extension and a pulled one, the pulled extension wins on every load path. The local method is silently dropped apart from the warning. That is the opposite of the
local > source-mounted > pulledprecedence thatresolveOriginConflicts(extension_catalog_store.ts) applies to primary model types. A user who writes a local extension to override a pulled package's method gets the pulled one.
Surfaced during swamp-club#2557. Before #2643, extension files in one package that shared a target type were tombstoned by the aggregate's I2 rule, so within one package the alphabetically first file won by accident. Now all of them attach and this ordering decides.
Reproduction
Binary: main at 52a3a822 (swamp-club#2557 fixes), compiled from source.
A — two local extensions add probe to a local type @dupdemo/base. zz_ext.ts was written first and aa_ext.ts second.
| Load path | Winner |
|---|---|
| cold start, warm starts | zz_ext.ts |
| after editing zz, after editing aa | zz_ext.ts |
| catalog deleted, cold start, next start | zz_ext.ts |
| serve start, serve reload | zz_ext.ts |
The catalog rowid order is zz_ext.ts (2), then aa_ext.ts (3). findExtensionsForType orders by type_normalized, which is the same for every row, so ties fall back to storage order.
B — pulled @swamp/gcp-organization@2026.09.24.1 adds get_observed to @swamp/gcp/cloudresourcemanager/projects. A local extensions/models/local_observed.ts adds get_observed to the same type. The pulled description ("Read a project with optional GCP request status/timing") wins on start, warm start, cold start after deleting the catalog, serve start, and serve reload. The local extension never wins.
Repro script: /tmp/swamp-repro-dup-methods/repro.sh on the reporter's machine. Rebuild it from the steps above.
Expected
Define and enforce a deterministic winner for extension-method collisions that does not depend on write or attach order:
- Base-model methods always win. That already holds and should stay.
- Between extensions, origin precedence matches primary types: local, then source-mounted, then pulled.
- Within the same origin, a stable tie-break such as the canonical source path.
The warning should name both files and which one won.
Design notes
- Attach happens in several places:
ExtensionLoader.loadSingleType(all extensions for a type, catalog order),modelKindAdapter.attachPendingExtensionsForType(incremental, after a stale rescan or auto-install), and the full-load path viaprocessSecondaryExport. SortingfindExtensionsForTypealone does not help the incremental path. A higher-precedence extension that arrives later finds the method already taken, becausemodelRegistry.extendrefuses to replace a method. A fix probably needs per-method provenance (which extension contributed each method) so that a higher-precedence extension can replace a lower-precedence one, or a full re-attach of the type when a higher-precedence extension appears. resolveOriginConflictsalready has the pulled-path test, including the managedconfig/pulled-extensionsroot, and can be reused for origin rank.- Changing the winner is user-visible for anyone who has collisions today. It should be called out in release notes, and the warning text should make the outcome obvious.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.