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

Relationships

#3691 v3 namespaces: cross-namespace reads return the reader's own content, a namespaced local tier is overwritten by solo commits, and namespaces can collide with swamp's own .swamp/ directories (bug hunt group 7)

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

Background

swamp is reworking its datastore layer (tracking: swamp-club#2865). On a v3 datastore each namespace's records are materialised under their own cache root in every checkout: v3CacheRoot(repo, ns), which is .swamp/<ns>, or .swamp/ for the solo namespace "". A checkout's local tier (subdirectories its directories setting keeps out of the datastore) belongs to that checkout and is never namespaced.

A v3 bug hunt (2026-10-16) found that namespace cache roots collide with each other, with the local tier, and with swamp's own .swamp/ directories, and that cross-namespace reads use the wrong root. These are five findings with one theme. The code they cite is still present on current main; line numbers have moved since #3617, so the function names are given. Reproduce each against main first. The hunt's repro files aren't in the repo, so rebuild them from the descriptions below.

The bugs

1. Cross-namespace reads return another namespace's content, or nothing (V3-04, S2).

  • Reproduced:
    • in checkout alpha, data.latest("beta:m1", "state") and data query 'namespace == "beta"' return a record labelled beta with alpha's attributes ({who: "alpha"}) when the model ids are shared;
    • with different ids, they return empty attributes even though .swamp/beta/data/…/raw exists in alpha's checkout.
  • Why:
    • fromRow (src/domain/data/data_record_mapper.ts) reads content with dataRepo.getContentSync(type, modelId, name, version) on the reader's own namespace path and ignores row.namespace;
    • hydrateForeignRecord (data_query_service.ts) returns early when attributes are non-empty, and otherwise relies on fetchForeignContent, which v3 filesystem datastores never wire;
    • the read model puts every namespace's rows into each checkout's catalog, so foreign rows are always there.
  • Trigger: one repo per namespace (staging/prod) sharing definition ids, with a workflow or CEL expression reading data.latest("prod:model", …) from staging. The values come back silently wrong.

2. A namespaced checkout's local tier aliases the solo namespace's cache root (V3-06, S1 narrow).

  • Reproduced:

    • checkout B (namespace alpha, directories without workflow-runs) ran nothing, yet findAllGlobal() returns the solo checkout's run;
    • B's local-only m1/state v1 {who: "alpha"} is overwritten on disk by the solo commit {who: "solo"};
    • getLatestRecord in alpha returns {who: "solo"}.

    B's local data was never committed, so the loss is permanent.

  • Why:

    • the v3 path resolver (default_datastore_path_resolver.ts) puts the local tier at .swamp/<subdir>, with no namespace;
    • v3CacheRoot(repo, "") is .swamp/, the same directory;
    • the read model materialises every namespace's keys without checking this checkout's directories filter, and ownership is keyed by (namespace, key), so B's local file has no owner and is overwritten.
  • Trigger: a namespaced checkout using datastore setup --directories on a v3 datastore shared with a solo checkout of the same repo, so with the same definition ids.

3. A symlinked .swamp/data or .swamp/outputs wedges that checkout (V3-25, S3). The read model's path guard (sqlite_read_model.ts, the PathTraversalError in its target resolution) uses realpath(.swamp) as the boundary, while the writer's assertSafePath uses .swamp/data. With a symlinked subdirectory, the commit lands and then every catch-up in that checkout throws. Only the symlinked checkout is affected, not the others.

  • Is it still a wedge after #3617? That made damaged chunks a per-key failure. Check whether a path error still fails the whole catch-up.

4. Namespace names that match swamp's own .swamp/ directories can be claimed (V3-31, S3). RESERVED_NAMESPACE_NAMES (src/domain/data/namespace.ts) is DEFAULT_DATASTORE_SUBDIRS ∪ ALWAYS_LOCAL_SUBDIRS. It omits the rest of SWAMP_SUBDIRS (definitions, workflows, vault, datastore-bundles, …). Reproduced:

  • with namespace vault or definitions, repo upgrade (migrateFromSymlinks) moves the namespace's records into the git-tracked vaults/ or models/ directories in every checkout;
  • with datastore-bundles, extension bundle eviction deletes the namespace's files.

5. A reserved namespace accepted by v3 setup makes solo recovery delete committed files (V3-32, S3).

  • Why: setupV3Filesystem (src/libswamp/datastores/setup.ts) has no isReservedNamespaceName check (the v2 path has one), and restoreNamespace accepts reserved names.
  • Reproduced: with namespace data, its commits materialise under .swamp/data, the solo namespace's data directory. A solo checkout's recovery sweep (#sweepUnowned for namespace "") then deletes them as unowned, for good.

What to do

  1. Cross-namespace reads (1): resolve a foreign row's content from row.namespace, using the local materialisation under v3CacheRoot(repo, row.namespace), for example a local foreign-content fetcher wired for v3. Never read the reader's own path when namespaces differ. Key the hydration on the namespace, not on whether the attributes are empty.

  2. The local tier vs the solo cache root (2). Pick one, and say which:

    • (Recommended) refuse, at setup and at v3 open, a namespaced checkout on a datastore that also has the solo namespace, when its directories omit a materialised subdirectory. That's the narrowest change, beside the existing directories check in v3_repo_context.ts.
    • or never materialise another namespace's key whose path lands in this checkout's local tier.

    Either way, a local-only file is never written by another namespace's commit. Check the solo-with-directories variant the verifier flagged: two solo checkouts with different directories.

  3. Reserved names (4, 5):

    • reserve every first-level .swamp/ name swamp owns, derived from SWAMP_SUBDIRS so new directories are reserved automatically;
    • refuse a reserved name in v3 setup (filesystem and bucket), in namespace set, and at v3 context open for an existing binding, with the existing reserved-name message;
    • make migrateFromSymlinks and bundle eviction skip namespace cache roots;
    • make the solo recovery sweep skip other namespaces' cache roots.
  4. Symlinked subdirectories (3): treat a resolved path outside the boundary as a refused key (logged once, skipped), not a retried apply error; or align the writer's and the read model's boundaries (realpath per subdirectory). Prefer refusing a symlinked materialised subdirectory at v3 open with a clear message, so the writer never commits what the reader can't apply.

  5. Docs: update design/enablers/datastores.md on namespace cache roots, the reserved names, and the local tier.

Tests

Each must fail on main first. Two checkouts on one v3 datastore in-process (filesystem; one bucket case where the path differs).

  • 1: alpha reads beta:m1 with shared and with distinct ids, and gets beta's content, through both data.latest and data query.
  • 2: a namespaced checkout with a local-only subdirectory beside a solo checkout is refused (or keeps its local file byte-for-byte after the solo commit, per the option chosen). Add the solo-with-directories variant.
  • 3: a symlinked .swamp/data is refused at open, or its key is skipped without wedging. Other checkouts are unaffected.
  • 4: namespaces vault, definitions, workflows, datastore-bundles are refused at setup and namespace set. repo upgrade and bundle eviction leave an existing namespace cache root alone.
  • 5: v3 setup refuses namespace data. A solo recovery never removes another namespace's committed file.
  • A property test: no namespace name namespace set accepts maps v3CacheRoot onto a swamp-owned .swamp/ directory.
  • swamp-uat passes unchanged.

Severity: S1 (narrow: permanent loss of local-only data) and S2 (silently wrong cross-namespace reads); the rest S3. It must land before v3 is dogfooded (datastore rework Phase 4 I).

Blocked by: nothing (swamp-club#3617 has shipped). The recovery-sweep scope issue (bug hunt group 8, not filed yet) also changes sqlite_read_model.ts, and follows this one.

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

Shipped

10/21/2026, 12:50:26 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/20/2026, 9:12:57 PM

Sign in to post a ripple.