kaizen-cli 0.1.45

Distributable agent observability: real-time-tailable sessions, agile-style retros, and repo-level improvement (Cursor, Claude Code, Codex). SQLite, redact before any sync you enable.
Documentation
use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};

const MAX_SCAN_BYTES: u64 = 8 * 1024 * 1024;
const MAX_ENTRIES: usize = 128;

#[derive(Clone)]
struct Entry {
    len: u64,
    prompt: Option<String>,
}

pub(super) fn from_trace(raw: &str) -> Option<String> {
    let path = trusted_trace(raw)?;
    let len = std::fs::metadata(&path).ok()?.len();
    let mut cache = prompt_cache().lock().ok()?;
    if let Some(entry) = cache.get_mut(&path) {
        return refresh(&path, len, entry);
    }
    let prompt = read_latest(&path, len.saturating_sub(MAX_SCAN_BYTES));
    insert(&mut cache, path, len, prompt.clone());
    prompt
}

fn refresh(path: &Path, len: u64, entry: &mut Entry) -> Option<String> {
    if len != entry.len {
        let start = if len < entry.len {
            len.saturating_sub(MAX_SCAN_BYTES)
        } else {
            entry.len.max(len.saturating_sub(MAX_SCAN_BYTES))
        };
        entry.prompt = read_latest(path, start).or_else(|| entry.prompt.clone());
        entry.len = len;
    }
    entry.prompt.clone()
}

fn insert(cache: &mut HashMap<PathBuf, Entry>, path: PathBuf, len: u64, prompt: Option<String>) {
    if cache.len() >= MAX_ENTRIES {
        cache.clear();
    }
    cache.insert(path, Entry { len, prompt });
}

fn read_latest(path: &Path, start: u64) -> Option<String> {
    let text = read_from(path, start)?;
    text.lines()
        .rev()
        .find_map(super::event_display::prompt_from_line)
}

fn read_from(path: &Path, start: u64) -> Option<String> {
    let mut file = std::fs::File::open(path).ok()?;
    file.seek(SeekFrom::Start(start)).ok()?;
    let mut bytes = Vec::new();
    file.read_to_end(&mut bytes).ok()?;
    Some(String::from_utf8_lossy(&bytes).into_owned())
}

fn trusted_trace(raw: &str) -> Option<PathBuf> {
    let path = std::fs::canonicalize(raw).ok()?;
    let home = std::fs::canonicalize(std::env::var_os("HOME")?).ok()?;
    trusted_root(&path, &home).then_some(path)
}

fn trusted_root(path: &Path, home: &Path) -> bool {
    path.extension().is_some_and(|ext| ext == "jsonl")
        && [".codex/sessions", ".claude/projects", ".cursor/projects"]
            .iter()
            .any(|root| path.starts_with(home.join(root)))
}

fn prompt_cache() -> &'static Mutex<HashMap<PathBuf, Entry>> {
    static CACHE: OnceLock<Mutex<HashMap<PathBuf, Entry>>> = OnceLock::new();
    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn refresh_replaces_prompt_after_trace_truncation() {
        let temp = tempfile::NamedTempFile::new().unwrap();
        let line = serde_json::json!({"role":"user","content":"New prompt"});
        std::fs::write(temp.path(), format!("{line}\n")).unwrap();
        let len = std::fs::metadata(temp.path()).unwrap().len();
        let mut entry = Entry {
            len: len + 100,
            prompt: Some("Old prompt".into()),
        };
        assert_eq!(
            refresh(temp.path(), len, &mut entry).as_deref(),
            Some("New prompt")
        );
    }
}