perspt_agent/
ledger.rs

1//! DuckDB Merkle Ledger
2//!
3//! Persistent storage for session history, commits, and Merkle proofs.
4
5use anyhow::Result;
6use std::path::Path;
7
8/// Merkle commit record
9#[derive(Debug, Clone)]
10pub struct MerkleCommit {
11    pub commit_id: String,
12    pub session_id: String,
13    pub node_id: String,
14    pub merkle_root: [u8; 32],
15    pub parent_hash: Option<[u8; 32]>,
16    pub timestamp: i64,
17    pub energy: f32,
18    pub stable: bool,
19}
20
21/// Session record
22#[derive(Debug, Clone)]
23pub struct SessionRecord {
24    pub session_id: String,
25    pub task: String,
26    pub started_at: i64,
27    pub ended_at: Option<i64>,
28    pub status: String,
29    pub total_nodes: usize,
30    pub completed_nodes: usize,
31}
32
33/// Merkle Ledger using DuckDB for persistence
34pub struct MerkleLedger {
35    /// Database connection string
36    db_path: String,
37    /// In-memory cache of current session
38    current_session: Option<SessionRecord>,
39}
40
41impl MerkleLedger {
42    /// Create a new ledger (opens or creates database)
43    pub fn new(db_path: &str) -> Result<Self> {
44        let ledger = Self {
45            db_path: db_path.to_string(),
46            current_session: None,
47        };
48
49        // Initialize schema
50        ledger.init_schema()?;
51
52        Ok(ledger)
53    }
54
55    /// Create an in-memory ledger (for testing)
56    pub fn in_memory() -> Result<Self> {
57        Self::new(":memory:")
58    }
59
60    /// Initialize database schema
61    fn init_schema(&self) -> Result<()> {
62        // Note: In a real implementation, this would use duckdb crate
63        // For now, we'll use a file-based approach with JSON
64        log::info!("Initializing Merkle Ledger at: {}", self.db_path);
65
66        // Create the ledger directory if needed
67        if self.db_path != ":memory:" {
68            if let Some(parent) = Path::new(&self.db_path).parent() {
69                std::fs::create_dir_all(parent)?;
70            }
71        }
72
73        Ok(())
74    }
75
76    /// Start a new session
77    pub fn start_session(&mut self, session_id: &str, task: &str) -> Result<()> {
78        let record = SessionRecord {
79            session_id: session_id.to_string(),
80            task: task.to_string(),
81            started_at: chrono_timestamp(),
82            ended_at: None,
83            status: "RUNNING".to_string(),
84            total_nodes: 0,
85            completed_nodes: 0,
86        };
87
88        self.current_session = Some(record);
89        log::info!("Started session: {}", session_id);
90
91        Ok(())
92    }
93
94    /// Commit a stable node state
95    pub fn commit_node(
96        &mut self,
97        node_id: &str,
98        merkle_root: [u8; 32],
99        parent_hash: Option<[u8; 32]>,
100        energy: f32,
101    ) -> Result<String> {
102        let session_id = self
103            .current_session
104            .as_ref()
105            .map(|s| s.session_id.clone())
106            .unwrap_or_else(|| "unknown".to_string());
107
108        let commit = MerkleCommit {
109            commit_id: generate_commit_id(),
110            session_id,
111            node_id: node_id.to_string(),
112            merkle_root,
113            parent_hash,
114            timestamp: chrono_timestamp(),
115            energy,
116            stable: energy < 0.1,
117        };
118
119        log::info!("Committed node {} with energy {:.4}", node_id, energy);
120
121        // Update session progress
122        if let Some(ref mut session) = self.current_session {
123            session.completed_nodes += 1;
124        }
125
126        Ok(commit.commit_id)
127    }
128
129    /// End the current session
130    pub fn end_session(&mut self, status: &str) -> Result<()> {
131        if let Some(ref mut session) = self.current_session {
132            session.ended_at = Some(chrono_timestamp());
133            session.status = status.to_string();
134            log::info!(
135                "Ended session {} with status: {}",
136                session.session_id,
137                status
138            );
139        }
140        Ok(())
141    }
142
143    /// Get recent commits for a session
144    pub fn get_recent_commits(&self, session_id: &str, limit: usize) -> Vec<MerkleCommit> {
145        // Placeholder - would query DuckDB
146        log::debug!(
147            "Getting recent {} commits for session {}",
148            limit,
149            session_id
150        );
151        Vec::new()
152    }
153
154    /// Rollback to a specific commit
155    pub fn rollback_to(&mut self, commit_id: &str) -> Result<()> {
156        log::info!("Rolling back to commit: {}", commit_id);
157        // Would restore state from the commit
158        Ok(())
159    }
160
161    /// Get session statistics
162    pub fn get_stats(&self) -> LedgerStats {
163        LedgerStats {
164            total_sessions: 0,
165            total_commits: 0,
166            db_size_bytes: 0,
167        }
168    }
169
170    /// Get the current merkle root
171    pub fn current_merkle_root(&self) -> [u8; 32] {
172        [0u8; 32] // Placeholder
173    }
174}
175
176/// Ledger statistics
177#[derive(Debug, Clone)]
178pub struct LedgerStats {
179    pub total_sessions: usize,
180    pub total_commits: usize,
181    pub db_size_bytes: u64,
182}
183
184/// Generate a unique commit ID
185fn generate_commit_id() -> String {
186    use std::time::{SystemTime, UNIX_EPOCH};
187    let now = SystemTime::now()
188        .duration_since(UNIX_EPOCH)
189        .unwrap()
190        .as_nanos();
191    format!("{:x}", now)
192}
193
194/// Get current timestamp
195fn chrono_timestamp() -> i64 {
196    use std::time::{SystemTime, UNIX_EPOCH};
197    SystemTime::now()
198        .duration_since(UNIX_EPOCH)
199        .unwrap()
200        .as_secs() as i64
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_ledger_creation() {
209        let ledger = MerkleLedger::in_memory().unwrap();
210        assert!(ledger.current_session.is_none());
211    }
212
213    #[test]
214    fn test_session_lifecycle() {
215        let mut ledger = MerkleLedger::in_memory().unwrap();
216
217        ledger.start_session("test-123", "Test task").unwrap();
218        assert!(ledger.current_session.is_some());
219
220        ledger.commit_node("node-1", [0u8; 32], None, 0.05).unwrap();
221        assert_eq!(ledger.current_session.as_ref().unwrap().completed_nodes, 1);
222
223        ledger.end_session("COMPLETED").unwrap();
224        assert_eq!(ledger.current_session.as_ref().unwrap().status, "COMPLETED");
225    }
226}