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