fix(pS-usage): close delete-ward residuals — remint on corrupt, named abort keys, truncation protect-all, abort->protect-all set

This commit is contained in:
2026-07-28 13:54:10 -04:00
parent a4aeb9dcc8
commit ec83f14738
8 changed files with 197 additions and 36 deletions
+31 -7
View File
@@ -145,11 +145,20 @@ UsagePublishPlan planUsagePublish(const std::optional<std::string>& existing,
}
const std::optional<UsageRecord> theirs = decodeUsageRecord(*existing);
if (!theirs) {
// Undecodable existing value under MY OWN key: a sibling sharing this key
// (copy) always writes decodable records, so this is corruption. Overwrite
// with mine the self-heal restores correct protection for my holds; the
// prune side independently ABORTS while an unreadable record is present
// (foldUsageRecords), so the corrupt window can never cause a delete.
// Undecodable existing value under MY key: corruption (a sibling sharing
// this key via copy always writes decodable records). REMINT rather than
// overwrite: writing mine over the corrupt key would clear the prune-side
// abort, but a same-key sibling B's holds would then be unprotected until
// B publishes again. Leaving the corrupt key in place keeps the prune-side
// abort firing (foldUsageRecords.abortPrune) so the window where B's holds
// might be unprotected can never resolve toward delete. Mine is published
// under the new key that remint produces.
// NOTE (>16 MB gap): readReasamplerExtState returning nullopt for a value
// larger than 16 MB is indistinguishable from "absent" at the publish site;
// that narrow case takes the fresh-write branch above rather than remint.
// Both outcomes are safe (fresh write is also correct for a truly absent key);
// the gap is documented in the header's fail-safe list.
plan.remint = true;
return plan;
}
@@ -235,9 +244,24 @@ UsageFoldResult foldUsageRecords(
for (const std::optional<UsageRecord>& rec : decoded) {
if (!rec) {
// A present-but-unreadable record: it may protect ANYTHING, so the prune
// must halt outright — heldPaths is irrelevant once abortPrune is set (the
// caller deletes nothing).
// must halt outright. Belt-and-braces: return the PROTECT-ALL set (all
// readable records' paths) so the fail-safe holds even under a future
// caller that forgets to check abortPrune before using heldPaths. The
// abort flag is still the authoritative signal; heldPaths is the
// maximum-protection fallback.
result.abortPrune = true;
// Collect EVERY path from EVERY readable record, bypassing the liveness
// filter entirely (on abort the protected set is unknowable, so every
// decoded hold must be included regardless of track-guid membership).
std::unordered_set<std::string> seen;
for (const std::optional<UsageRecord>& r : decoded) {
if (!r) continue;
for (const UsageHold& h : r->holds) {
if (h.relativePath.empty()) continue;
if (seen.insert(h.relativePath).second)
result.heldPaths.push_back(h.relativePath);
}
}
return result;
}
records.push_back(*rec);