Relationships
#1626 migrate-index still drops shallow-path entries on 2026.08.07.1 — #1584 marked shipped but no released version carries the fix (self-contained repro)
Opened by sntxrr · 8/12/2026· Shipped 8/12/2026
Re-filing swamp-club#1584, which is marked shipped but is still present in the only published artifact. Not a complaint about the triage — I suspect the fix is merged and the label ran ahead of the release. This issue exists to carry a self-contained repro so the fix can be confirmed against a version number.
The status does not match any released artifact
| evidence | value |
|---|---|
| #1584 filed | 2026-08-10 |
@swamp/s3-datastore latest in registry |
2026.08.07.1 |
| that version published | 2026-08-07, three days before the report |
| re-pull of that version today | byte-identical source |
| comments on #1584 | none naming a fix version |
For contrast, #1554 was closed with a comment naming @swamp/s3-datastore@2026.07.31.1, which I could pull and verify. There is no such version here.
Repro — no S3, no bucket, no credentials, no network
partitionKeyFromPath is a public static on the exported S3CacheSyncService, so the shipped code can be called directly. Save as repro.ts:
const modPath = Deno.args[0];
const { S3CacheSyncService } = await import(`file://${await Deno.realPath(modPath)}`);
const cases: Array<[string, string]> = [
["data/@vendor/model/<uuid>/<dataName>/<n>/raw", "data/@v/m/1111/current/3/raw"],
["outputs/@vendor/model/<method>/<uuid>-<ts>.yaml", "outputs/@v/m/sync/1111-2026-01-01T00-00-00-000Z.yaml"],
["definitions-evaluated/@vendor/model/<uuid>.yaml", "definitions-evaluated/@v/m/1111.yaml"],
["auto-definitions/@vendor/model/<uuid>.yaml", "auto-definitions/@v/m/1111.yaml"],
["workflow-runs/<workflowId>/<file>.yaml", "workflow-runs/wf-1/workflow-run-abc.yaml"],
["workflows-evaluated/<file>.yaml", "workflows-evaluated/workflow-wf-1.yaml"],
["<root file>", ".catalog-export.json"],
];
let failures = 0;
for (const [shape, sample] of cases) {
const key = S3CacheSyncService.partitionKeyFromPath(sample);
if (key === undefined) failures++;
console.log(` ${key === undefined ? "DROPPED" : "ok "} ${shape} -> ${key ?? "undefined"}`);
}
console.log(`${failures} of ${cases.length} shapes dropped`);
Deno.exit(failures === 0 ? 0 : 1);Run it against the installed extension (the --allow-env is only because importing the module pulls in the AWS SDK):
deno run --allow-read --allow-env repro.ts \
<repo>/.swamp/pulled-extensions/@swamp/s3-datastore/datastores/_lib/s3_cache_sync.tsActual output on @swamp/s3-datastore@2026.08.07.1
ok data/@vendor/model/<uuid>/<dataName>/<n>/raw -> data--@v--m--1111
ok outputs/@vendor/model/<method>/<uuid>-<ts>.yaml -> outputs--@v--m--sync
ok definitions-evaluated/@vendor/model/<uuid>.yaml -> definitions-evaluated--@v--m
ok auto-definitions/@vendor/model/<uuid>.yaml -> auto-definitions
ok workflow-runs/<workflowId>/<file>.yaml -> workflow-runs--wf-1
DROPPED workflows-evaluated/<file>.yaml -> undefined
DROPPED <root file> -> undefined
2 of 7 shapes droppedExit code 1. Expected: 0 of 7 dropped.
Any entry returning undefined is discarded by groupEntriesByPartition via if (!key) continue; with no log line, so it never reaches a shard.
Root cause (unchanged from #1584)
static partitionKeyFromPath(rel: string): string | undefined {
const segments = rel.split("/");
if (segments.length < 2) return undefined; // (a) drops every root-level file
const subdir = segments[0];
switch (subdir) {
...
case "workflow-runs":
case "workflows-evaluated": {
if (segments.length < 3) return undefined; // (b) wrong for workflows-evaluated
return `${subdir}--${segments[1]}`;
}The two share a case arm but are different depths by design: workflow-runs/<workflowId>/<file> is 3 segments, workflows-evaluated/<file>.yaml is 2. Guard (b) therefore rejects 100% of workflows-evaluated entries. Guard (a) rejects any root-level file, of which .catalog-export.json is the notable one.
Production evidence that it has not self-healed
Same datastore as #1584, now 478 commits later (_meta.json v2, commitSeq=478, 104 partitions, 23,019 sharded entries):
STILL MISSING .catalog-export.json (object present in S3, 8.6 MB)
STILL MISSING workflows-evaluated/workflow-063a0cb1-....yaml
STILL MISSING workflows-evaluated/workflow-e14e03ef-....yamlNothing re-derives them, so the gap is permanent until the partitioner changes.
Severity: low, fails safe
Repeating this so it is not over-prioritised: the S3 objects still exist — this is index-only. toDelete is computed from index entries, so a shorter index yields fewer deletion candidates, never more; this cannot cause data loss. Practical cost is that affected files look absent and get re-pushed.
The reason to fix it anyway is the silent continue. That is the same shape as the silently-skipped push in swamp-club#1557, which went unnoticed here for seven days. And .catalog-export.json is the catalog itself.
Suggested fix
- Split the case arm:
workflows-evaluatedrequires>= 2and partitions onsegments[0];workflow-runskeeps>= 3. - Give root-level files a partition (e.g.
_root) instead of dropping at (a). - Replace the silent
continuewith a counter and a log line — or fail the migration outright. A migration that drops entries without saying so is worse than one that refuses to run. - Assert
sum(shard entries) === entryCountbefore writing_meta.json. That single check would have caught this at authoring time and would prevent the whole class.
Point 4 is the one I would most like to see, independent of the specific paths.
Ask
Which released version carries the #1584 fix? If it is merged but unreleased, this issue can just track the release. The repro above exits non-zero on the current artifact and will exit zero once it lands, so it doubles as the acceptance check.
Environment
- CLI
20260809.004828.0-sha.b61c9de2 @swamp/s3-datastore@2026.08.07.1(latest available)- Deno
2.8.3, darwin/aarch64
Upstream repository: https://github.com/systeminit/swamp-extensions
Environment
- Extension:
@swamp/s3-datastore@2026.08.07.1 - swamp:
20260812.013400.0-sha.278864d1 - OS:
darwin(aarch64) - Deno:
2.8.3 - Shell:
/bin/zsh
Shipped
Click a lifecycle step above to view its details.
stack72 commented 8/12/2026, 6:44:06 PM
The fix from PR #187 is now published. Pull @swamp/s3-datastore@2026.08.12.1 and @swamp/gcs-datastore@2026.08.12.1 — the reporter's repro script should exit 0 against these versions.
sntxrr commented 8/14/2026, 5:13:39 PM
Confirmed fixed in @swamp/s3-datastore@2026.08.12.1. Thank you — that was a fast turnaround, and all four suggestions landed.
The repro from the issue body now exits 0 against the shipped artifact:
ok data/@vendor/model/<uuid>/<dataName>/<n>/raw -> data--@v--m--1111
ok outputs/@vendor/model/<method>/<uuid>-<ts>.yaml -> outputs--@v--m--sync
ok definitions-evaluated/@vendor/model/<uuid>.yaml -> definitions-evaluated--@v--m
ok auto-definitions/@vendor/model/<uuid>.yaml -> auto-definitions
ok workflow-runs/<workflowId>/<file>.yaml -> workflow-runs--wf-1
ok workflows-evaluated/<file>.yaml -> workflows-evaluated
ok <root file> -> _root
0 of 7 shapes dropped(was 2 of 7 on 2026.08.07.1)
partitionKeyFromPath splits the case arm and gives root files _root; groupEntriesByPartition collects dropped entries and warns naming up to five. The abort guard is stronger than what I suggested — I asked for an assertion, and you made it refuse to write an incomplete _meta.json:
[s3-sync] Migration aborted: N index entries but only M could be partitioned...That is the right call. It turns this whole class from a silent gap into a loud failure.
One thing worth documenting: the fix is not retroactive
This is the part that could confuse someone upgrading with an index already migrated by the buggy version, so it may be worth a release note.
Upgrading alone does not backfill entries that were dropped by an earlier migration, and a normal push will not do it either:
$ swamp datastore sync --push
Pushing all local data to remote...
Pushed 0 filesThe three entries stayed missing. The reason is that a scoped walk only considers paths in dirtyPaths; entries absent from the index are never candidates, so nothing re-adds them. Re-running migrate-index also does not help on a v2 index, because pullIndex({forceRemote:true}) assembles from the shards — which are exactly the thing missing the entries.
What did fix it was forcing a full walk (localDirty: true, bulkInvalidated: true, dirtyPaths: [] in the sync sidecar) and pushing:
Pushed 366 files
partitions: 106 -> 118 (_root and workflows-evaluated created)
index entries: 26556 -> 26905
commitSeq: 710 -> 711
.catalog-export.json PRESENT
workflows-evaluated/workflow-063a0cb1-....yaml PRESENT
workflows-evaluated/workflow-e14e03ef-....yaml PRESENT
S3 objects lost: 0doctor datastores passes on both writers afterwards, including the new catalog-coverage check.
If you would rather users not hand-edit the sidecar, a --rebuild/--force flag on migrate-index that sources from the monolithic index (still retained) rather than the shards would be a cleaner recovery path. Low priority — the gap is index-only and fails safe, since toDelete is index-derived so a short index can only reduce deletion candidates.
Unrelated gotcha hit while validating
swamp extension pull @swamp/s3-datastore upgraded the on-disk extension to 2026.08.12.1 but left extensions/models/upstream_extensions.json pinned at 2026.08.07.1. My containerised swamp serve installs from that lockfile, so it would have silently kept running the buggy version, and an extension install would have downgraded the host back.
swamp extension update moves both, and swamp extension outdated reads the lockfile — so those are the reliable pair. If pull leaving the lockfile stale is intended, it might deserve a note in its help text; if not, happy to file it separately.
Verified on
- CLI
20260813.161427.0-sha.03547878 @swamp/s3-datastore@2026.08.12.1- Namespaced S3 datastore, 26,905 index entries, 118 partitions, two concurrent writers
Sign in to post a ripple.