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:
co-authored by
Claude Fable 5.1
parent
99b907be04
commit
0744d52639
@@ -161,6 +161,9 @@ pub struct WriteEvent {
|
||||
// WriteAnomalyDetector
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Upper bound on distinct session ids the detector tracks at once.
|
||||
const MAX_TRACKED_SESSIONS: usize = 4096;
|
||||
|
||||
/// Tracks write events and raises alerts for suspicious behaviour.
|
||||
#[derive(Debug)]
|
||||
pub struct WriteAnomalyDetector {
|
||||
@@ -189,6 +192,23 @@ impl WriteAnomalyDetector {
|
||||
if event.timestamp > self.last_timestamp {
|
||||
self.last_timestamp = event.timestamp;
|
||||
}
|
||||
// Bound the per-session map: a long-lived process sees an unbounded
|
||||
// number of distinct session ids. When it overflows, forget the
|
||||
// sessions with the fewest writes (they are furthest from the limit
|
||||
// this map exists to enforce); the current one is re-added below.
|
||||
if self.session_counts.len() >= MAX_TRACKED_SESSIONS
|
||||
&& !self.session_counts.contains_key(&event.session_id)
|
||||
{
|
||||
let mut counts: Vec<u32> = self.session_counts.values().copied().collect();
|
||||
let keep_from = counts.len() / 2;
|
||||
counts.select_nth_unstable(keep_from);
|
||||
let threshold = counts[keep_from];
|
||||
self.session_counts.retain(|_, c| *c >= threshold);
|
||||
if self.session_counts.len() >= MAX_TRACKED_SESSIONS {
|
||||
// Every session had the same count: drop them all.
|
||||
self.session_counts.clear();
|
||||
}
|
||||
}
|
||||
*self
|
||||
.session_counts
|
||||
.entry(event.session_id.clone())
|
||||
|
||||
Reference in New Issue
Block a user