Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 66 additions & 41 deletions rust/src/cli/config_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -915,16 +915,18 @@ pub fn cmd_cache(args: &[String]) {
let orphans = crate::core::knowledge::maintenance::prune_orphaned_stores();

let removed = bm25.removed + graph.removed + archive_removed + orphans.removed as u32;
let failed = bm25.failed + graph.failed;
let freed =
bm25.bytes_freed + graph.bytes_freed + archive_freed + orphans.reclaimed_bytes;
println!(
"Pruned {} entries, freed {:.1} MB (BM25: {}, graphs: {}, archive: {}, orphaned stores: {})",
"Pruned {} entries, freed {:.1} MB (BM25: {}, graphs: {}, archive: {}, orphaned stores: {}, failed: {})",
removed,
freed as f64 / 1_048_576.0,
bm25.removed,
graph.removed,
archive_removed,
orphans.removed,
failed,
);
}
_ => {
Expand All @@ -951,13 +953,50 @@ pub fn cmd_cache(args: &[String]) {
pub struct PruneResult {
pub scanned: u32,
pub removed: u32,
pub failed: u32,
pub bytes_freed: u64,
}

fn record_removed_file(result: &mut PruneResult, path: &std::path::Path, label: &str) {
let bytes = std::fs::metadata(path).map_or(0, |meta| meta.len());

match std::fs::remove_file(path) {
Ok(()) => {
result.bytes_freed += bytes;
result.removed += 1;
println!(" {label}: {}", path.display());
}
Err(error) => {
result.failed += 1;
eprintln!(" Failed to remove {}: {error}", path.display());
}
}
}

fn record_removed_dir(
result: &mut PruneResult,
path: &std::path::Path,
bytes: u64,
message: impl FnOnce() -> String,
) {
match std::fs::remove_dir_all(path) {
Ok(()) => {
result.bytes_freed += bytes;
result.removed += 1;
println!(" {}", message());
}
Err(error) => {
result.failed += 1;
eprintln!(" Failed to remove {}: {error}", path.display());
}
}
}

pub fn prune_bm25_caches() -> PruneResult {
let mut result = PruneResult {
scanned: 0,
removed: 0,
failed: 0,
bytes_freed: 0,
};

Expand Down Expand Up @@ -985,12 +1024,7 @@ pub fn prune_bm25_caches() -> PruneResult {
] {
let quarantined = dir.join(q_name);
if quarantined.exists() {
if let Ok(meta) = std::fs::metadata(&quarantined) {
result.bytes_freed += meta.len();
}
let _ = std::fs::remove_file(&quarantined);
result.removed += 1;
println!(" Removed quarantined: {}", quarantined.display());
record_removed_file(&mut result, &quarantined, "Removed quarantined");
}
}

Expand All @@ -1004,30 +1038,22 @@ pub fn prune_bm25_caches() -> PruneResult {
if let Ok(meta) = std::fs::metadata(&index_path)
&& meta.len() > max_bytes
{
result.bytes_freed += meta.len();
let _ = std::fs::remove_file(&index_path);
result.removed += 1;
println!(
" Removed oversized ({:.1} MB): {}",
meta.len() as f64 / 1_048_576.0,
index_path.display()
);
record_removed_file(&mut result, &index_path, "Removed oversized");
}

let marker = dir.join("project_root.txt");
if let Ok(root_str) = std::fs::read_to_string(&marker) {
let root_path = std::path::Path::new(root_str.trim());
if !root_path.exists() {
let freed = dir_size(&dir);
result.bytes_freed += freed;
let _ = std::fs::remove_dir_all(&dir);
result.removed += 1;
println!(
" Removed orphaned ({:.1} MB, project gone: {}): {}",
freed as f64 / 1_048_576.0,
root_str.trim(),
dir.display()
);
record_removed_dir(&mut result, &dir, freed, || {
format!(
"Removed orphaned ({:.1} MB, project gone: {}): {}",
freed as f64 / 1_048_576.0,
root_str.trim(),
dir.display()
)
});
}
}
}
Expand All @@ -1039,6 +1065,7 @@ pub fn prune_graph_caches() -> PruneResult {
let mut result = PruneResult {
scanned: 0,
removed: 0,
failed: 0,
bytes_freed: 0,
};

Expand Down Expand Up @@ -1072,15 +1099,14 @@ pub fn prune_graph_caches() -> PruneResult {
&& !std::path::Path::new(&root).exists()
{
let freed = dir_size(&dir);
result.bytes_freed += freed;
let _ = std::fs::remove_dir_all(&dir);
result.removed += 1;
println!(
" Removed orphaned graph ({:.1} MB, project gone: {}): {}",
freed as f64 / 1_048_576.0,
root,
dir.display()
);
record_removed_dir(&mut result, &dir, freed, || {
format!(
"Removed orphaned graph ({:.1} MB, project gone: {}): {}",
freed as f64 / 1_048_576.0,
root,
dir.display()
)
});
continue;
}

Expand All @@ -1091,14 +1117,13 @@ pub fn prune_graph_caches() -> PruneResult {
&& meta.len() > 100 * 1024 * 1024
{
let freed = dir_size(&dir);
result.bytes_freed += freed;
let _ = std::fs::remove_dir_all(&dir);
result.removed += 1;
println!(
" Removed oversized graph ({:.1} MB): {}",
freed as f64 / 1_048_576.0,
dir.display()
);
record_removed_dir(&mut result, &dir, freed, || {
format!(
"Removed oversized graph ({:.1} MB): {}",
freed as f64 / 1_048_576.0,
dir.display()
)
});
}
}

Expand Down
3 changes: 2 additions & 1 deletion rust/src/doctor/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,10 @@ fn build_and_persist_fix_report(
},
path: None,
note: Some(format!(
"scanned {}, removed {}, freed {:.1} MB",
"scanned {}, removed {}, failed {}, freed {:.1} MB",
prune_result.scanned,
prune_result.removed,
prune_result.failed,
prune_result.bytes_freed as f64 / 1_048_576.0
)),
});
Expand Down
31 changes: 31 additions & 0 deletions rust/tests/index_scoping_scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,39 @@ fn prune_caches_handles_empty_isolated_dir() {

let result = lean_ctx::cli::prune_graph_caches();
assert_eq!(result.removed, 0);
assert_eq!(result.failed, 0);
assert_eq!(result.bytes_freed, 0);
let result2 = lean_ctx::cli::prune_bm25_caches();
assert_eq!(result2.removed, 0);
assert_eq!(result2.failed, 0);
assert_eq!(result2.bytes_freed, 0);
}

#[cfg(unix)]
#[test]
fn prune_bm25_counts_failed_remove_without_freed_bytes() {
use std::os::unix::fs::PermissionsExt;

let tmp = tempfile::tempdir().unwrap();
let _guard = EnvGuard::new("LEAN_CTX_DATA_DIR", tmp.path().to_str().unwrap());
let vectors_dir = tmp.path().join("vectors");
let cache_dir = vectors_dir.join("blocked");
std::fs::create_dir_all(&cache_dir).unwrap();
std::fs::write(
cache_dir.join("project_root.txt"),
"/definitely/missing/project/root",
)
.unwrap();
std::fs::write(cache_dir.join("bm25_index.bin"), b"keep").unwrap();
std::fs::set_permissions(&vectors_dir, std::fs::Permissions::from_mode(0o500)).unwrap();

let result = lean_ctx::cli::prune_bm25_caches();

std::fs::set_permissions(&vectors_dir, std::fs::Permissions::from_mode(0o700)).unwrap();
assert_eq!(result.removed, 0);
assert_eq!(result.failed, 1);
assert_eq!(result.bytes_freed, 0);
assert!(cache_dir.exists());
}

/// Guards env var modifications so parallel tests don't race.
Expand Down
Loading