pub fn save_tee(command: &str, output: &str) -> Option<String> {
let tee_dir = crate::core::paths::state_dir().ok()?.join("tee");
std::fs::create_dir_all(&tee_dir).ok()?;
if let Ok(now) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
&& TEE_CLEANUP_THROTTLE.is_due(now.as_secs())
{
cleanup_old_tee_logs(&tee_dir);
}
let cmd_slug: String = command
.chars()
.take(40)
.map(|c| {
if c.is_alphanumeric() || c == '-' {
c
} else {
'_'
}
})
.collect();
let cmd_hash = blake3::hash(command.as_bytes()).to_hex();
let filename = format!("{cmd_slug}_{}.log", &cmd_hash.as_str()[..8]);
let path = tee_dir.join(&filename);
let masked = crate::core::redaction::redact_text(output);
let (redacted, _) = crate::core::secret_detection::scan_and_redact_from_config(&masked);
std::fs::write(&path, redacted).ok()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
}
let handle = path.to_string_lossy().to_string();
crate::core::relevance_tracker::register_compressed(
handle.clone(),
output,
"ctx_shell",
crate::core::tokens::count_tokens(output),
0,
);
Some(handle)
}
struct CleanupThrottle {
last_run_unix_secs: std::sync::atomic::AtomicU64,
interval_secs: u64,
}
impl CleanupThrottle {
const fn new(interval_secs: u64) -> Self {
Self {
last_run_unix_secs: std::sync::atomic::AtomicU64::new(0),
interval_secs,
}
}
fn is_due(&self, now_unix_secs: u64) -> bool {
use std::sync::atomic::Ordering;
let last = self.last_run_unix_secs.load(Ordering::Relaxed);
let due = last == 0 || now_unix_secs.saturating_sub(last) >= self.interval_secs;
due && self
.last_run_unix_secs
.compare_exchange(last, now_unix_secs, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
}
}
static TEE_CLEANUP_THROTTLE: CleanupThrottle = CleanupThrottle::new(10 * 60);
pub(crate) fn cleanup_old_tee_logs(tee_dir: &std::path::Path) {
let cutoff = std::time::SystemTime::now().checked_sub(std::time::Duration::from_hours(24));
let Some(cutoff) = cutoff else { return };
if let Ok(entries) = std::fs::read_dir(tee_dir) {
for entry in entries.flatten() {
if let Ok(meta) = entry.metadata()
&& let Ok(modified) = meta.modified()
&& modified < cutoff
{
let _ = std::fs::remove_file(entry.path());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cleanup_throttle_gates_by_interval() {
let throttle = CleanupThrottle::new(600);
assert!(
throttle.is_due(1_000),
"never run before: first call is always due"
);
assert!(
!throttle.is_due(1_100),
"only 100s elapsed of a 600s interval"
);
assert!(!throttle.is_due(1_599), "still short of the interval by 1s");
assert!(
throttle.is_due(1_600),
"exactly interval-elapsed must be due again"
);
}
#[test]
fn cleanup_throttle_lets_only_one_racer_through_per_interval() {
let throttle = CleanupThrottle::new(600);
let hits = std::thread::scope(|scope| {
let handles: Vec<_> = (0..16)
.map(|_| scope.spawn(|| throttle.is_due(1_000)))
.collect();
handles
.into_iter()
.map(|h| h.join().unwrap())
.filter(|&due| due)
.count()
});
assert_eq!(hits, 1, "exactly one racer should win the cleanup slot");
}
#[test]
fn tee_path_is_content_addressed() {
let _lock = crate::core::data_dir::test_env_lock();
let first = save_tee("cargo test --lib", "output run 1").expect("tee saved");
let second = save_tee("cargo test --lib", "output run 2").expect("tee saved");
assert_eq!(first, second, "same command must map to the same tee path");
let other = save_tee("cargo build", "output").expect("tee saved");
assert_ne!(first, other, "different commands get different tee paths");
let content = std::fs::read_to_string(&second).unwrap();
assert!(content.contains("run 2"));
for p in [first, other] {
let _ = std::fs::remove_file(p);
}
}
}