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

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 > pulled precedence that resolveOriginConflicts (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:

  1. Base-model methods always win. That already holds and should stay.
  2. Between extensions, origin precedence matches primary types: local, then source-mounted, then pulled.
  3. 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 via processSecondaryExport. Sorting findExtensionsForType alone does not help the incremental path. A higher-precedence extension that arrives later finds the method already taken, because modelRegistry.extend refuses 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.
  • resolveOriginConflicts already has the pulled-path test, including the managed config/pulled-extensions root, 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.
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 8 MOREREVIEW+ 20 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

9/27/2026, 12:28:29 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack729/26/2026, 12:24:24 AM

Sign in to post a ripple.