use std::path::Path;
use serde::Serialize;
pub const MIN_GC_RETENTION_DAYS: u64 = 3;
pub const DEFAULT_GC_TTL_DAYS: u64 = 14;
pub const DEFAULT_GC_RETENTION_DAYS: u64 = 14;
pub const GC_HEAT_THRESHOLD: f64 = 1.5;
pub const PIN_SUFFIX: &str = ".pin";
#[derive(Debug, Clone, PartialEq)]
pub struct GcCandidate {
pub path: String,
pub session_id: String,
pub last_activity_epoch_secs: u64,
pub recalls: u64,
}
impl GcCandidate {
pub fn new(path: &str, session_id: &str, last_activity_epoch_secs: u64, recalls: u64) -> Self {
Self {
path: path.to_string(),
session_id: session_id.to_string(),
last_activity_epoch_secs,
recalls,
}
}
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct GcReport {
pub scanned: usize,
pub removed: usize,
pub failed: usize,
pub exempt_pinned: usize,
pub exempt_high_heat: usize,
pub exempt_recent: usize,
pub summary: String,
}
pub fn gc_candidates(
candidates: &[GcCandidate],
ttl_days: u64,
heat_threshold: f64,
pinned_paths: &[&str],
) -> Vec<GcCandidate> {
let ttl_secs = ttl_days.max(MIN_GC_RETENTION_DAYS) * 86400;
let now_secs = now_unix();
candidates
.iter()
.filter(|c| {
let age = now_secs.saturating_sub(c.last_activity_epoch_secs);
let heat = crate::session::heat_score(c.recalls, c.last_activity_epoch_secs, now_secs);
age >= ttl_secs && heat < heat_threshold && !pinned_paths.contains(&c.path.as_str())
})
.cloned()
.collect()
}
pub fn reclaim_gc_candidates(candidates: &[GcCandidate]) -> Result<GcReport, String> {
let mut removed = 0usize;
let mut failed = 0usize;
for c in candidates {
match std::fs::remove_file(&c.path) {
Ok(()) => removed += 1,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => {
failed += 1;
tracing::warn!(
target: "leankg::session",
error = %e,
path = %c.path,
"session GC failed to remove ref"
);
}
}
}
Ok(GcReport {
scanned: candidates.len(),
removed,
failed,
exempt_pinned: 0,
exempt_high_heat: 0,
exempt_recent: 0,
summary: format!(
"session GC: removed {removed} of {} candidates",
candidates.len()
),
})
}
pub fn is_pinned(path: &Path) -> bool {
let mut marker = path.as_os_str().to_os_string();
marker.push(PIN_SUFFIX);
Path::new(&marker).exists()
}
fn now_unix() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_pin_suffix_is_dot_pin() {
assert_eq!(PIN_SUFFIX, ".pin");
assert!(is_pinned(Path::new("/x/refs/a.md.pin")) == false);
}
}