lean_ctx/core/anti_interrupt/
tracker.rs1use std::sync::Mutex;
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum InterruptionEvent {
14 EchoRepetition {
16 tokens: u64,
18 },
19 RedundantRead {
21 path: String,
23 },
24 ContextSwitch {
26 from: String,
28 to: String,
30 },
31 BounceWaste {
33 tokens: u64,
35 },
36 StaleContext {
38 fact_key: String,
40 },
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45struct TimestampedEvent {
46 event: InterruptionEvent,
47 timestamp: DateTime<Utc>,
48 prevented: bool,
49}
50
51static SESSION_EVENTS: Mutex<Vec<TimestampedEvent>> = Mutex::new(Vec::new());
52
53const MAX_SESSION_EVENTS: usize = 10_000;
54
55fn session_events() -> std::sync::MutexGuard<'static, Vec<TimestampedEvent>> {
56 SESSION_EVENTS
57 .lock()
58 .unwrap_or_else(std::sync::PoisonError::into_inner)
59}
60
61pub(crate) fn record_interruption(event: InterruptionEvent, prevented: bool) {
63 let mut events = session_events();
64 if events.len() >= MAX_SESSION_EVENTS {
65 let remove_count = MAX_SESSION_EVENTS / 10;
66 events.drain(0..remove_count);
67 }
68 events.push(TimestampedEvent {
69 event,
70 timestamp: Utc::now(),
71 prevented,
72 });
73}
74
75pub(crate) fn session_interruptions() -> Vec<(InterruptionEvent, bool)> {
77 session_events()
78 .iter()
79 .map(|event| (event.event.clone(), event.prevented))
80 .collect()
81}
82
83pub(crate) fn reset_session() {
85 session_events().clear();
86}
87
88pub(crate) fn prevented_counts() -> PreventedCounts {
90 let events = session_events();
91 let mut counts = PreventedCounts::default();
92 for event in events.iter().filter(|event| event.prevented) {
93 match &event.event {
94 InterruptionEvent::EchoRepetition { tokens } => {
95 counts.echo_prevented = counts.echo_prevented.saturating_add(*tokens);
96 }
97 InterruptionEvent::RedundantRead { .. } => counts.redundant_reads_prevented += 1,
98 InterruptionEvent::ContextSwitch { .. } => counts.context_switches_prevented += 1,
99 InterruptionEvent::BounceWaste { tokens } => {
100 counts.bounce_waste_prevented =
101 counts.bounce_waste_prevented.saturating_add(*tokens);
102 }
103 InterruptionEvent::StaleContext { .. } => counts.stale_context_prevented += 1,
104 }
105 }
106 counts
107}
108
109#[derive(Debug, Default)]
111pub(crate) struct PreventedCounts {
112 pub echo_prevented: u64,
114 pub redundant_reads_prevented: u64,
116 pub context_switches_prevented: u64,
118 pub bounce_waste_prevented: u64,
120 pub stale_context_prevented: u64,
122}
123
124#[cfg(test)]
125pub(crate) static TEST_LOCK: Mutex<()> = Mutex::new(());
127
128#[cfg(test)]
129mod tests {
130 use std::thread;
131
132 use super::{
133 InterruptionEvent, TEST_LOCK, prevented_counts, record_interruption, reset_session,
134 session_interruptions,
135 };
136
137 #[test]
138 fn record_and_retrieve_events() {
139 let _guard = TEST_LOCK.lock().expect("test lock should be available");
140 reset_session();
141 record_interruption(
142 InterruptionEvent::RedundantRead {
143 path: "src/lib.rs".to_string(),
144 },
145 true,
146 );
147
148 let events = session_interruptions();
149 assert_eq!(events.len(), 1);
150 assert!(events[0].1);
151 assert!(matches!(
152 events[0].0,
153 InterruptionEvent::RedundantRead { .. }
154 ));
155 }
156
157 #[test]
158 fn reset_clears_events() {
159 let _guard = TEST_LOCK.lock().expect("test lock should be available");
160 reset_session();
161 record_interruption(InterruptionEvent::EchoRepetition { tokens: 20 }, false);
162 reset_session();
163
164 assert!(session_interruptions().is_empty());
165 }
166
167 #[test]
168 fn prevented_counts_are_correct() {
169 let _guard = TEST_LOCK.lock().expect("test lock should be available");
170 reset_session();
171 record_interruption(InterruptionEvent::EchoRepetition { tokens: 30 }, true);
172 record_interruption(InterruptionEvent::EchoRepetition { tokens: 12 }, false);
173 record_interruption(
174 InterruptionEvent::ContextSwitch {
175 from: "core".to_string(),
176 to: "cli".to_string(),
177 },
178 true,
179 );
180 record_interruption(InterruptionEvent::BounceWaste { tokens: 8 }, true);
181 record_interruption(
182 InterruptionEvent::StaleContext {
183 fact_key: "decision:format".to_string(),
184 },
185 true,
186 );
187
188 let counts = prevented_counts();
189 assert_eq!(counts.echo_prevented, 30);
190 assert_eq!(counts.redundant_reads_prevented, 0);
191 assert_eq!(counts.context_switches_prevented, 1);
192 assert_eq!(counts.bounce_waste_prevented, 8);
193 assert_eq!(counts.stale_context_prevented, 1);
194 }
195
196 #[test]
197 fn concurrent_recording_does_not_panic() {
198 let _guard = TEST_LOCK.lock().expect("test lock should be available");
199 reset_session();
200 let threads: Vec<_> = (0..10)
201 .map(|_| {
202 thread::spawn(|| {
203 for _ in 0..100 {
204 record_interruption(InterruptionEvent::EchoRepetition { tokens: 1 }, true);
205 }
206 })
207 })
208 .collect();
209
210 for handle in threads {
211 handle.join().expect("recording thread should not panic");
212 }
213 assert_eq!(session_interruptions().len(), 1_000);
214 }
215}