1use super::*;
16
17impl Store {
18 pub async fn open(path: &Path) -> tokio_rusqlite::Result<Self> {
19 Self::open_with_options(path, StoreOptions::default()).await
20 }
21
22 pub async fn open_with_clock(
23 path: &Path,
24 clock: Arc<dyn lash_core::Clock>,
25 ) -> tokio_rusqlite::Result<Self> {
26 Self::open_with_options_and_clock(path, StoreOptions::default(), clock).await
27 }
28
29 pub async fn open_with_options(
30 path: &Path,
31 options: StoreOptions,
32 ) -> tokio_rusqlite::Result<Self> {
33 Self::open_with_options_and_clock(path, options, Arc::new(lash_core::SystemClock)).await
34 }
35
36 pub async fn open_with_options_and_clock(
37 path: &Path,
38 options: StoreOptions,
39 clock: Arc<dyn lash_core::Clock>,
40 ) -> tokio_rusqlite::Result<Self> {
41 Self::open_with_options_clock_and_process_registry(path, options, clock, None).await
42 }
43
44 pub(crate) async fn open_with_options_clock_and_process_registry(
45 path: &Path,
46 options: StoreOptions,
47 clock: Arc<dyn lash_core::Clock>,
48 process_registry_path: Option<&Path>,
49 ) -> tokio_rusqlite::Result<Self> {
50 let conn = SqliteConnection::open(path).await?;
51 ensure_schema(&conn).await?;
52 apply_pragmas(&conn, StoreBacking::File).await?;
53 let process_registry_attached = if let Some(process_registry_path) = process_registry_path {
54 if !process_registry_path.exists() {
55 return Err(tokio_rusqlite::Error::Error(
56 rusqlite::Error::SqliteFailure(
57 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
58 Some(format!(
59 "configured Lash process registry does not exist: {}",
60 process_registry_path.display()
61 )),
62 ),
63 ));
64 }
65 let path = process_registry_path.to_string_lossy().into_owned();
66 conn.call(move |conn| {
67 conn.execute("ATTACH DATABASE ?1 AS process_registry", params![path])?;
68 let expected_version = crate::schema::PROCESS_SCHEMA_VERSION;
69 let deadline = std::time::Instant::now()
70 + std::time::Duration::from_millis(crate::conn::BUSY_TIMEOUT_MS as u64);
71 loop {
72 let version: i32 = conn.query_row(
73 "PRAGMA process_registry.user_version",
74 [],
75 |row| row.get(0),
76 )?;
77 let has_processes = conn
78 .query_row(
79 "SELECT 1 FROM process_registry.sqlite_master
80 WHERE type = 'table' AND name = 'processes'",
81 [],
82 |_| Ok(()),
83 )
84 .optional()?
85 .is_some();
86 if version == expected_version && has_processes {
87 break;
88 }
89 if version == 0 && !has_processes && std::time::Instant::now() < deadline {
90 std::thread::sleep(std::time::Duration::from_millis(10));
91 continue;
92 }
93 return Err(rusqlite::Error::SqliteFailure(
94 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_MISMATCH),
95 Some(format!(
96 "configured database is not a Lash process registry: expected schema version {expected_version} with table `processes`, found version {version}"
97 )),
98 ));
99 }
100 Ok(())
101 })
102 .await?;
103 true
104 } else {
105 false
106 };
107 Ok(Self {
108 conn,
109 clock,
110 artifact_cache: Mutex::new(BTreeMap::new()),
111 options,
112 commit_count: AtomicU64::new(0),
113 process_registry_attached,
114 #[cfg(test)]
115 checkpoint_probe_count: AtomicUsize::new(0),
116 #[cfg(test)]
117 checkpoint_write_transaction_count: AtomicUsize::new(0),
118 })
119 }
120
121 pub async fn open_readonly(path: &Path) -> tokio_rusqlite::Result<Self> {
124 let conn = SqliteConnection::open_readonly(path).await?;
125 Ok(Self {
126 conn,
127 clock: Arc::new(lash_core::SystemClock),
128 artifact_cache: Mutex::new(BTreeMap::new()),
129 options: StoreOptions::default(),
130 commit_count: AtomicU64::new(0),
131 process_registry_attached: false,
132 #[cfg(test)]
133 checkpoint_probe_count: AtomicUsize::new(0),
134 #[cfg(test)]
135 checkpoint_write_transaction_count: AtomicUsize::new(0),
136 })
137 }
138
139 pub async fn load_picker_info(&self) -> Option<SessionPickerInfo> {
140 self.conn
141 .call(|conn| {
142 let meta = conn
143 .query_row(
144 "SELECT session_id, cwd, relation_json
145 FROM session_meta WHERE singleton = 1",
146 [],
147 |row| {
148 let relation_json: Option<String> = row.get(2)?;
149 let relation = relation_json
150 .and_then(|json| serde_json::from_str(&json).ok())
151 .unwrap_or_default();
152 Ok((
153 row.get::<_, String>(0)?,
154 row.get::<_, Option<String>>(1)?,
155 relation,
156 ))
157 },
158 )
159 .optional()?;
160 let Some((session_id, cwd, relation)) = meta else {
161 return Ok(None);
162 };
163
164 let head_json: String = conn
165 .query_row(
166 "SELECT head_json FROM session_head WHERE singleton = 1",
167 [],
168 |row| row.get(0),
169 )
170 .optional()?
171 .unwrap_or_else(|| "{}".to_string());
172 let head_meta =
173 serde_json::from_str::<SessionHeadMeta>(&head_json).unwrap_or_default();
174 let graph = Self::load_session_graph_from_conn(conn, head_meta.leaf_node_id);
175
176 Ok(Some(SessionPickerInfo {
177 session_id,
178 cwd,
179 relation,
180 first_user_message: graph.first_user_message(),
181 user_message_count: graph.user_message_count(),
182 }))
183 })
184 .await
185 .ok()
186 .flatten()
187 }
188
189 pub async fn memory() -> tokio_rusqlite::Result<Self> {
190 Self::memory_with_options(StoreOptions {
191 blob_profile: BuiltinBlobProfile::LowLatency,
192 gc_policy: StoreGcPolicy::default(),
193 })
194 .await
195 }
196
197 pub async fn memory_with_clock(
198 clock: Arc<dyn lash_core::Clock>,
199 ) -> tokio_rusqlite::Result<Self> {
200 Self::memory_with_options_and_clock(
201 StoreOptions {
202 blob_profile: BuiltinBlobProfile::LowLatency,
203 gc_policy: StoreGcPolicy::default(),
204 },
205 clock,
206 )
207 .await
208 }
209
210 pub async fn memory_with_options(options: StoreOptions) -> tokio_rusqlite::Result<Self> {
211 Self::memory_with_options_and_clock(options, Arc::new(lash_core::SystemClock)).await
212 }
213
214 pub async fn memory_with_options_and_clock(
215 options: StoreOptions,
216 clock: Arc<dyn lash_core::Clock>,
217 ) -> tokio_rusqlite::Result<Self> {
218 let conn = SqliteConnection::open_in_memory().await?;
219 ensure_schema(&conn).await?;
220 apply_pragmas(&conn, StoreBacking::Memory).await?;
221 Ok(Self {
222 conn,
223 clock,
224 artifact_cache: Mutex::new(BTreeMap::new()),
225 options,
226 commit_count: AtomicU64::new(0),
227 process_registry_attached: false,
228 #[cfg(test)]
229 checkpoint_probe_count: AtomicUsize::new(0),
230 #[cfg(test)]
231 checkpoint_write_transaction_count: AtomicUsize::new(0),
232 })
233 }
234
235 #[cfg(test)]
236 pub(crate) fn checkpoint_claim_counts(&self) -> (usize, usize) {
237 (
238 self.checkpoint_probe_count
239 .load(std::sync::atomic::Ordering::Relaxed),
240 self.checkpoint_write_transaction_count
241 .load(std::sync::atomic::Ordering::Relaxed),
242 )
243 }
244
245 pub async fn save_session_head_meta(&self, meta: SessionHeadMeta) {
246 let head_json = encode_json(&meta);
247 let session_id = meta.session_id.clone();
248 let head_revision = meta.head_revision as i64;
249 let result = self
250 .conn
251 .call(move |conn| {
252 conn.execute(
253 "INSERT OR REPLACE INTO session_head (singleton, session_id, head_json, head_revision)
254 VALUES (1, ?1, ?2, ?3)",
255 params![session_id, head_json, head_revision],
256 )
257 })
258 .await;
259 if let Err(err) = result {
260 tracing::warn!(error = %err, "failed to persist session head");
261 }
262 }
263
264 pub async fn load_session_head_meta(&self) -> Option<SessionHeadMeta> {
265 self.conn
266 .call(|conn| Ok(load_session_head_meta_from_conn(conn)))
267 .await
268 .ok()
269 .flatten()
270 }
271
272 pub async fn save_session_head(&self, head: SessionHead) {
273 self.replace_session_graph(&head.graph).await;
274 self.save_session_head_meta(session_head_meta(&head)).await;
275 }
276
277 pub async fn load_session_head(&self) -> Option<SessionHead> {
278 let meta = self.load_session_head_meta().await?;
279 let mut graph = self.load_session_graph().await;
280 graph.set_leaf_node_id(meta.leaf_node_id.clone());
281 Some(SessionHead {
282 session_id: meta.session_id,
283 head_revision: meta.head_revision,
284 agent_frames: meta.agent_frames,
285 current_agent_frame_id: meta.current_agent_frame_id,
286 graph,
287 config: meta.config,
288 checkpoint_ref: meta.checkpoint_ref,
289 token_ledger: merge_token_ledger_entries(self.load_usage_deltas().await),
290 })
291 }
292
293 pub async fn head_copy_from_store(&self, source: &Store) {
294 if let Some(head) = source.load_session_head().await {
295 if let Some(checkpoint_ref) = &head.checkpoint_ref
296 && let Some(record) = source
297 .get_typed_blob::<SessionCheckpoint>(checkpoint_ref)
298 .await
299 {
300 for blob_ref in [
301 record.tool_state_ref.as_ref(),
302 record.plugin_snapshot_ref.as_ref(),
303 ]
304 .into_iter()
305 .flatten()
306 {
307 if let Some(blob) = source.get_blob(blob_ref).await {
308 let descriptor = match record
309 .tool_state_ref
310 .as_ref()
311 .filter(|candidate| *candidate == blob_ref)
312 {
313 Some(_) => BlobArtifactDescriptor::tool_state_snapshot(),
314 None => BlobArtifactDescriptor::plugin_session_snapshot(),
315 };
316 let _ = self.put_artifact_blob(descriptor, &blob).await;
317 }
318 }
319 if let Some(blob) = source.get_blob(checkpoint_ref).await {
320 let _ = self
321 .put_artifact_blob(BlobArtifactDescriptor::checkpoint_manifest(), &blob)
322 .await;
323 }
324 }
325 self.replace_session_graph(&head.graph).await;
326 self.save_session_head_meta(session_head_meta(&head)).await;
327 }
328 }
329
330 pub async fn save_session_meta(&self, meta: SessionMeta) {
331 let relation_json = serde_json::to_string(&meta.relation).ok();
332 let session_id_for_log = meta.session_id.clone();
333 let result = self
334 .conn
335 .call(move |conn| {
336 conn.execute(
337 "INSERT OR REPLACE INTO session_meta
338 (singleton, session_id, session_name, created_at, model, cwd, relation_json)
339 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6)",
340 params![
341 meta.session_id,
342 meta.session_name,
343 meta.created_at,
344 meta.model,
345 meta.cwd,
346 relation_json
347 ],
348 )
349 })
350 .await;
351 if let Err(err) = result {
352 tracing::warn!(
353 error = %err,
354 session_id = session_id_for_log,
355 "failed to persist session metadata"
356 );
357 }
358 }
359
360 pub async fn load_session_meta(&self) -> Option<SessionMeta> {
361 self.conn
362 .call(|conn| Ok(load_session_meta_from_conn(conn)))
363 .await
364 .ok()
365 .flatten()
366 }
367}