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:
- Repo A: namespace
infra. Create modelunique-modeland write JSON resourcesitem-000000,item-000001.swamp datastore sync --push. - Repo B: namespace
security. Create modelunique-modeland writeitem-000000toitem-000003with a different payload.swamp datastore sync --push. - Repo A:
swamp datastore catalog pull --namespaces security. - Repo A: a
command/shellmodel whoserunisecho 'content=${{ data.latest("security:unique-model", "item-000001").attributes.content }}', thenswamp 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.contentvalidation_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):
- The fetcher is not wired for run commands.
setForeignContentFetcheris called inrequireInitializedRepoReadOnly(src/cli/repo_context.ts:1038) andrequireInitializedRepo(:1323) but not inrequireInitializedRepoUnlocked(:1355), whichmodel method run,workflow runand single-modelmodel evaluateuse. So evendata.versionand wildcarddata.latest, which go through the hydrating asyncquery(), get empty attributes. Confirmed by running: those readers fail inmodel method runwhile the CLI query succeeds. getLatestRecordnever uses the fetcher. A non-wildcardns:prefixeddata.latestgoesmodel_resolver.ts:1370->DataQueryService.getLatestRecord->buildRecordFromRow(src/domain/data/data_query_service.ts:591).ensureContentOfreturns early when the row's namespace is not the repository's, andfromRowreads local disk only. The foreign hydration loop exists only at the end ofquery()(: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
setForeignContentFetcherfromsyncServiceinrequireInitializedRepoUnlocked, 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 inquery()could become a per-record helper shared by both, and run before projection so--select attributesis covered. integration/giga_swamp_cel_namespace_test.ts("Once security holds the record") asserts onlynamespaceandmodelId. Add anattributesassertion.design/enablers/data-query.mdsays CELdata.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)
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.