fix(agent): provenance survives compaction; bound alert/session growth; snapshot the WAL

- ProvenanceStore::remap: compaction renumbers cache indices (which are the
  provenance record ids) but nothing renumbered the ledger, so after any
  compaction — including the automatic one in delete() — every surviving
  record's hash was filed under a different record and the next
  save_or_update raised a bogus High "integrity mismatch" alert.
- Pending anomaly alerts are capped (newest 1024 kept). Alerts never block a
  save, and a session over its write limit alerts on every write, so a caller
  that didn't drain them grew the queue without bound.
- WriteAnomalyDetector tracks at most 4096 sessions, forgetting the
  least-active half on overflow instead of leaking one entry per session id
  for the life of the process.
- snapshot() copies the pending WAL next to the .h5 copy, so a snapshot is
  the store as it is now rather than as of the last checkpoint (it used to
  silently omit up to wal_max_entries recent saves).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
osobh
2026-09-19 06:08:53 -07:00
co-authored by Claude Fable 5.1
parent 99b907be04
commit 0744d52639
3 changed files with 128 additions and 4 deletions
+17
View File
@@ -105,6 +105,23 @@ impl ProvenanceStore {
self.records.insert(provenance.record_id, provenance);
}
/// Renumber records after the store was compacted. `index_map[old]` is
/// the record's new id, or `None` if it was removed. Without this, every
/// surviving record's hash ends up filed under some other record's id and
/// the next integrity check reports a bogus mismatch.
pub fn remap(&mut self, index_map: &[Option<usize>]) {
let old = std::mem::take(&mut self.records);
for (old_id, mut prov) in old {
let new_id = usize::try_from(old_id)
.ok()
.and_then(|i| index_map.get(i).copied().flatten());
if let Some(new_id) = new_id {
prov.record_id = new_id as u64;
self.records.insert(new_id as u64, prov);
}
}
}
/// Retrieve by record ID.
pub fn get(&self, record_id: u64) -> Option<&MemoryProvenance> {
self.records.get(&record_id)