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);
18static RAW_CAP_COUNT: AtomicU64 = AtomicU64::new(0);
19static RAW_CAP_PREVENTED: AtomicU64 = AtomicU64::new(0);
20
21/// Fully-delivered entries whose delivery flag was reset by a host compaction.
22pub fn record_compaction(n: u64) {
23    if n > 0 {
24        COMPACTION.fetch_add(n, Ordering::Relaxed);
25    }
26}
27
28/// Fully-delivered entries dropped by an idle-TTL cache clear.
29pub fn record_idle(n: u64) {
30    if n > 0 {
31        IDLE.fetch_add(n, Ordering::Relaxed);
32    }
33}
34
35/// Fully-delivered entries evicted under RAM / token-budget pressure.
36pub fn record_eviction(n: u64) {
37    if n > 0 {
38        EVICTION.fetch_add(n, Ordering::Relaxed);
39    }
40}
41
42/// A re-read that fell back to full content because the reading conversation
43/// differed from the one the entry was delivered to (conversation scoping, #954).
44pub fn record_conversation_mismatch() {
45    CONVERSATION.fetch_add(1, Ordering::Relaxed);
46}
47
48/// Framed output was larger than raw content — `cap_to_raw()` fell back to
49/// verbatim to prevent negative savings. Tracked so the dashboard can
50/// distinguish "no savings" from "savings prevented inflation".
51pub fn record_raw_cap(prevented_inflation_tokens: u64) {
52    RAW_CAP_COUNT.fetch_add(1, Ordering::Relaxed);
53    RAW_CAP_PREVENTED.fetch_add(prevented_inflation_tokens, Ordering::Relaxed);
54}
55
56/// Immutable snapshot of the re-delivery counters.
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
58pub struct Snapshot {
59    pub compaction: u64,
60    pub idle: u64,
61    pub eviction: u64,
62    pub conversation: u64,
63    pub raw_cap_count: u64,
64    pub raw_cap_prevented: u64,
65}
66
67impl Snapshot {
68    /// Total forced re-deliveries across all causes.
69    pub fn total(&self) -> u64 {
70        self.compaction
71            .saturating_add(self.idle)
72            .saturating_add(self.eviction)
73            .saturating_add(self.conversation)
74    }
75}
76
77/// Reads the current counters into a consistent snapshot.
78pub fn snapshot() -> Snapshot {
79    Snapshot {
80        compaction: COMPACTION.load(Ordering::Relaxed),
81        idle: IDLE.load(Ordering::Relaxed),
82        eviction: EVICTION.load(Ordering::Relaxed),
83        conversation: CONVERSATION.load(Ordering::Relaxed),
84        raw_cap_count: RAW_CAP_COUNT.load(Ordering::Relaxed),
85        raw_cap_prevented: RAW_CAP_PREVENTED.load(Ordering::Relaxed),
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn total_sums_every_cause() {
95        let s = Snapshot {
96            compaction: 1,
97            idle: 2,
98            eviction: 4,
99            conversation: 8,
100            raw_cap_count: 0,
101            raw_cap_prevented: 0,
102        };
103        assert_eq!(s.total(), 15);
104    }
105
106    #[test]
107    fn zero_is_a_noop() {
108        // A zero-sized event must never move a counter (avoids logging "0 wiped").
109        let before = snapshot();
110        record_compaction(0);
111        record_idle(0);
112        record_eviction(0);
113        // Only `>=` is safe to assert on a process-global counter: other tests
114        // may increment concurrently. A 0-sized call adds nothing of its own.
115        let after = snapshot();
116        assert!(after.compaction >= before.compaction);
117        assert!(after.idle >= before.idle);
118        assert!(after.eviction >= before.eviction);
119    }
120
121    #[test]
122    fn each_cause_increments_monotonically() {
123        let before = snapshot();
124        record_compaction(2);
125        record_idle(3);
126        record_eviction(5);
127        record_conversation_mismatch();
128        let after = snapshot();
129        // Monotonic deltas (`>=`) tolerate concurrent increments from sibling
130        // tests while still proving each recorder targets the right counter.
131        assert!(after.compaction >= before.compaction + 2);
132        assert!(after.idle >= before.idle + 3);
133        assert!(after.eviction >= before.eviction + 5);
134        assert!(after.conversation > before.conversation);
135        assert!(after.total() >= before.total() + 11);
136    }
137}