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 let conn = SqliteConnection::open(path).await?;
42 ensure_schema(&conn).await?;
43 apply_pragmas(&conn, StoreBacking::File).await?;
44 Ok(Self {
45 conn,
46 clock,
47 artifact_cache: Mutex::new(BTreeMap::new()),
48 options,
49 commit_count: AtomicU64::new(0),
50 #[cfg(test)]
51 checkpoint_probe_count: AtomicUsize::new(0),
52 #[cfg(test)]
53 checkpoint_write_transaction_count: AtomicUsize::new(0),
54 })
55 }
56
57 pub async fn open_readonly(path: &Path) -> tokio_rusqlite::Result<Self> {
60 let conn = SqliteConnection::open_readonly(path).await?;
61 Ok(Self {
62 conn,
63 clock: Arc::new(lash_core::SystemClock),
64 artifact_cache: Mutex::new(BTreeMap::new()),
65 options: StoreOptions::default(),
66 commit_count: AtomicU64::new(0),
67 #[cfg(test)]
68 checkpoint_probe_count: AtomicUsize::new(0),
69 #[cfg(test)]
70 checkpoint_write_transaction_count: AtomicUsize::new(0),
71 })
72 }
73
74 pub async fn load_picker_info(&self) -> Option<SessionPickerInfo> {
75 self.conn
76 .call(|conn| {
77 let meta = conn
78 .query_row(
79 "SELECT session_id, cwd, relation_json
80 FROM session_meta WHERE singleton = 1",
81 [],
82 |row| {
83 let relation_json: Option<String> = row.get(2)?;
84 let relation = relation_json
85 .and_then(|json| serde_json::from_str(&json).ok())
86 .unwrap_or_default();
87 Ok((
88 row.get::<_, String>(0)?,
89 row.get::<_, Option<String>>(1)?,
90 relation,
91 ))
92 },
93 )
94 .optional()?;
95 let Some((session_id, cwd, relation)) = meta else {
96 return Ok(None);
97 };
98
99 let head_json: String = conn
100 .query_row(
101 "SELECT head_json FROM session_head WHERE singleton = 1",
102 [],
103 |row| row.get(0),
104 )
105 .optional()?
106 .unwrap_or_else(|| "{}".to_string());
107 let head_meta =
108 serde_json::from_str::<SessionHeadMeta>(&head_json).unwrap_or_default();
109 let graph = Self::load_session_graph_from_conn(conn, head_meta.leaf_node_id);
110
111 Ok(Some(SessionPickerInfo {
112 session_id,
113 cwd,
114 relation,
115 first_user_message: graph.first_user_message(),
116 user_message_count: graph.user_message_count(),
117 }))
118 })
119 .await
120 .ok()
121 .flatten()
122 }
123
124 pub async fn memory() -> tokio_rusqlite::Result<Self> {
125 Self::memory_with_options(StoreOptions {
126 blob_profile: BuiltinBlobProfile::LowLatency,
127 gc_policy: StoreGcPolicy::default(),
128 })
129 .await
130 }
131
132 pub async fn memory_with_clock(
133 clock: Arc<dyn lash_core::Clock>,
134 ) -> tokio_rusqlite::Result<Self> {
135 Self::memory_with_options_and_clock(
136 StoreOptions {
137 blob_profile: BuiltinBlobProfile::LowLatency,
138 gc_policy: StoreGcPolicy::default(),
139 },
140 clock,
141 )
142 .await
143 }
144
145 pub async fn memory_with_options(options: StoreOptions) -> tokio_rusqlite::Result<Self> {
146 Self::memory_with_options_and_clock(options, Arc::new(lash_core::SystemClock)).await
147 }
148
149 pub async fn memory_with_options_and_clock(
150 options: StoreOptions,
151 clock: Arc<dyn lash_core::Clock>,
152 ) -> tokio_rusqlite::Result<Self> {
153 let conn = SqliteConnection::open_in_memory().await?;
154 ensure_schema(&conn).await?;
155 apply_pragmas(&conn, StoreBacking::Memory).await?;
156 Ok(Self {
157 conn,
158 clock,
159 artifact_cache: Mutex::new(BTreeMap::new()),
160 options,
161 commit_count: AtomicU64::new(0),
162 #[cfg(test)]
163 checkpoint_probe_count: AtomicUsize::new(0),
164 #[cfg(test)]
165 checkpoint_write_transaction_count: AtomicUsize::new(0),
166 })
167 }
168
169 #[cfg(test)]
170 pub(crate) fn checkpoint_claim_counts(&self) -> (usize, usize) {
171 (
172 self.checkpoint_probe_count
173 .load(std::sync::atomic::Ordering::Relaxed),
174 self.checkpoint_write_transaction_count
175 .load(std::sync::atomic::Ordering::Relaxed),
176 )
177 }
178
179 pub async fn save_session_head_meta(&self, meta: SessionHeadMeta) {
180 let head_json = encode_json(&meta);
181 let session_id = meta.session_id.clone();
182 let head_revision = meta.head_revision as i64;
183 let result = self
184 .conn
185 .call(move |conn| {
186 conn.execute(
187 "INSERT OR REPLACE INTO session_head (singleton, session_id, head_json, head_revision)
188 VALUES (1, ?1, ?2, ?3)",
189 params![session_id, head_json, head_revision],
190 )
191 })
192 .await;
193 if let Err(err) = result {
194 tracing::warn!(error = %err, "failed to persist session head");
195 }
196 }
197
198 pub async fn load_session_head_meta(&self) -> Option<SessionHeadMeta> {
199 self.conn
200 .call(|conn| Ok(load_session_head_meta_from_conn(conn)))
201 .await
202 .ok()
203 .flatten()
204 }
205
206 pub async fn save_session_head(&self, head: SessionHead) {
207 self.replace_session_graph(&head.graph).await;
208 self.save_session_head_meta(session_head_meta(&head)).await;
209 }
210
211 pub async fn load_session_head(&self) -> Option<SessionHead> {
212 let meta = self.load_session_head_meta().await?;
213 let mut graph = self.load_session_graph().await;
214 graph.set_leaf_node_id(meta.leaf_node_id.clone());
215 Some(SessionHead {
216 session_id: meta.session_id,
217 head_revision: meta.head_revision,
218 agent_frames: meta.agent_frames,
219 current_agent_frame_id: meta.current_agent_frame_id,
220 graph,
221 config: meta.config,
222 checkpoint_ref: meta.checkpoint_ref,
223 token_ledger: merge_token_ledger_entries(self.load_usage_deltas().await),
224 })
225 }
226
227 pub async fn head_copy_from_store(&self, source: &Store) {
228 if let Some(head) = source.load_session_head().await {
229 if let Some(checkpoint_ref) = &head.checkpoint_ref
230 && let Some(record) = source
231 .get_typed_blob::<SessionCheckpoint>(checkpoint_ref)
232 .await
233 {
234 for blob_ref in [
235 record.tool_state_ref.as_ref(),
236 record.plugin_snapshot_ref.as_ref(),
237 ]
238 .into_iter()
239 .flatten()
240 {
241 if let Some(blob) = source.get_blob(blob_ref).await {
242 let descriptor = match record
243 .tool_state_ref
244 .as_ref()
245 .filter(|candidate| *candidate == blob_ref)
246 {
247 Some(_) => BlobArtifactDescriptor::tool_state_snapshot(),
248 None => BlobArtifactDescriptor::plugin_session_snapshot(),
249 };
250 let _ = self.put_artifact_blob(descriptor, &blob).await;
251 }
252 }
253 if let Some(blob) = source.get_blob(checkpoint_ref).await {
254 let _ = self
255 .put_artifact_blob(BlobArtifactDescriptor::checkpoint_manifest(), &blob)
256 .await;
257 }
258 }
259 self.replace_session_graph(&head.graph).await;
260 self.save_session_head_meta(session_head_meta(&head)).await;
261 }
262 }
263
264 pub async fn save_session_meta(&self, meta: SessionMeta) {
265 let relation_json = serde_json::to_string(&meta.relation).ok();
266 let session_id_for_log = meta.session_id.clone();
267 let result = self
268 .conn
269 .call(move |conn| {
270 conn.execute(
271 "INSERT OR REPLACE INTO session_meta
272 (singleton, session_id, session_name, created_at, model, cwd, relation_json)
273 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6)",
274 params![
275 meta.session_id,
276 meta.session_name,
277 meta.created_at,
278 meta.model,
279 meta.cwd,
280 relation_json
281 ],
282 )
283 })
284 .await;
285 if let Err(err) = result {
286 tracing::warn!(
287 error = %err,
288 session_id = session_id_for_log,
289 "failed to persist session metadata"
290 );
291 }
292 }
293
294 pub async fn load_session_meta(&self) -> Option<SessionMeta> {
295 self.conn
296 .call(|conn| Ok(load_session_meta_from_conn(conn)))
297 .await
298 .ok()
299 .flatten()
300 }
301}