From 1f27270d66df3b1447e0f4b9754f2ab2a379fbd8 Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Sun, 26 Jul 2026 09:25:37 +0200 Subject: [PATCH] Reclaim KV checkpoints the index cannot reach Co-Authored-By: Claude Opus 5 --- src/app.rs | 2 ++ src/engine.rs | 2 ++ src/engine/kvstore.rs | 61 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/src/app.rs b/src/app.rs index 152602e..6c8edd5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -228,6 +228,8 @@ impl App { // Before the counters are taken, so they measure what is // actually reachable. sweep_orphan_checkpoints(&kv_cache_path(), &projects); + #[cfg(target_os = "macos")] + crate::engine::sweep_transient_cache(&transient_cache_path()); let context_limit = preferences.context_tokens.max(0) as u32; // Reopen on the project we left, with a fresh draft chat. let last_project = preferences diff --git a/src/engine.rs b/src/engine.rs index 9ef6b9a..628a044 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -26,6 +26,8 @@ use validation::{validate_dspark, validate_main}; pub(crate) use validation::validate_model_artifact; +#[cfg(target_os = "macos")] +pub(crate) use kvstore::sweep_unreachable as sweep_transient_cache; #[cfg(target_os = "macos")] pub(crate) use metal::configure_sources as configure_metal_sources; diff --git a/src/engine/kvstore.rs b/src/engine/kvstore.rs index 07dd9de..a72aa6f 100644 --- a/src/engine/kvstore.rs +++ b/src/engine/kvstore.rs @@ -1,5 +1,6 @@ use sha2::{Digest, Sha256}; use std::cmp::Ordering; +use std::collections::HashSet; use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; @@ -68,6 +69,8 @@ impl KvStore { directory: directory.to_owned(), budget_bytes, }; + // Reclaim first, so the budget is measured against the whole directory. + sweep_unreachable(directory); // A lowered budget takes effect at once, like ds4's eviction on open. store.evict(None); Ok(store) @@ -174,6 +177,41 @@ impl KvStore { } } +/// Deletes what the index cannot reach: a checkpoint whose index is missing or +/// unreadable can never be found, and an index whose checkpoint is gone is +/// dead weight. Without this, an interrupted store or a checkpoint written +/// before the index existed occupies disc that eviction cannot even see, and +/// the directory grows past the budget unnoticed. +pub(crate) fn sweep_unreachable(directory: &Path) { + let Ok(files) = fs::read_dir(directory) else { + return; + }; + let reachable = files + .filter_map(Result::ok) + .map(|file| file.path()) + .filter(|path| path.extension().is_some_and(|value| value == "meta")) + .filter_map(|path| read_entry(&path).ok()) + .filter(|entry| entry.checkpoint.is_file()) + .map(|entry| entry.checkpoint) + .collect::>(); + let Ok(files) = fs::read_dir(directory) else { + return; + }; + for file in files.flatten() { + let path = file.path(); + let dead = match path.extension().and_then(|value| value.to_str()) { + Some("bin") => !reachable.contains(&path), + Some("meta") => !reachable.contains(&path.with_extension("bin")), + // Staging files from an interrupted index write. + Some("tmp") => true, + _ => false, + }; + if dead { + let _ = fs::remove_file(&path); + } + } +} + fn eviction_score(entry: &Entry, now: u64) -> f64 { let age = now.saturating_sub(entry.last_used.max(entry.created_at)) as f64; let hits = f64::from(entry.hits) * f64::exp2(-age / HIT_HALF_LIFE_SECONDS); @@ -364,6 +402,29 @@ mod tests { fs::remove_dir_all(directory).unwrap(); } + #[test] + fn opening_reclaims_checkpoints_the_index_cannot_reach() { + let directory = temporary_directory("unreachable"); + let store = KvStore::open(&directory, GIB).unwrap(); + let indexed = add(&store, b"indexed", 100, 32); + let stray = directory.join("deadbeef.bin"); + let widow = directory.join("widow.meta"); + let staging = directory.join("indexed.meta.tmp"); + fs::write(&stray, vec![0; 4_096]).unwrap(); + fs::write(&widow, vec![0; 64]).unwrap(); + fs::write(&staging, vec![0; 64]).unwrap(); + + KvStore::open(&directory, GIB).unwrap(); + + assert!(indexed.checkpoint.is_file()); + assert!(index_path(&indexed.checkpoint).is_file()); + assert!(!stray.exists()); + assert!(!widow.exists()); + assert!(!staging.exists()); + + fs::remove_dir_all(directory).unwrap(); + } + #[test] fn budget_evicts_the_least_valuable_unprotected_entry() { let directory = temporary_directory("evict");