What happens
In get_prior_attempts, a reverted attempt's outcome flips to reopened when a later supersede event matches its closing removal. The match logic (selvedge/storage.py ~1487–1499) accepts either:
- an explicit link —
s["supersedes"] == closing["id"], or
- the timestamp fallback —
not s["supersedes"] and ts(s) >= ts(closing).
Branch 2 is evaluated per closing removal, so a single id-less supersede event matches every earlier removal on the path, not just the decision it actually re-opens. The comment directly above the fallback names this exact hazard for explicit links ("otherwise a supersede of one revert would flip every earlier, unrelated revert on the same path to 'reopened'") — but the id-less fallback re-introduces it.
Repro sketch
- Entity
users.auth_token: log add → remove (decision A, reverted).
- Same entity later: log
add → remove (decision B, independent, also reverted).
- Hand-log a supersede with no id link (
selvedge log users.auth_token supersede with empty --supersedes, or log_change over MCP without an id).
get_prior_attempts users.auth_token reports both A and B as reopened. Only B (the one the supersede follows) should flip; A's revert still stands.
Expected
An id-less supersede should re-open at most the single removal it auto-links to (the most recent one at write time — matching log_supersede's own auto-link rule), leaving earlier, unrelated reverts reverted.
Related observation (same code area)
log_supersede's id-less auto-link query matches the entity and its prefix descendants (entity_path LIKE 'canonical.%'), so selvedge supersede users can silently bind the most recent removal on a child such as users.email, writing a supersede on the parent path that points at a child's event. Worth deciding whether cross-path auto-linking is ever intended.
Found during the v0.3.10 release review; present in v0.3.10.
What happens
In
get_prior_attempts, a reverted attempt's outcome flips toreopenedwhen a later supersede event matches its closing removal. The match logic (selvedge/storage.py~1487–1499) accepts either:s["supersedes"] == closing["id"], ornot s["supersedes"] and ts(s) >= ts(closing).Branch 2 is evaluated per closing removal, so a single id-less supersede event matches every earlier removal on the path, not just the decision it actually re-opens. The comment directly above the fallback names this exact hazard for explicit links ("otherwise a supersede of one revert would flip every earlier, unrelated revert on the same path to 'reopened'") — but the id-less fallback re-introduces it.
Repro sketch
users.auth_token: logadd→remove(decision A, reverted).add→remove(decision B, independent, also reverted).selvedge log users.auth_token supersedewith empty--supersedes, orlog_changeover MCP without an id).get_prior_attempts users.auth_tokenreports both A and B asreopened. Only B (the one the supersede follows) should flip; A's revert still stands.Expected
An id-less supersede should re-open at most the single removal it auto-links to (the most recent one at write time — matching
log_supersede's own auto-link rule), leaving earlier, unrelated revertsreverted.Related observation (same code area)
log_supersede's id-less auto-link query matches the entity and its prefix descendants (entity_path LIKE 'canonical.%'), soselvedge supersede userscan silently bind the most recent removal on a child such asusers.email, writing a supersede on the parent path that points at a child's event. Worth deciding whether cross-path auto-linking is ever intended.Found during the v0.3.10 release review; present in v0.3.10.