Skip to main content

lean_ctx/core/
cache_telemetry.rs

1//! Read-cache reliability telemetry.
2//!
3//! The subjective "re-reads feel unreliable" signal is turned into data here:
4//! every event that wipes or invalidates a *fully-delivered* entry — and thus
5//! forces the next read to re-send the whole file instead of the cheap
6//! `[unchanged]` stub — increments a process-global counter grouped by cause.
7//!
8//! Counters are monotonic `AtomicU64`s. They are surfaced ONLY in the
9//! `ctx_cache status` diagnostic, never inside a cacheable tool-output body, so
10//! output determinism (#498) is preserved.
11
12use std::sync::atomic::{AtomicU64, Ordering};
13
14static COMPACTION: AtomicU64 = AtomicU64::new(0);
15static IDLE: AtomicU64 = AtomicU64::new(0);
16static EVICTION: AtomicU64 = AtomicU64::new(0);
17static CONVERSATION: AtomicU64 = AtomicU64::new(0);
18
19/// Fully-delivered entries whose delivery flag was reset by a host compaction.
20pub fn record_compaction(n: u64) {
21    if n > 0 {
22        COMPACTION.fetch_add(n, Ordering::Relaxed);
23    }
24}
25
26/// Fully-delivered entries dropped by an idle-TTL cache clear.
27pub fn record_idle(n: u64) {
28    if n > 0 {
29        IDLE.fetch_add(n, Ordering::Relaxed);
30    }
31}
32
33/// Fully-delivered entries evicted under RAM / token-budget pressure.
34pub fn record_eviction(n: u64) {
35    if n > 0 {
36        EVICTION.fetch_add(n, Ordering::Relaxed);
37    }
38}
39
40/// A re-read that fell back to full content because the reading conversation
41/// differed from the one the entry was delivered to (conversation scoping, #954).
42pub fn record_conversation_mismatch() {
43    CONVERSATION.fetch_add(1, Ordering::Relaxed);
44}
45
46/// Immutable snapshot of the re-delivery counters.
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
48pub struct Snapshot {
49    pub compaction: u64,
50    pub idle: u64,
51    pub eviction: u64,
52    pub conversation: u64,
53}
54
55impl Snapshot {
56    /// Total forced re-deliveries across all causes.
57    pub fn total(&self) -> u64 {
58        self.compaction
59            .saturating_add(self.idle)
60            .saturating_add(self.eviction)
61            .saturating_add(self.conversation)
62    }
63}
64
65/// Reads the current counters into a consistent snapshot.
66pub fn snapshot() -> Snapshot {
67    Snapshot {
68        compaction: COMPACTION.load(Ordering::Relaxed),
69        idle: IDLE.load(Ordering::Relaxed),
70        eviction: EVICTION.load(Ordering::Relaxed),
71        conversation: CONVERSATION.load(Ordering::Relaxed),
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn total_sums_every_cause() {
81        let s = Snapshot {
82            compaction: 1,
83            idle: 2,
84            eviction: 4,
85            conversation: 8,
86        };
87        assert_eq!(s.total(), 15);
88    }
89
90    #[test]
91    fn zero_is_a_noop() {
92        // A zero-sized event must never move a counter (avoids logging "0 wiped").
93        let before = snapshot();
94        record_compaction(0);
95        record_idle(0);
96        record_eviction(0);
97        // Only `>=` is safe to assert on a process-global counter: other tests
98        // may increment concurrently. A 0-sized call adds nothing of its own.
99        let after = snapshot();
100        assert!(after.compaction >= before.compaction);
101        assert!(after.idle >= before.idle);
102        assert!(after.eviction >= before.eviction);
103    }
104
105    #[test]
106    fn each_cause_increments_monotonically() {
107        let before = snapshot();
108        record_compaction(2);
109        record_idle(3);
110        record_eviction(5);
111        record_conversation_mismatch();
112        let after = snapshot();
113        // Monotonic deltas (`>=`) tolerate concurrent increments from sibling
114        // tests while still proving each recorder targets the right counter.
115        assert!(after.compaction >= before.compaction + 2);
116        assert!(after.idle >= before.idle + 3);
117        assert!(after.eviction >= before.eviction + 5);
118        assert!(after.conversation > before.conversation);
119        assert!(after.total() >= before.total() + 11);
120    }
121}