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

Relationships

#3306 Cross-namespace data.* reads return empty attributes in model method run and workflow run

Opened by zack · 10/12/2026· Shipped 10/12/2026

Summary

In swamp model method run (and workflow run), a data record read from another namespace through a CEL data.* helper comes back with empty attributes. The row itself is right (namespace, modelId, version, size, contentType are the foreign namespace's), but the bytes are never fetched, so data.latest("security:unique-model", "item-000001").attributes.content fails with No such key: content.

design/enablers/datastores.md (Foreign Catalog Export/Pull, "On-demand content fetch (Phase 6d)") says: when a cross-namespace CEL expression reads attributes whose content is not local, the DataQueryService calls fetchForeignContent through its ForeignContentFetcher callback. That does not happen on this path.

This is what remains of swamp-club#3295 after its fix (#3017, 9a580b97): the namespaced lookup now resolves the right row, and the next thing a caller does with it, read an attribute, fails. It is caught by the swamp-uat test tests/cli/data/namespace/query_test.ts, "swamp data query namespace-qualified latest reads the named namespace's record when the caller has one of the same name", which fails on the [s3] and [gcs] shards.

Steps to reproduce

Two repos on one S3 (or GCS) datastore prefix, swamp 20261012.163314.0-sha.bb132c57:

  1. Repo A: namespace infra. Create model unique-model and write JSON resources item-000000, item-000001. swamp datastore sync --push.
  2. Repo B: namespace security. Create model unique-model and write item-000000 to item-000003 with a different payload. swamp datastore sync --push.
  3. Repo A: swamp datastore catalog pull --namespaces security.
  4. Repo A: a command/shell model whose run is echo 'content=${{ data.latest("security:unique-model", "item-000001").attributes.content }}', then swamp model method run <name> execute --json.

Actual

Expression in methods.execute.arguments.run could not be evaluated:
Invalid expression: No such key: content
> data.latest("security:unique-model", "item-000001").attributes.content

validation_failed, exit 1.

Observed from repo A against an S3 emulator, for security's item-000001:

Read Result
data.latest("security:unique-model","item-000001") metadata namespace=security version=1 contentType=application/json size=32 path="", security's modelId, size(attributes) == 0
data.version("security:unique-model","item-000001",1).attributes.content No such key: content
data.latest("*:unique-model","item-000003").attributes.content No such key: content
data.query('ns == "security" && ...')[0].attributes.content No such key: content
CLI swamp data query 'ns == "security" && ...' --json full payload present: "content": {"index":1,"content":"xxxxxxxx"}
CLI swamp data query ... --select attributes --json {}

The unprojected CLI query shows the bytes are fetchable from repo A; the @swamp/s3-datastore and @swamp/gcs-datastore extensions both implement fetchForeignContent as a getObject on {namespace}/{relPath}. Nothing needs to change in the extensions.

Expected

data.latest("security:unique-model", "item-000001").attributes holds security's payload, as do the other data.* helpers for a foreign row, in model method run and workflow run.

Cause

Two gaps, both needed for the reproduction above to pass (line numbers at bb132c57; the files are unchanged on main):

  1. The fetcher is not wired for run commands. setForeignContentFetcher is called in requireInitializedRepoReadOnly (src/cli/repo_context.ts:1038) and requireInitializedRepo (:1323) but not in requireInitializedRepoUnlocked (:1355), which model method run, workflow run and single-model model evaluate use. So even data.version and wildcard data.latest, which go through the hydrating async query(), get empty attributes. Confirmed by running: those readers fail in model method run while the CLI query succeeds.
  2. getLatestRecord never uses the fetcher. A non-wildcard ns: prefixed data.latest goes model_resolver.ts:1370 -> DataQueryService.getLatestRecord -> buildRecordFromRow (src/domain/data/data_query_service.ts:591). ensureContentOf returns early when the row's namespace is not the repository's, and fromRow reads local disk only. The foreign hydration loop exists only at the end of query() (:786). From reading, not isolated by experiment.

Related, same defect on a third path: swamp data query ... --select attributes returns {} for a foreign record while the unprojected query returns the payload, because the hydration loop only touches unprojected records (projection runs first).

Suggested fix

  • Wire setForeignContentFetcher from syncService in requireInitializedRepoUnlocked, as the other two constructors do. Check any other place that builds a repo context for expression evaluation (serve, workers).
  • Hydrate a foreign JSON row through the fetcher in buildRecordFromRow; the loop in query() could become a per-record helper shared by both, and run before projection so --select attributes is covered.
  • integration/giga_swamp_cel_namespace_test.ts ("Once security holds the record") asserts only namespace and modelId. Add an attributes assertion.
  • design/enablers/data-query.md says CEL data.query() (querySync) reads local disk only, which is at odds with Phase 6d for that helper. Decide which holds and make the docs agree.

Environment

  • swamp 20261012.163314.0-sha.bb132c57, Linux
  • Datastores: @swamp/s3-datastore (reproduced locally against ministack), @swamp/gcs-datastore (same failure in swamp-uat CI, not run locally)
  • CI evidence: swamp-uat PR #627, jobs 114922279338 (gcs) and 114922279341 (s3)
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 8 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/12/2026, 7:03:20 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
zack assigned zack10/12/2026, 5:43:46 PM

Sign in to post a ripple.