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

Relationships

#2557 serve: `serve reload` drops `export const extension` methods from a pulled package, while other extensions on the same type survive

Opened by swamp_lord · 9/25/2026· Shipped 9/26/2026

Summary

A pulled package ships two export const extension files that both add methods to the same model type, which comes from another pulled package. After swamp extension pull and swamp serve reload, one file's methods are attached again and the other file's methods disappear. A workflow step that calls them then fails with Unknown method '<name>'. Restarting serve restores them.

Environment

  • swamp 20260925.013241.0-sha.4489c40d (release binary), unmodified
  • local filesystem datastore (no remote datastore, no managed config)
  • swamp serve --hot-reload, fresh repo from swamp repo init

Package shape

  • @swamp/gcp/cloudresourcemanager provides the model type @swamp/gcp/cloudresourcemanager/projects

  • @swamp/gcp-organization (private; depends on the above) ships two extensions to that type:

    • models/extensions/models/gcp_projects_observed.ts: export const extension = { type: "[@swamp](/u/swamp)/gcp/cloudresourcemanager/projects", methods: [{ ...crmObservedMethods("projects"), get_observed: {…} }] }, adding get_observed, create_observed, update_observed and move_observed
    • models/extensions/models/gcp_project_offboarding.ts: export const extension = { type: "[@swamp](/u/swamp)/gcp/cloudresourcemanager/projects", methods: [{ inspect_offboarding: {…}, request_project_deletion: {…} }] }

    gcp_project_offboarding.ts also ships a second time under the package's reports/, because a report imports it.

Steps to reproduce

swamp repo init repo
swamp serve --repo-dir repo --host 127.0.0.1 --port 19171 --hot-reload &
swamp extension pull @swamp/gcp-organization@2026.09.24.1 --server http://127.0.0.1:19171 --yes --force
swamp serve reload --server http://127.0.0.1:19171
swamp model type describe @swamp/gcp/cloudresourcemanager/projects --server http://127.0.0.1:19171 --json \
  | jq '[.methods[].name] | {get_observed: index("get_observed") != null, inspect_offboarding: index("inspect_offboarding") != null}'

Observed

Step get_observed (gcp_projects_observed.ts) inspect_offboarding (gcp_project_offboarding.ts)
pull 2026.09.24.1 through serve, then serve reload missing present
pull 2026.09.25.1 through serve, then serve reload missing present
restart serve present present

It reproduces the same way with both package versions. All four methods from gcp_projects_observed.ts are missing, not only one. Nothing about gcp_projects_observed.ts is logged during the reload, even at --log-level debug. Other extensions in the same package are attached again, for example a gcp_hierarchy.ts extension on a sibling type logs method 'inventory_hierarchy' already exists ….

A workflow step that calls the method then fails:

Unknown method 'get_observed'. Available methods: create, get, update, delete, sync,
list, …, inspect_offboarding, request_project_deletion

Expected

After serve reload, every export const extension in the pulled packages is attached to its target type, the same as after a fresh start.

Code notes (4489c40d)

  • src/serve/extension_reload.ts reloadPulledExtensions() (≈L140–160) goes through each lockfile entry's catalog rows. For each model row it calls modelRegistry.invalidateType(), removeAttachedExtensionsForType(), registerLazy() and ensureTypeLoaded(). Rows with kind = 'extension' are not handled in that loop: the catalog stores extension files as kind = extension with the target type in type_normalized.
  • Reloading @swamp/gcp/cloudresourcemanager's projects row therefore removes every extension attached to projects (removeAttachedExtensionsForType deletes the whole per-type map in model_kind_adapter.ts). It then relies on re-attachment through ExtensionLoader.ensureTypeLoaded() / findExtensionsForType() (extension_loader.ts ≈L693) or attachPendingExtensionsForType() (model_kind_adapter.ts ≈L825).
  • Both re-attach paths loop over every findExtensionsForType() row, yet only gcp_projects_observed.ts fails to come back. We have not pinned down why. Candidates: the module cache returns the already-imported bundle for gcp_projects_observed.ts and the extend step skips it; the extension's methods come from a spread (...crmObservedMethods("projects")); or the duplicate gcp_project_offboarding.ts under reports/ shifts ordering. A fresh start attaches both correctly.

On a long-running instance (several pulls, reloads and restarts), 7 of one package's 39 sources were left Tombstoned in _extension_catalog.db (swamp doctor extensions --verbose), including gcp_projects_observed.ts. After that, even a restart did not restore the methods. A clean instance going through the same version upgrade did not end up in that state. This may be a separate catalog issue; swamp doctor extensions --repair is the expected recovery.

Workaround

Restart serve after pulling a package that adds extensions to another package's types (when the catalog has no tombstoned sources).

  • swamp-club#123 (shipped): extensions dropped when the base type is not yet registered at scan time.
  • swamp-club#124 (closed): in-body methods on export const model not re-registered.
  • swamp-club#2092 (shipped): serve reload and pulled-extension workflows.
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 11 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

9/26/2026, 12:13:47 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack729/25/2026, 10:03:12 PM
Editable. Press Enter to edit.

stack72 commented 9/26/2026, 12:14:06 AM

Fixed in swamp-club/swamp#2643 (merged).

Root cause: the reload loop was fine. The extension aggregate invariant I2 kept only one extension file per target type within a package and tombstoned the rest, so a pull wrote no catalog row for them, and serve reload re-attaches from the catalog. In @swamp/gcp-organization@2026.09.24.1 that dropped 7 of 23 extension files, which is the 7 Tombstoned sources you saw. Extension files are now exempt from that rule, matching the cross-package check. The same PR also makes one extension that fails to import get skipped with a warning instead of dropping every extension after it on that type.

If an instance was already affected, run swamp doctor extensions --repair (verified to restore all 23), or pull the package again. A single restart only partly heals it: files whose type is a string literal come back, but ones like withGcpVerbosity(...) need a second start. That cold-path gap is tracked in swamp-club#2560.

Sign in to post a ripple.