Skip to main content

qualia_core_db/inference/runtime/artifacts/
cleanup.rs

1use std::fs;
2use std::path::Path;
3use std::time::SystemTime;
4
5use super::budget::ArtifactError;
6use super::run_dir::RUN_MARKER_FILE;
7
8const RUN_PREFIX: &str = "qualia-inference-";
9const MARKER_CONTENT: &[u8] = b"qualia-inference-run-v1\n";
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct StaleCleanupReport {
13    pub candidates: u64,
14    pub removed_runs: u64,
15    pub removed_bytes: u64,
16    pub failures: u64,
17}
18
19fn directory_bytes(path: &Path) -> std::io::Result<u64> {
20    let mut total = 0u64;
21    for entry in fs::read_dir(path)? {
22        let entry = entry?;
23        let file_type = entry.file_type()?;
24        if file_type.is_symlink() {
25            continue;
26        }
27        if file_type.is_dir() {
28            total = total.saturating_add(directory_bytes(&entry.path())?);
29        } else if file_type.is_file() {
30            total = total.saturating_add(entry.metadata()?.len());
31        }
32    }
33    Ok(total)
34}
35
36/// Remove only stale, marker-owned staging runs that are direct children of `parent`.
37///
38/// Retained evidence has a caller-chosen name and is never eligible. Symlink candidates are
39/// rejected, canonical parent containment is verified, and individual failures are counted.
40pub fn cleanup_stale_runs(
41    parent: &Path,
42    modified_before: SystemTime,
43) -> Result<StaleCleanupReport, ArtifactError> {
44    let canonical_parent = parent.canonicalize()?;
45    let mut report = StaleCleanupReport::default();
46    for entry in fs::read_dir(&canonical_parent)? {
47        let Ok(entry) = entry else {
48            report.failures = report.failures.saturating_add(1);
49            continue;
50        };
51        let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
52            continue;
53        };
54        if !name.starts_with(RUN_PREFIX) {
55            continue;
56        }
57        let Ok(file_type) = entry.file_type() else {
58            report.failures = report.failures.saturating_add(1);
59            continue;
60        };
61        if !file_type.is_dir() || file_type.is_symlink() {
62            continue;
63        }
64        report.candidates = report.candidates.saturating_add(1);
65        let path = entry.path();
66        let marker = path.join(RUN_MARKER_FILE);
67        if fs::read(&marker).ok().as_deref() != Some(MARKER_CONTENT) {
68            continue;
69        }
70        let Ok(metadata) = entry.metadata() else {
71            report.failures = report.failures.saturating_add(1);
72            continue;
73        };
74        if metadata
75            .modified()
76            .map_or(true, |time| time >= modified_before)
77        {
78            continue;
79        }
80        let Ok(canonical_path) = path.canonicalize() else {
81            report.failures = report.failures.saturating_add(1);
82            continue;
83        };
84        if canonical_path.parent() != Some(canonical_parent.as_path()) {
85            report.failures = report.failures.saturating_add(1);
86            continue;
87        }
88        let bytes = directory_bytes(&canonical_path).unwrap_or(0);
89        match fs::remove_dir_all(&canonical_path) {
90            Ok(()) => {
91                report.removed_runs = report.removed_runs.saturating_add(1);
92                report.removed_bytes = report.removed_bytes.saturating_add(bytes);
93            }
94            Err(_) => report.failures = report.failures.saturating_add(1),
95        }
96    }
97    Ok(report)
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn removes_only_marker_owned_prefixed_directories() {
106        let parent = tempfile::tempdir().unwrap();
107        let stale = parent.path().join("qualia-inference-test-stale");
108        let unmarked = parent.path().join("qualia-inference-test-unmarked");
109        let retained = parent.path().join("retained-evidence");
110        fs::create_dir_all(&stale).unwrap();
111        fs::create_dir_all(&unmarked).unwrap();
112        fs::create_dir_all(&retained).unwrap();
113        fs::write(stale.join(RUN_MARKER_FILE), MARKER_CONTENT).unwrap();
114        fs::write(stale.join("payload.bin"), [1u8; 8]).unwrap();
115        fs::write(retained.join(RUN_MARKER_FILE), MARKER_CONTENT).unwrap();
116
117        let report = cleanup_stale_runs(
118            parent.path(),
119            SystemTime::now() + std::time::Duration::from_secs(1),
120        )
121        .unwrap();
122        assert_eq!(report.removed_runs, 1);
123        assert!(report.removed_bytes >= 8);
124        assert!(!stale.exists());
125        assert!(unmarked.exists());
126        assert!(retained.exists());
127    }
128}