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 fromswamp repo init
Package shape
@swamp/gcp/cloudresourcemanagerprovides 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: {…} }] }, addingget_observed,create_observed,update_observedandmove_observedmodels/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.tsalso ships a second time under the package'sreports/, 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_deletionExpected
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.tsreloadPulledExtensions()(≈L140–160) goes through each lockfile entry's catalog rows. For eachmodelrow it callsmodelRegistry.invalidateType(),removeAttachedExtensionsForType(),registerLazy()andensureTypeLoaded(). Rows withkind = 'extension'are not handled in that loop: the catalog stores extension files askind = extensionwith the target type intype_normalized.- Reloading
@swamp/gcp/cloudresourcemanager'sprojectsrow therefore removes every extension attached toprojects(removeAttachedExtensionsForTypedeletes the whole per-type map inmodel_kind_adapter.ts). It then relies on re-attachment throughExtensionLoader.ensureTypeLoaded()/findExtensionsForType()(extension_loader.ts≈L693) orattachPendingExtensionsForType()(model_kind_adapter.ts≈L825). - Both re-attach paths loop over every
findExtensionsForType()row, yet onlygcp_projects_observed.tsfails to come back. We have not pinned down why. Candidates: the module cache returns the already-imported bundle forgcp_projects_observed.tsand the extend step skips it; the extension's methods come from a spread (...crmObservedMethods("projects")); or the duplicategcp_project_offboarding.tsunderreports/shifts ordering. A fresh start attaches both correctly.
Related observation (not reproduced in isolation)
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).
Related
- swamp-club#123 (shipped): extensions dropped when the base type is not yet registered at scan time.
- swamp-club#124 (closed): in-body
methodsonexport const modelnot re-registered. - swamp-club#2092 (shipped):
serve reloadand pulled-extension workflows.
Shipped
Click a lifecycle step above to view its details.
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.