Skip to main content
← Back to list
01Issue
BugOpenSwamp CLIPublic
AssigneesNone

Relationships

#1737 Retyping a model instance strands catalog rows no command can reclaim — prune matches on model_id, delete resolves the current type (follow-up to #1009)

Opened by esteban · 8/19/2026

Summary

swamp data prune (from #1009) reclaims data whose owning model definition no longer exists. Retyping a model instance produces a different orphan that falls through it: the definition still exists, so the rows look owned, but .swamp/data is keyed by <collective>/<type>/<uuid> — so changing the instance's type: strands both its data tree and its catalog rows under the old type_normalized.

Those rows are reachable by no management command, while remaining fully visible to data query, which is the one read path workflows actually use.

Reproduction

  1. Have a model instance with data, e.g. type @ourcollective/old-type, name story-analyzer.
  2. Change its type: to another type — for us @twonines/redmine-story-status, same uuid, after adopting a published extension in place of a local one.
  3. Write new data, so a live tree exists alongside the stranded one.
  4. Try to reclaim the old rows.
Command What happens to the stranded rows Why
swamp data prune ignores them it matches on model_id, which a retype does not change, so the rows look owned by a live definition
swamp data delete <model> <name> No data named "<name>" exists for model story-analyzer resolves the model name to its current type, so it never looks under the old type_normalized
swamp data list <model> hides them also resolves by current type
swamp doctor extensions overallStatus: pass, orphanFiles: [] orphan detection covers extension files, not catalog rows
swamp data query '<cel>' returns them matches on tags (modelName, specName), which the stranded rows still carry

We hit this with 380 rows across two retyped instances (335 + 45). After deleting the stranded files by hand, data query 'modelName == "story-analyzer"' still answered with all six data names as isLatest: true and content: {}.

Why it is more than cosmetic

Two rows shared a name — the stranded one at v27 and the live one at v1 — and both reported isLatest: true, with the stale one sorting first. So:

data.query('modelName == "story-analyzer" && specName == "analysis"')[0]

selects the dangling row, with empty content, in preference to live data. Our workflow escaped this only because all twelve of its queries also pin workflowRunId == run.id — pinning we had added for an unrelated reason. Anything using the documented [0] shape without a run pin silently reads the wrong row after a retype.

The only cleanup available

Hand-editing the catalog, which is exactly what a datastore command should exist for:

sqlite3 .swamp/data/_catalog.db "PRAGMA wal_checkpoint(TRUNCATE);"
sqlite3 .swamp/data/_catalog.db ".backup '<somewhere safe>/_catalog.db.bak'"
sqlite3 .swamp/data/_catalog.db "DELETE FROM catalog WHERE type_normalized LIKE '@ourcollective/%'; VACUUM;"

Afterwards: catalog 1090 -> 710 rows, integrity_check ok, .swamp/data 68 MB -> 35 MB, data query returning only live rows with content, doctor extensions pass, workflow validate passed, 29 tests green.

Suggested fix

  1. Have prune match on (namespace, type_normalized, model_id) against the registered definition rather than model_id alone, so a retype makes the old rows reclaimable through the command that already exists for this. This is the direct extension of #1009 to the retype case.
  2. Failing that — or in addition — data query should not return rows whose backing file is absent. A row that answers isLatest: true with content: {} is worse than a missing row, because it wins a [0] selection. This half matters even with fix 1, since the window between retype and prune is unbounded.

A --dry-run on whatever form this takes would be welcome, as #1009 suggested.

Environment

  • swamp 20260809.004828.0-sha.b61c9de2, Deno 2.8.3, Linux x86_64
  • Filesystem datastore, single namespace, catalog schema as shipped
  • Follow-up to #1009 (orphaned data from a removed/migrated model — shipped); the retype path is the case its fix does not reach
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

8/19/2026, 9:07:27 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.