zag_agent/
process_store.rs1use crate::config::Config;
7use anyhow::{Context, Result};
8use chrono::{DateTime, FixedOffset};
9use log::debug;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ProcessEntry {
15 pub id: String,
17 pub pid: u32,
19 #[serde(default)]
21 pub session_id: Option<String>,
22 pub provider: String,
23 pub model: String,
24 pub command: String,
26 #[serde(default)]
28 pub prompt: Option<String>,
29 pub started_at: String,
30 pub status: String,
32 #[serde(default)]
33 pub exit_code: Option<i32>,
34 #[serde(default)]
35 pub exited_at: Option<String>,
36 #[serde(default)]
38 pub root: Option<String>,
39 #[serde(default)]
41 pub parent_process_id: Option<String>,
42 #[serde(default)]
44 pub parent_session_id: Option<String>,
45}
46
47#[derive(Debug, Clone, Default, Serialize, Deserialize)]
48pub struct ProcessStore {
49 pub processes: Vec<ProcessEntry>,
50}
51
52impl ProcessStore {
53 fn path() -> PathBuf {
54 Config::global_base_dir().join("processes.json")
55 }
56
57 pub fn load() -> Result<Self> {
59 let path = Self::path();
60 debug!("Loading process store from {}", path.display());
61 if !path.exists() {
62 return Ok(Self::default());
63 }
64 let content = std::fs::read_to_string(&path)
65 .with_context(|| format!("Failed to read process store: {}", path.display()))?;
66 let store: ProcessStore = serde_json::from_str(&content)
67 .with_context(|| format!("Failed to parse process store: {}", path.display()))?;
68 debug!("Loaded {} process entries", store.processes.len());
69 Ok(store)
70 }
71
72 pub fn save(&self) -> Result<()> {
74 let path = Self::path();
75 if let Some(parent) = path.parent() {
76 std::fs::create_dir_all(parent)
77 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
78 }
79 let content =
80 serde_json::to_string_pretty(self).context("Failed to serialize process store")?;
81 crate::file_util::atomic_write_str(&path, &content)
82 .with_context(|| format!("Failed to write process store: {}", path.display()))?;
83 debug!("Process store saved ({} entries)", self.processes.len());
84 Ok(())
85 }
86
87 pub fn add(&mut self, entry: ProcessEntry) {
89 self.processes.retain(|e| e.id != entry.id);
90 debug!(
91 "Adding process: id={}, pid={}, provider={}",
92 entry.id, entry.pid, entry.provider
93 );
94 self.processes.push(entry);
95 }
96
97 pub fn update_status(&mut self, id: &str, status: &str, exit_code: Option<i32>) {
99 if let Some(entry) = self.processes.iter_mut().find(|e| e.id == id) {
100 entry.status = status.to_string();
101 entry.exit_code = exit_code;
102 entry.exited_at = Some(chrono::Utc::now().to_rfc3339());
103 debug!(
104 "Updated process {}: status={}, exit_code={:?}",
105 id, status, exit_code
106 );
107 }
108 }
109
110 pub fn find(&self, id: &str) -> Option<&ProcessEntry> {
112 self.processes.iter().find(|e| e.id == id)
113 }
114
115 pub fn list_recent(&self, limit: Option<usize>) -> Vec<&ProcessEntry> {
117 let mut entries: Vec<&ProcessEntry> = self.processes.iter().collect();
118 entries.sort_by(|a, b| {
119 parse_started_at(&b.started_at)
120 .cmp(&parse_started_at(&a.started_at))
121 .then_with(|| b.id.cmp(&a.id))
122 });
123 if let Some(n) = limit {
124 entries.truncate(n);
125 }
126 entries
127 }
128}
129
130fn parse_started_at(s: &str) -> Option<DateTime<FixedOffset>> {
131 DateTime::parse_from_rfc3339(s).ok()
132}
133
134#[cfg(test)]
135#[path = "process_store_tests.rs"]
136mod tests;