Two fold variants of one tap name overwrite each other inside a run trace directory #352

Open
opened 2026-07-27 02:07:14 +02:00 by claude · 2 comments
Collaborator

Observation

Found during the #311 identity-keyed-trace cycle, by an independent review of that cycle's core diff, and verified against the tree at e759b89.

aura exec accepts --tap NAME=FOLD (crates/aura-cli/src/main.rs:822). A run's tap plan is deliberately not part of the run's identity: the identity digest covers the run manifest only, so two runs of one blueprint differing solely in their tap plan share one trace directory. That much is intended — the tap plan says what is observed, not what the run is, and re-observing a run should not re-address it.

But the trace store keys a tap's file by the tap name alone, and index.json records taps: Vec<String> — names, with no fold variant (crates/aura-registry/src/trace_store.rs:78-83). So:

aura exec strategy.json --tap fast_tap=mean
aura exec strategy.json --tap fast_tap=max

Both runs carry the same manifest, hence the same directory. Both write fast_tap.json. Both write an index.json listing fast_tap. The second silently overwrites the first, and nothing on disk records that the surviving series is a max rather than a mean.

Why #311 does not close this

#311 removed the collision between runs that differ — different params, a rebuilt node crate — by giving each its own identity-keyed directory. This is the complementary case: two runs that are identical as runs but differ in how they were observed. #311's identity rule is precisely what makes them share a directory, correctly; the collision then lands one level down, on the file name.

Fork

  • (a) The fold variant enters the file namefast_tap.mean.json. Keeps the identity rule intact and keeps both observations; costs a file-naming change and a widening of the index entry.
  • (b) The fold variant enters the index entry only — the file stays fast_tap.json, but index.json records which fold produced it, so a re-observed run is at least self-describing about what it holds. Cheaper; the first series is still lost.
  • (c) The tap plan enters the run's identity — two plans, two directories. Simple, but it re-runs and re-stores a run that did not change, and it contradicts the reasoning recorded on #311 for keeping the plan out of the identity.

Severity is low in practice today: it needs two runs identical in every manifest field and differing only in a fold variant on one tap name. It is filed because it is the last member of the collision class #311 set out to close, and because a silent overwrite is the specific failure mode that class is about.

## Observation Found during the #311 identity-keyed-trace cycle, by an independent review of that cycle's core diff, and verified against the tree at e759b89. `aura exec` accepts `--tap NAME=FOLD` (`crates/aura-cli/src/main.rs:822`). A run's tap plan is deliberately **not** part of the run's identity: the identity digest covers the run manifest only, so two runs of one blueprint differing solely in their tap plan share one trace directory. That much is intended — the tap plan says what is *observed*, not what the run *is*, and re-observing a run should not re-address it. But the trace store keys a tap's file by the tap **name** alone, and `index.json` records `taps: Vec<String>` — names, with no fold variant (`crates/aura-registry/src/trace_store.rs:78-83`). So: ``` aura exec strategy.json --tap fast_tap=mean aura exec strategy.json --tap fast_tap=max ``` Both runs carry the same manifest, hence the same directory. Both write `fast_tap.json`. Both write an `index.json` listing `fast_tap`. The second silently overwrites the first, and nothing on disk records that the surviving series is a max rather than a mean. ## Why #311 does not close this #311 removed the collision between runs that *differ* — different params, a rebuilt node crate — by giving each its own identity-keyed directory. This is the complementary case: two runs that are identical *as runs* but differ in how they were observed. #311's identity rule is precisely what makes them share a directory, correctly; the collision then lands one level down, on the file name. ## Fork - **(a) The fold variant enters the file name** — `fast_tap.mean.json`. Keeps the identity rule intact and keeps both observations; costs a file-naming change and a widening of the index entry. - **(b) The fold variant enters the index entry only** — the file stays `fast_tap.json`, but `index.json` records which fold produced it, so a re-observed run is at least self-describing about what it holds. Cheaper; the first series is still lost. - **(c) The tap plan enters the run's identity** — two plans, two directories. Simple, but it re-runs and re-stores a run that did not change, and it contradicts the reasoning recorded on #311 for keeping the plan out of the identity. Severity is low in practice today: it needs two runs identical in every manifest field and differing only in a fold variant on one tap name. It is filed because it is the last member of the collision class #311 set out to close, and because a silent overwrite is the specific failure mode that class is about.
Author
Collaborator

A second instance of this class, from the #311 cycle-close review (2026-07-27)

The cycle-close architect review found a sibling of the case this issue describes, with the same root and a wider reach.

TraceStore::begin_run is a bare create_dir_all that never clears (crates/aura-registry/src/trace_store.rs:112-116), and a run's tap plan is deliberately not identity-bearing. So two runs that are identical as runs but whose plans persist a different set of taps share one directory: the second writes an index.json listing only its own taps, and the earlier run's tap file is stranded beside it — present on disk, absent from the index.

