Skip to main content

lean_ctx/core/agents/
persistence.rs

1use super::AgentRegistry;
2use std::path::PathBuf;
3
4pub(super) fn agents_dir() -> Result<PathBuf, String> {
5    let dir = crate::core::data_dir::lean_ctx_data_dir()?;
6    Ok(dir.join("agents"))
7}
8
9pub(super) fn mutate_persistent<T>(
10    mutate: impl FnOnce(&mut AgentRegistry) -> T,
11) -> Result<T, String> {
12    let dir = agents_dir()?;
13    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
14    let _lock = FileLock::acquire(&dir.join("registry.lock"))?;
15    let path = dir.join("registry.json");
16    let mut registry = std::fs::read_to_string(&path)
17        .ok()
18        .and_then(|content| serde_json::from_str(&content).ok())
19        .unwrap_or_default();
20    let result = mutate(&mut registry);
21    let json = serde_json::to_string_pretty(&registry).map_err(|e| e.to_string())?;
22    std::fs::write(path, json).map_err(|e| e.to_string())?;
23    Ok(result)
24}
25
26pub(super) fn generate_short_id() -> String {
27    use std::collections::hash_map::DefaultHasher;
28    use std::hash::{Hash, Hasher};
29    use std::time::SystemTime;
30
31    let mut hasher = DefaultHasher::new();
32    SystemTime::now().hash(&mut hasher);
33    std::process::id().hash(&mut hasher);
34    format!("{:08x}", hasher.finish() as u32)
35}
36
37/// #576 already fixed this exact hardcoded-`true` anti-pattern for
38/// `daemon::is_daemon_running` by delegating to `ipc::process::is_alive`
39/// (which has a real Windows `OpenProcess` check); this duplicate copy was
40/// missed, so on non-unix targets `cleanup_stale` could never flip a dead
41/// MCP session's entry to `Finished`, leaving `registry.json` accumulating
42/// stale `Active` entries forever — the root cause of the "N active agents"
43/// dashboard bug on Windows.
44pub fn is_process_alive(pid: u32) -> bool {
45    crate::ipc::process::is_alive(pid)
46}
47
48pub(crate) struct FileLock {
49    path: PathBuf,
50}
51
52impl FileLock {
53    pub(crate) fn acquire(path: &std::path::Path) -> Result<Self, String> {
54        for _ in 0..50 {
55            if std::fs::OpenOptions::new()
56                .write(true)
57                .create_new(true)
58                .open(path)
59                .is_ok()
60            {
61                return Ok(Self {
62                    path: path.to_path_buf(),
63                });
64            }
65            if let Ok(metadata) = std::fs::metadata(path)
66                && let Ok(modified) = metadata.modified()
67                && modified.elapsed().unwrap_or_default().as_secs() > 5
68            {
69                let _ = std::fs::remove_file(path);
70                continue;
71            }
72            std::thread::sleep(std::time::Duration::from_millis(100));
73        }
74        Err("Could not acquire lock after 5 seconds".to_string())
75    }
76}
77
78impl Drop for FileLock {
79    fn drop(&mut self) {
80        let _ = std::fs::remove_file(&self.path);
81    }
82}