1pub fn save_tee(command: &str, output: &str) -> Option<String> {
10 if crate::core::policy::runtime::is_active() {
13 return None;
14 }
15 let command = crate::core::policy::content::protect_active(command).ok()?;
17 let output = crate::core::policy::content::protect_active(output).ok()?;
18 let command = command.as_ref();
19 let output = output.as_ref();
20 let tee_dir = crate::core::paths::state_dir().ok()?.join("tee");
21 std::fs::create_dir_all(&tee_dir).ok()?;
22
23 if let Ok(now) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
30 && TEE_CLEANUP_THROTTLE.is_due(now.as_secs())
31 {
32 cleanup_old_tee_logs(&tee_dir);
33 }
34
35 let cmd_slug: String = command
36 .chars()
37 .take(40)
38 .map(|c| {
39 if c.is_alphanumeric() || c == '-' {
40 c
41 } else {
42 '_'
43 }
44 })
45 .collect();
46 let cmd_hash = blake3::hash(command.as_bytes()).to_hex();
51 let filename = format!("{cmd_slug}_{}.log", &cmd_hash.as_str()[..8]);
52 let path = tee_dir.join(&filename);
53
54 crate::core::ccr::write_tee(&path, output)?;
55 let handle = path.to_string_lossy().to_string();
56 crate::core::relevance_tracker::register_compressed(
57 handle.clone(),
58 output,
59 "ctx_shell",
60 crate::core::tokens::count_tokens(output),
61 0,
62 );
63 Some(handle)
64}
65
66struct CleanupThrottle {
70 last_run_unix_secs: std::sync::atomic::AtomicU64,
71 interval_secs: u64,
72}
73
74impl CleanupThrottle {
75 const fn new(interval_secs: u64) -> Self {
76 Self {
77 last_run_unix_secs: std::sync::atomic::AtomicU64::new(0),
78 interval_secs,
79 }
80 }
81
82 fn is_due(&self, now_unix_secs: u64) -> bool {
85 use std::sync::atomic::Ordering;
86 let last = self.last_run_unix_secs.load(Ordering::Relaxed);
87 let due = last == 0 || now_unix_secs.saturating_sub(last) >= self.interval_secs;
88 due && self
89 .last_run_unix_secs
90 .compare_exchange(last, now_unix_secs, Ordering::Relaxed, Ordering::Relaxed)
91 .is_ok()
92 }
93}
94
95static TEE_CLEANUP_THROTTLE: CleanupThrottle = CleanupThrottle::new(10 * 60);
96
97pub(crate) fn cleanup_old_tee_logs(tee_dir: &std::path::Path) {
101 let cutoff = std::time::SystemTime::now().checked_sub(std::time::Duration::from_hours(24));
102 let Some(cutoff) = cutoff else { return };
103
104 if let Ok(entries) = std::fs::read_dir(tee_dir) {
105 for entry in entries.flatten() {
106 if let Ok(meta) = entry.metadata()
107 && let Ok(modified) = meta.modified()
108 && modified < cutoff
109 {
110 let _ = std::fs::remove_file(entry.path());
111 }
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
123 fn cleanup_throttle_gates_by_interval() {
124 let throttle = CleanupThrottle::new(600);
125 assert!(
126 throttle.is_due(1_000),
127 "never run before: first call is always due"
128 );
129 assert!(
130 !throttle.is_due(1_100),
131 "only 100s elapsed of a 600s interval"
132 );
133 assert!(!throttle.is_due(1_599), "still short of the interval by 1s");
134 assert!(
135 throttle.is_due(1_600),
136 "exactly interval-elapsed must be due again"
137 );
138 }
139
140 #[test]
141 fn cleanup_throttle_lets_only_one_racer_through_per_interval() {
142 let throttle = CleanupThrottle::new(600);
143 let hits = std::thread::scope(|scope| {
144 let handles: Vec<_> = (0..16)
145 .map(|_| scope.spawn(|| throttle.is_due(1_000)))
146 .collect();
147 handles
148 .into_iter()
149 .map(|h| h.join().unwrap())
150 .filter(|&due| due)
151 .count()
152 });
153 assert_eq!(hits, 1, "exactly one racer should win the cleanup slot");
154 }
155
156 #[test]
160 fn tee_path_is_content_addressed() {
161 let _lock = crate::core::data_dir::test_env_lock();
164 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
165 let first = save_tee("cargo test --lib", "output run 1").expect("tee saved");
166 let second = save_tee("cargo test --lib", "output run 2").expect("tee saved");
167 assert_eq!(first, second, "same command must map to the same tee path");
168
169 let other = save_tee("cargo build", "output").expect("tee saved");
170 assert_ne!(first, other, "different commands get different tee paths");
171
172 let content = std::fs::read_to_string(&second).unwrap();
174 assert!(content.contains("run 2"));
175
176 for p in [first, other] {
177 let _ = std::fs::remove_file(p);
178 }
179 }
180}