Two fold variants of one tap name overwrite each other inside a run trace directory #352
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 execaccepts--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.jsonrecordstaps: Vec<String>— names, with no fold variant (crates/aura-registry/src/trace_store.rs:78-83). So:Both runs carry the same manifest, hence the same directory. Both write
fast_tap.json. Both write anindex.jsonlistingfast_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
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.fast_tap.json, butindex.jsonrecords 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.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.
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_runis a barecreate_dir_allthat 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 anindex.jsonlisting 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::Liveon 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 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):
The identical command had succeeded minutes earlier against the same handle, and
price.jsonis 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".