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

Relationships

#3690 v3: a checkout holding v2 records joins a v3 datastore through .swamp.yaml unguarded, overwriting its records and diverging for good (bug hunt group 5)

Opened by stack72 · 10/20/2026· Shipped 10/21/2026

Background

swamp is reworking its datastore layer (tracking: swamp-club#2865). A v3 datastore starts empty: datastore setup … --format 3 refuses a repository whose .swamp/ already holds records, because v3 records live in the commit log and a checkout's files are rebuilt from it (design/enablers/datastores.md, "a v3 datastore starts empty"). Every checkout of a v3 datastore should then hold the same records.

A v3 bug hunt (2026-10-16) found that that check runs only at setup. The other way into a v3 datastore, a teammate pulling the committed .swamp.yaml that says format: 3, has no check at all. It's the common way a team adopts v3: one person runs setup, commits .swamp.yaml, and everyone else pulls.

Related fixes already shipped:

  • swamp-club#3617: a v2 setup no longer adopts a v3 directory;
  • swamp-club#3642: a never-restored copy refuses to read.

This is the remaining direction: a checkout with v2 records opening a v3 datastore.

The bug (V3-18, S2)

A checkout holding v2 records joins a v3 datastore through .swamp.yaml with no guard. Its records are silently overwritten or left mixed, and its checkouts diverge for good.

Reproduced in the hunt (filesystem v3; the same open path serves S3/GCS since swamp-club#3534):

  1. Checkout A sets up v3 and commits versions of m1/state.
  2. Checkout B, which already holds local v2 records for the same model, pulls A's .swamp.yaml and runs any command.
  3. Results:
    • B's local v1 is overwritten by A's commit;
    • A lists versions [1, 4] while B lists [1, 2, 3, 4];
    • data get --version 3 in B returns B's pre-join v2 content, which no commit holds;
    • in B, data list (catalog says v4) disagrees with data versions;
    • B's next write takes v4, and the checkouts never converge.

Why: the records check exists only in setup (setupV3Filesystem / the bucket setup, src/libswamp/datastores/setup.ts "holds records. A v3 datastore starts empty"). Opening a v3 context (v3RepositoryFactoryConfig in src/infrastructure/datastore_v3/v3_repo_context.ts) only catches up, and catch-up never removes a file no commit owns. Version listing and allocation also read version directories on disk.

Trigger: any teammate with existing local data pulling the commit that moves the repo to v3. The step-by-step v3 adoption the docs describe leads straight into it.

The hunt's verifier rated it S3. I've raised it to S2: it silently overwrites a user's records and leaves checkouts diverged with no supported way back.

What to do

  1. Check at the first v3 open of a checkout. The first time a checkout opens a v3 datastore (no v3 read-model state yet under .swamp/), run setup's records check against the checkout's .swamp/. Reuse the same function, so "holds records" means the same thing in both places.
    • If the checkout holds records, refuse before catching up, writing nothing, with a message that names:
      • what was found;
      • that a v3 datastore starts empty;
      • the way out.
  2. The way out is a supported command, not a hand edit. Recommend swamp doctor datastores --repair, which moves the checkout's v2 records aside (to .swamp/_read_model_v3/at-risk/ or a dated .swamp/pre-v3-records/) and lets the next command catch up. Reuse swamp-club#3617's at-risk mechanism. Say in the message that the moved records aren't in the v3 datastore and how to bring them in, once v3 migration (datastore rework Phase 6) exists. Deleting them isn't a supported path.
  3. Both storages: the check runs on filesystem v3 and on S3/GCS v3 (the same open path).
  4. Never on later opens: once a checkout has v3 state, the check doesn't run again (no per-command cost). A checkout whose v3 state was deleted by hand counts as a first open and is checked again.
  5. Docs: design/enablers/datastores.md ("joining a v3 datastore") and the swamp skill's datastore reference: the join path, the refusal, and the repair.

Tests

Each must fail on main first.

  • Integration, filesystem v3:
    • checkout A sets up v3 and commits;
    • checkout B holds v2 records for the same model and opens with A's .swamp.yaml: refused, nothing written, B's files unchanged;
    • after doctor datastores --repair, B catches up and lists exactly A's versions, and the moved records are where the message said.
  • The same on a bucket: the opt-in S3 run in integration/datastore_v3_bucket_test.ts, or the in-memory remote.
  • No false refusal: a checkout with an empty .swamp/ (or only always-local subdirectories: logs, telemetry, bundles…) joins as today.
  • Later opens: a checkout with v3 state is not checked again.
  • swamp-uat passes unchanged. A swamp-uat journey for the join can follow (v3 datastore tests).

Severity: S2. Silent overwrite of a user's records and permanent divergence on the documented adoption path. It must land before v3 is dogfooded (datastore rework Phase 4 I).

Blocked by: nothing (swamp-club#3617 has shipped).

02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 14 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/21/2026, 12:13:46 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/20/2026, 9:13:21 PM

Sign in to post a ripple.