From e27d63a55a9875a6c5c51220096bc45f49e22a32 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Fri, 12 Jun 2026 02:48:57 -0700 Subject: [PATCH] chore(tui): remove unused prompt persistence module Harvests the dead-code cleanup from #3135. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Hmbown <101357273+Hmbown@users.noreply.github.com> --- crates/tui/src/main.rs | 1 - crates/tui/src/prompt_persist.rs | 287 ------------------------------- 2 files changed, 288 deletions(-) delete mode 100644 crates/tui/src/prompt_persist.rs diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 93108c10..67ed3447 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -57,7 +57,6 @@ mod pricing; mod project_context; mod project_context_cache; mod project_doc; -mod prompt_persist; mod prompt_zones; mod prompts; mod purge; diff --git a/crates/tui/src/prompt_persist.rs b/crates/tui/src/prompt_persist.rs deleted file mode 100644 index b2ab1190..00000000 --- a/crates/tui/src/prompt_persist.rs +++ /dev/null @@ -1,287 +0,0 @@ -//! Cross-session persistence for the immutable base section of the system -//! prompt. -//! -//! ## Why -//! -//! DeepSeek's KV prefix cache matches byte sequences from the start of the -//! system prompt. The base section (mode prompt, project context, skills, -//! context management, compaction template) is stable across sessions for -//! the same workspace. By caching this section on disk and reusing it when -//! the SHA-256 matches, we can skip the entire base-section assembly on -//! session start and immediately provide byte-identical bytes to the API. -//! -//! This is especially valuable for the DeepSeek service-side prefix cache: -//! when the base section bytes are identical across sessions, the server -//! can reuse its cached KV states for the entire base section, giving -//! ~90% discount on cached tokens. -//! -//! ## Cache layout -//! -//! ```text -//! ~/.codewhale/prompt_cache/ -//! .bin — the serialized base section text -//! .meta — JSON metadata (workspace path, mtime, timestamp) -//! ``` -//! -//! The cache key is the SHA-256 of the base section text, computed by -//! `PrefixFingerprint::compute`. The metadata file includes the workspace -//! path and its mtime so that workspace changes invalidate the cache even -//! if the base section hash happens to collide (extremely unlikely with -//! SHA-256, but cheap to guard against). - -use std::fs; -use std::path::{Path, PathBuf}; -use std::time::SystemTime; - -use serde::{Deserialize, Serialize}; - -use crate::logging; - -/// Metadata stored alongside a cached base section. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[allow(dead_code)] -struct CacheMetadata { - /// Absolute path to the workspace that produced this base section. - workspace: PathBuf, - /// Modification time of the workspace directory at cache-write time. - /// Used as a secondary invalidation signal: if the workspace mtime - /// changed, the cache is stale even if the base section hash matches - /// (which would require a hash collision). - workspace_mtime_secs: u64, - /// Unix timestamp when the cache was written. - cached_at_secs: u64, -} - -/// Return the directory where prompt caches are stored. -/// -/// Creates the directory if it doesn't exist. -#[allow(dead_code)] -fn cache_dir() -> Option { - // Override hook so tests never touch the real ~/.codewhale cache (and - // never race each other through it). - let dir = match std::env::var_os("CODEWHALE_PROMPT_CACHE_DIR") { - Some(dir) if !dir.is_empty() => PathBuf::from(dir), - _ => dirs::home_dir()?.join(".codewhale").join("prompt_cache"), - }; - if let Err(err) = fs::create_dir_all(&dir) { - logging::warn(format!("Failed to create prompt cache dir: {err}")); - return None; - } - Some(dir) -} - -/// Get the modification time of a directory as seconds since epoch. -#[allow(dead_code)] -fn dir_mtime_secs(path: &Path) -> u64 { - fs::metadata(path) - .and_then(|m| m.modified()) - .ok() - .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|d| d.as_secs()) - .unwrap_or(0) -} - -/// Try to load a cached base section from disk. -/// -/// Returns `Some(text)` if a valid cache entry exists for the given hash -/// and workspace, or `None` if the cache is missing, stale, or corrupt. -#[allow(dead_code)] -pub fn load_cached_base_section(base_hash: &str, workspace: &Path) -> Option { - let dir = cache_dir()?; - let bin_path = dir.join(format!("{base_hash}.bin")); - let meta_path = dir.join(format!("{base_hash}.meta")); - - // Check that both files exist. - if !bin_path.exists() || !meta_path.exists() { - return None; - } - - // Read and validate metadata. - let meta_bytes = fs::read(&meta_path).ok()?; - let meta: CacheMetadata = serde_json::from_slice(&meta_bytes).ok()?; - - // Verify workspace path matches. - if meta.workspace != workspace { - return None; - } - - // Verify workspace mtime hasn't changed (guards against hash collisions). - let current_mtime = dir_mtime_secs(workspace); - if current_mtime != meta.workspace_mtime_secs { - logging::info(format!( - "Prompt cache stale: workspace mtime changed ({meta_mtime} → {current_mtime})", - meta_mtime = meta.workspace_mtime_secs - )); - return None; - } - - // Read the cached base section. - let text = fs::read_to_string(&bin_path).ok()?; - logging::info(format!( - "Prompt cache hit: {base_hash} ({} bytes)", - text.len() - )); - Some(text) -} - -/// Save a base section to disk for cross-session reuse. -/// -/// The cache key is `base_hash` (SHA-256 of the base section text). The -/// metadata includes the workspace path and its mtime for invalidation. -#[allow(dead_code)] -pub fn save_cached_base_section(base_hash: &str, base_text: &str, workspace: &Path) { - let dir = match cache_dir() { - Some(d) => d, - None => return, - }; - - let bin_path = dir.join(format!("{base_hash}.bin")); - let meta_path = dir.join(format!("{base_hash}.meta")); - - // Write the base section text. - if let Err(err) = fs::write(&bin_path, base_text) { - logging::warn(format!("Failed to write prompt cache bin: {err}")); - return; - } - - // Write the metadata. - let meta = CacheMetadata { - workspace: workspace.to_path_buf(), - workspace_mtime_secs: dir_mtime_secs(workspace), - cached_at_secs: SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0), - }; - if let Err(err) = fs::write(&meta_path, serde_json::to_vec(&meta).unwrap_or_default()) { - logging::warn(format!("Failed to write prompt cache meta: {err}")); - } - - logging::info(format!("Prompt cache saved: {base_hash}")); -} - -/// Evict stale cache entries. -/// -/// Removes cache entries older than `max_age_secs` or whose workspace -/// mtime no longer matches. This is a best-effort cleanup; it runs -/// lazily when the cache is accessed. -#[allow(dead_code)] -pub fn evict_stale_entries(max_age_secs: u64) { - let dir = match cache_dir() { - Some(d) => d, - None => return, - }; - - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - - let entries = match fs::read_dir(&dir) { - Ok(e) => e, - Err(_) => return, - }; - - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().is_some_and(|e| e == "meta") - && let Ok(bytes) = fs::read(&path) - && let Ok(meta) = serde_json::from_slice::(&bytes) - { - let stale = now.saturating_sub(meta.cached_at_secs) > max_age_secs; - let workspace_gone = !meta.workspace.exists(); - let mtime_changed = - workspace_gone || dir_mtime_secs(&meta.workspace) != meta.workspace_mtime_secs; - - if stale || workspace_gone || mtime_changed { - let hash = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown"); - let _ = fs::remove_file(&path); - let _ = fs::remove_file(path.with_extension("bin")); - logging::info(format!("Evicted prompt cache: {hash}")); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - /// Point the cache at a private tempdir for the duration of `f`. - /// - /// The env var is process-global, so a mutex serializes these tests; - /// without it they all share ~/.codewhale/prompt_cache and the eviction - /// test can delete another test's entry mid-flight (and the suite - /// pollutes the developer's real cache). - fn with_isolated_cache_dir(f: impl FnOnce() -> T) -> T { - static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let cache_tmp = tempdir().expect("tempdir"); - // SAFETY: serialized by ENV_LOCK; only this module reads the var. - unsafe { std::env::set_var("CODEWHALE_PROMPT_CACHE_DIR", cache_tmp.path()) }; - let out = f(); - unsafe { std::env::remove_var("CODEWHALE_PROMPT_CACHE_DIR") }; - out - } - - #[test] - fn save_and_load_round_trip() { - with_isolated_cache_dir(|| { - let tmp = tempdir().expect("tempdir"); - let workspace = tmp.path(); - let hash = "abc123"; - let text = "Hello, world!"; - - save_cached_base_section(hash, text, workspace); - let loaded = load_cached_base_section(hash, workspace); - assert_eq!(loaded.as_deref(), Some(text)); - }); - } - - #[test] - fn load_returns_none_for_missing_cache() { - with_isolated_cache_dir(|| { - let tmp = tempdir().expect("tempdir"); - assert!(load_cached_base_section("nonexistent", tmp.path()).is_none()); - }); - } - - #[test] - fn load_returns_none_for_wrong_workspace() { - with_isolated_cache_dir(|| { - let tmp1 = tempdir().expect("tempdir"); - let tmp2 = tempdir().expect("tempdir"); - let hash = "def456"; - let text = "cached content"; - - save_cached_base_section(hash, text, tmp1.path()); - assert!(load_cached_base_section(hash, tmp2.path()).is_none()); - }); - } - - #[test] - fn evict_preserves_fresh_entries() { - with_isolated_cache_dir(|| { - let tmp = tempdir().expect("tempdir"); - let workspace = tmp.path(); - let hash = "fresh_entry"; - let text = "fresh content"; - - save_cached_base_section(hash, text, workspace); - - // Evict entries older than 3600 seconds (1 hour). Fresh entries - // should survive. - evict_stale_entries(3600); - - // The entry should still be there since it was just saved. - assert_eq!( - load_cached_base_section(hash, workspace).as_deref(), - Some(text) - ); - }); - } -}