1use anyhow::{Context, Result};
6pub use perspt_store::{LlmRequestRecord, NodeStateRecord, SessionRecord, SessionStore};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone)]
11pub struct MerkleCommit {
12 pub commit_id: String,
13 pub session_id: String,
14 pub node_id: String,
15 pub merkle_root: [u8; 32],
16 pub parent_hash: Option<[u8; 32]>,
17 pub timestamp: i64,
18 pub energy: f32,
19 pub stable: bool,
20}
21
22#[derive(Debug, Clone)]
24pub struct SessionRecordLegacy {
25 pub session_id: String,
26 pub task: String,
27 pub started_at: i64,
28 pub ended_at: Option<i64>,
29 pub status: String,
30 pub total_nodes: usize,
31 pub completed_nodes: usize,
32}
33
34pub struct MerkleLedger {
36 store: SessionStore,
38 current_session: Option<SessionRecordLegacy>,
40 session_dir: Option<PathBuf>,
42}
43
44impl MerkleLedger {
45 pub fn new() -> Result<Self> {
47 let store = SessionStore::new().context("Failed to initialize session store")?;
48 Ok(Self {
49 store,
50 current_session: None,
51 session_dir: None,
52 })
53 }
54
55 pub fn in_memory() -> Result<Self> {
57 let temp_dir = std::env::temp_dir();
59 let db_path = temp_dir.join(format!("perspt_test_{}.db", uuid::Uuid::new_v4()));
60 let store = SessionStore::open(&db_path)?;
61 Ok(Self {
62 store,
63 current_session: None,
64 session_dir: None,
65 })
66 }
67
68 pub fn start_session(&mut self, session_id: &str, task: &str, working_dir: &str) -> Result<()> {
70 let record = SessionRecord {
71 session_id: session_id.to_string(),
72 task: task.to_string(),
73 working_dir: working_dir.to_string(),
74 merkle_root: None,
75 detected_toolchain: None,
76 status: "RUNNING".to_string(),
77 };
78
79 self.store.create_session(&record)?;
80
81 let dir = self.store.create_session_dir(session_id)?;
83 self.session_dir = Some(dir);
84
85 let legacy_record = SessionRecordLegacy {
86 session_id: session_id.to_string(),
87 task: task.to_string(),
88 started_at: chrono_timestamp(),
89 ended_at: None,
90 status: "RUNNING".to_string(),
91 total_nodes: 0,
92 completed_nodes: 0,
93 };
94 self.current_session = Some(legacy_record);
95
96 log::info!("Started persistent session: {}", session_id);
97 Ok(())
98 }
99
100 pub fn record_energy(
102 &self,
103 node_id: &str,
104 energy: &crate::types::EnergyComponents,
105 total_energy: f32,
106 ) -> Result<()> {
107 let session_id = self
108 .current_session
109 .as_ref()
110 .map(|s| s.session_id.clone())
111 .context("No active session to record energy")?;
112
113 let record = perspt_store::EnergyRecord {
114 node_id: node_id.to_string(),
115 session_id,
116 v_syn: energy.v_syn,
117 v_str: energy.v_str,
118 v_log: energy.v_log,
119 v_boot: energy.v_boot,
120 v_sheaf: energy.v_sheaf,
121 v_total: total_energy,
122 };
123
124 self.store.record_energy(&record)?;
125 Ok(())
126 }
127
128 pub fn commit_node(
130 &mut self,
131 node_id: &str,
132 merkle_root: [u8; 32],
133 _parent_hash: Option<[u8; 32]>,
134 energy: f32,
135 state_json: String,
136 ) -> Result<String> {
137 let session_id = self
138 .current_session
139 .as_ref()
140 .map(|s| s.session_id.clone())
141 .context("No active session to commit")?;
142
143 let commit_id = generate_commit_id();
144
145 let record = NodeStateRecord {
146 node_id: node_id.to_string(),
147 session_id: session_id.clone(),
148 state: state_json,
149 v_total: energy,
150 merkle_hash: Some(merkle_root.to_vec()),
151 attempt_count: 1, };
153
154 self.store.record_node_state(&record)?;
155 self.store.update_merkle_root(&session_id, &merkle_root)?;
156
157 log::info!("Committed node {} to store", node_id);
158
159 if let Some(ref mut session) = self.current_session {
161 session.completed_nodes += 1;
162 }
163
164 Ok(commit_id)
165 }
166
167 pub fn end_session(&mut self, status: &str) -> Result<()> {
169 if let Some(ref mut session) = self.current_session {
170 session.ended_at = Some(chrono_timestamp());
171 session.status = status.to_string();
172 log::info!(
173 "Ended session {} with status: {}",
174 session.session_id,
175 status
176 );
177 }
178 Ok(())
179 }
180
181 pub fn artifacts_dir(&self) -> Option<&Path> {
183 self.session_dir.as_deref()
184 }
185
186 pub fn get_stats(&self) -> LedgerStats {
188 LedgerStats {
189 total_sessions: 0, total_commits: 0,
191 db_size_bytes: 0,
192 }
193 }
194
195 pub fn current_merkle_root(&self) -> [u8; 32] {
197 [0u8; 32] }
199
200 pub fn record_llm_request(
202 &self,
203 model: &str,
204 prompt: &str,
205 response: &str,
206 node_id: Option<&str>,
207 latency_ms: i32,
208 ) -> Result<()> {
209 let session_id = self
210 .current_session
211 .as_ref()
212 .map(|s| s.session_id.clone())
213 .context("No active session to record LLM request")?;
214
215 let record = LlmRequestRecord {
216 session_id,
217 node_id: node_id.map(|s| s.to_string()),
218 model: model.to_string(),
219 prompt: prompt.to_string(),
220 response: response.to_string(),
221 tokens_in: 0, tokens_out: 0,
223 latency_ms,
224 };
225
226 self.store.record_llm_request(&record)?;
227 log::debug!(
228 "Recorded LLM request: model={}, prompt_len={}, response_len={}",
229 model,
230 prompt.len(),
231 response.len()
232 );
233 Ok(())
234 }
235
236 pub fn store(&self) -> &SessionStore {
238 &self.store
239 }
240}
241
242#[derive(Debug, Clone)]
244pub struct LedgerStats {
245 pub total_sessions: usize,
246 pub total_commits: usize,
247 pub db_size_bytes: u64,
248}
249
250fn generate_commit_id() -> String {
252 use std::time::{SystemTime, UNIX_EPOCH};
253 let now = SystemTime::now()
254 .duration_since(UNIX_EPOCH)
255 .unwrap()
256 .as_nanos();
257 format!("{:x}", now)
258}
259
260fn chrono_timestamp() -> i64 {
262 use std::time::{SystemTime, UNIX_EPOCH};
263 SystemTime::now()
264 .duration_since(UNIX_EPOCH)
265 .unwrap()
266 .as_secs() as i64
267}