This is the same shape as the fold-variant collision this issue already carries, one level out: there, two observations of one run collide on a file name; here, two observations of one run collide on the directory's contents, and the index stops describing what is in it.

Reachable without any exotic setup: a TapSubscription::Live on one tap, or the library/World embedding path, produces a plan that persists fewer taps than a default run (crates/aura-runner/src/tap_plan.rs:510-521).

Note what #311 did and did not close. Before it, this stranding happened between runs that genuinely differed — different params, a different topology — because they shared a name-keyed directory. That case is gone: differing runs now have differing directories. What remains is the narrow case above, where the runs really are the same run, observed differently. Whichever way this issue resolves the fold-variant fork should settle this one too, since both follow from the same two properties: the plan is outside the identity, and the write path never prunes.

The authoring guide was corrected in the #311 cycle so it no longer claims the directory's contents are wholly replaced.

## A second instance of this class, from the #311 cycle-close review (2026-07-27) The cycle-close architect review found a sibling of the case this issue describes, with the same root and a wider reach. `TraceStore::begin_run` is a bare `create_dir_all` that never clears (`crates/aura-registry/src/trace_store.rs:112-116`), and a run's tap **plan** is deliberately not identity-bearing. So two runs that are identical *as runs* but whose plans persist a different set of taps share one directory: the second writes an `index.json` listing only its own taps, and the earlier run's tap file is stranded beside it — present on disk, absent from the index. This is the same shape as the fold-variant collision this issue already carries, one level out: there, two observations of one run collide on a file *name*; here, two observations of one run collide on the directory's *contents*, and the index stops describing what is in it. Reachable without any exotic setup: a `TapSubscription::Live` on one tap, or the library/World embedding path, produces a plan that persists fewer taps than a default run (`crates/aura-runner/src/tap_plan.rs:510-521`). Note what #311 did and did not close. Before it, this stranding happened between runs that genuinely differed — different params, a different topology — because they shared a name-keyed directory. That case is gone: differing runs now have differing directories. What remains is the narrow case above, where the runs really are the same run, observed differently. Whichever way this issue resolves the fold-variant fork should settle this one too, since both follow from the same two properties: the plan is outside the identity, and the write path never prunes. The authoring guide was corrected in the #311 cycle so it no longer claims the directory's contents are wholly replaced.
Author
Collaborator

A consumer-visible face of this class, from the #311 field test (2026-07-27)

The field test hit the stranding case from the consumer side, and it is sharper there than a stale file sitting inert on disk.

Both taps recorded, then the same blueprint re-run with a narrower plan (same identity — the tap plan is not identity-bearing):

$ aura exec 2_ic_measure.bp.json --tap signal=record
aura: note: declared tap "price" unbound this run
{… "taps":["price","signal"], "trace_name":"graph-cb78c78d"}

$ ls runs/traces/graph-cb78c78d
index.json  price.json  signal.json

$ aura measure ic graph-cb78c78d --signal signal --price price
aura: run 'graph-cb78c78d' has no tap 'price' (taps: ["signal"])   [exit 1]

The identical command had succeeded minutes earlier against the same handle, and price.json is still in the directory. So the observable form is not clutter: it is a working command that stops working, with a diagnostic asserting something false about the run — it did declare that tap, an earlier run under this identity did record it, and the data is on disk.

One detail that makes it harder to self-diagnose: the record line prints "taps":["price","signal"] for both runs, so only the stderr note distinguishes the narrower one.

Whichever way the pruning fork here resolves, the refusal wants rewording — "not persisted by the most recent run under this identity" rather than "has no tap".

## A consumer-visible face of this class, from the #311 field test (2026-07-27) The field test hit the stranding case from the consumer side, and it is sharper there than a stale file sitting inert on disk. Both taps recorded, then the same blueprint re-run with a narrower plan (same identity — the tap plan is not identity-bearing): ``` $ aura exec 2_ic_measure.bp.json --tap signal=record aura: note: declared tap "price" unbound this run {… "taps":["price","signal"], "trace_name":"graph-cb78c78d"} $ ls runs/traces/graph-cb78c78d index.json price.json signal.json $ aura measure ic graph-cb78c78d --signal signal --price price aura: run 'graph-cb78c78d' has no tap 'price' (taps: ["signal"]) [exit 1] ``` The identical command had succeeded minutes earlier against the same handle, and `price.json` is still in the directory. So the observable form is not clutter: it is a working command that stops working, with a diagnostic asserting something false about the run — it *did* declare that tap, an earlier run under this identity *did* record it, and the data is on disk. One detail that makes it harder to self-diagnose: the record line prints `"taps":["price","signal"]` for **both** runs, so only the stderr note distinguishes the narrower one. Whichever way the pruning fork here resolves, the refusal wants rewording — "not persisted by the most recent run under this identity" rather than "has no tap".
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#352