1use async_trait::async_trait;
4use chrono::Utc;
5use rusqlite::{OptionalExtension, TransactionBehavior, params};
6use starweaver_context::ResumableState;
7use starweaver_core::{CheckpointId, RunId, SessionId};
8use starweaver_runtime::{AgentCheckpoint, AgentStreamRecord};
9use starweaver_session::{
10 ApprovalRecord, CheckpointRef, CompactRunTrace, CompactSessionTrace, DeferredToolRecord,
11 EnvironmentStateRef, RunRecord, RunStatus, SessionFilter, SessionRecord, SessionStatus,
12 SessionStore, SessionStoreError, SessionStoreResult, StreamCursorRef,
13};
14
15use super::{
16 LocalStore,
17 db::{
18 insert_approval_records_tx, insert_deferred_tool_records_tx, insert_raw_stream_records_tx,
19 insert_stream_cursor_tx, load_session_tx, next_sequence_tx, upsert_run_tx,
20 upsert_session_tx,
21 },
22};
23use crate::{CliError, config::CliConfig};
24
25#[derive(Clone, Debug)]
27pub struct LocalSessionStore {
28 config: CliConfig,
29}
30
31impl LocalSessionStore {
32 #[must_use]
34 pub const fn new(config: CliConfig) -> Self {
35 Self { config }
36 }
37
38 fn open_store(&self) -> SessionStoreResult<LocalStore> {
39 LocalStore::open(&self.config).map_err(session_failed_cli)
40 }
41}
42
43#[async_trait]
44impl SessionStore for LocalSessionStore {
45 async fn save_session(&self, mut session: SessionRecord) -> SessionStoreResult<()> {
46 session.updated_at = Utc::now();
47 let mut store = self.open_store()?;
48 let tx = store
49 .conn
50 .transaction_with_behavior(TransactionBehavior::Immediate)
51 .map_err(session_failed)?;
52 upsert_session_tx(&tx, &session).map_err(session_failed)?;
53 tx.commit().map_err(session_failed)
54 }
55
56 async fn load_session(&self, session_id: &SessionId) -> SessionStoreResult<SessionRecord> {
57 self.open_store()?
58 .load_session(session_id.as_str())
59 .map_err(session_failed_cli)
60 }
61
62 async fn list_sessions(&self, filter: SessionFilter) -> SessionStoreResult<Vec<SessionRecord>> {
63 let store = self.open_store()?;
64 let mut stmt = store
65 .conn
66 .prepare("SELECT record_json FROM sessions ORDER BY updated_at DESC")
67 .map_err(session_failed)?;
68 let rows = stmt
69 .query_map([], |row| row.get::<_, String>(0))
70 .map_err(session_failed)?;
71 let mut sessions = Vec::new();
72 for row in rows {
73 let session: SessionRecord =
74 serde_json::from_str(&row.map_err(session_failed)?).map_err(session_failed)?;
75 if filter.status.is_some_and(|status| session.status != status) {
76 continue;
77 }
78 if filter
79 .profile
80 .as_ref()
81 .is_some_and(|profile| session.profile.as_ref() != Some(profile))
82 {
83 continue;
84 }
85 if filter
86 .workspace
87 .as_ref()
88 .is_some_and(|workspace| session.workspace.as_ref() != Some(workspace))
89 {
90 continue;
91 }
92 sessions.push(session);
93 if filter.limit.is_some_and(|limit| sessions.len() >= limit) {
94 break;
95 }
96 }
97 Ok(sessions)
98 }
99
100 async fn update_session_status(
101 &self,
102 session_id: &SessionId,
103 status: SessionStatus,
104 ) -> SessionStoreResult<()> {
105 let mut session = self.load_session(session_id).await?;
106 session.status = status;
107 self.save_session(session).await
108 }
109
110 async fn save_context_state(
111 &self,
112 session_id: &SessionId,
113 state: ResumableState,
114 ) -> SessionStoreResult<()> {
115 let mut session = self.load_session(session_id).await?;
116 session.state = state;
117 self.save_session(session).await
118 }
119
120 async fn save_environment_state(
121 &self,
122 session_id: &SessionId,
123 environment_state: EnvironmentStateRef,
124 ) -> SessionStoreResult<()> {
125 let mut session = self.load_session(session_id).await?;
126 session.environment_state = Some(environment_state);
127 self.save_session(session).await
128 }
129
130 async fn append_run(&self, mut run: RunRecord) -> SessionStoreResult<()> {
131 let mut store = self.open_store()?;
132 let tx = store
133 .conn
134 .transaction_with_behavior(TransactionBehavior::Immediate)
135 .map_err(session_failed)?;
136 let mut session = load_session_tx(&tx, run.session_id.as_str()).map_err(session_failed)?;
137 run.updated_at = Utc::now();
138 if let Some(existing_sequence) =
139 existing_run_sequence(&tx, run.session_id.as_str(), run.run_id.as_str())?
140 {
141 run.sequence_no = existing_sequence;
142 } else if run.sequence_no == 0
143 || sequence_exists(&tx, run.session_id.as_str(), run.sequence_no)?
144 {
145 run.sequence_no =
146 next_sequence_tx(&tx, run.session_id.as_str()).map_err(session_failed)?;
147 }
148 apply_run_to_session(&mut session, &run);
149 upsert_run_tx(&tx, &run).map_err(session_failed)?;
150 upsert_session_tx(&tx, &session).map_err(session_failed)?;
151 tx.commit().map_err(session_failed)
152 }
153
154 async fn load_run(
155 &self,
156 session_id: &SessionId,
157 run_id: &RunId,
158 ) -> SessionStoreResult<RunRecord> {
159 self.open_store()?
160 .load_run(session_id.as_str(), run_id.as_str())
161 .map_err(session_failed_cli)
162 }
163
164 async fn list_runs(&self, session_id: &SessionId) -> SessionStoreResult<Vec<RunRecord>> {
165 let store = self.open_store()?;
166 let mut stmt = store
167 .conn
168 .prepare("SELECT record_json FROM runs WHERE session_id = ?1 ORDER BY sequence_no ASC")
169 .map_err(session_failed)?;
170 let rows = stmt
171 .query_map(params![session_id.as_str()], |row| row.get::<_, String>(0))
172 .map_err(session_failed)?;
173 let runs = collect_json_records(rows)?;
174 Ok(runs)
175 }
176
177 async fn update_run_status(
178 &self,
179 session_id: &SessionId,
180 run_id: &RunId,
181 status: RunStatus,
182 output_preview: Option<String>,
183 ) -> SessionStoreResult<()> {
184 let mut run = self.load_run(session_id, run_id).await?;
185 run.status = status;
186 run.output_preview = output_preview;
187 run.updated_at = Utc::now();
188 self.append_run(run).await
189 }
190
191 async fn append_checkpoint(
192 &self,
193 session_id: &SessionId,
194 checkpoint: AgentCheckpoint,
195 ) -> SessionStoreResult<()> {
196 let mut store = self.open_store()?;
197 let tx = store
198 .conn
199 .transaction_with_behavior(TransactionBehavior::Immediate)
200 .map_err(session_failed)?;
201 let checkpoint_id = checkpoint.checkpoint_id.clone();
202 let checkpoint_run_id = checkpoint.run_id.clone();
203 let checkpoint_node = checkpoint.node;
204 let checkpoint_node_label = format!("{checkpoint_node:?}");
205 let checkpoint_sequence = checkpoint.run_step;
206 let stream_cursor = checkpoint.resume.cursor.stream_cursor;
207 let checkpoint_metadata = checkpoint.metadata.clone();
208 let mut run = load_run_tx(&tx, session_id.as_str(), checkpoint_run_id.as_str())?;
209 tx.execute(
210 "INSERT OR REPLACE INTO checkpoints
211 (checkpoint_id, session_id, run_id, sequence_no, node, checkpoint_json, created_at)
212 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
213 params![
214 checkpoint_id.as_str(),
215 session_id.as_str(),
216 checkpoint_run_id.as_str(),
217 i64::try_from(checkpoint_sequence).map_err(session_failed)?,
218 checkpoint_node_label,
219 serde_json::to_string(&checkpoint).map_err(session_failed)?,
220 Utc::now().to_rfc3339(),
221 ],
222 )
223 .map_err(session_failed)?;
224 run.latest_checkpoint = Some(CheckpointRef {
225 checkpoint_id,
226 run_id: checkpoint_run_id,
227 sequence: checkpoint_sequence,
228 node: format!("{checkpoint_node:?}"),
229 storage_ref: None,
230 stream_cursor,
231 created_at: Utc::now(),
232 metadata: checkpoint_metadata,
233 });
234 run.updated_at = Utc::now();
235 upsert_run_tx(&tx, &run).map_err(session_failed)?;
236 tx.commit().map_err(session_failed)
237 }
238
239 async fn load_checkpoints(
240 &self,
241 session_id: &SessionId,
242 run_id: &RunId,
243 ) -> SessionStoreResult<Vec<AgentCheckpoint>> {
244 let store = self.open_store()?;
245 let mut stmt = store
246 .conn
247 .prepare(
248 "SELECT checkpoint_json FROM checkpoints
249 WHERE session_id = ?1 AND run_id = ?2
250 ORDER BY sequence_no ASC, checkpoint_id ASC",
251 )
252 .map_err(session_failed)?;
253 let rows = stmt
254 .query_map(params![session_id.as_str(), run_id.as_str()], |row| {
255 row.get::<_, String>(0)
256 })
257 .map_err(session_failed)?;
258 let mut checkpoints = Vec::new();
259 for row in rows {
260 let json = row.map_err(session_failed)?;
261 if let Ok(checkpoint) = serde_json::from_str::<AgentCheckpoint>(&json) {
262 checkpoints.push(checkpoint);
263 }
264 }
265 Ok(checkpoints)
266 }
267
268 async fn append_stream_records(
269 &self,
270 session_id: &SessionId,
271 run_id: &RunId,
272 records: Vec<AgentStreamRecord>,
273 ) -> SessionStoreResult<()> {
274 let mut store = self.open_store()?;
275 let tx = store
276 .conn
277 .transaction_with_behavior(TransactionBehavior::Immediate)
278 .map_err(session_failed)?;
279 let mut run = load_run_tx(&tx, session_id.as_str(), run_id.as_str())?;
280 insert_raw_stream_records_tx(&tx, &run, &records).map_err(session_failed)?;
281 if let Some(sequence) = latest_raw_sequence(&tx, session_id.as_str(), run_id.as_str())? {
282 let cursor =
283 StreamCursorRef::new("raw_runtime", format!("run:{}", run_id.as_str()), sequence);
284 run.stream_cursors
285 .retain(|existing| existing.family != cursor.family);
286 run.stream_cursors.push(cursor.clone());
287 run.updated_at = Utc::now();
288 upsert_run_tx(&tx, &run).map_err(session_failed)?;
289 let mut session = load_session_tx(&tx, session_id.as_str()).map_err(session_failed)?;
290 upsert_session_cursor(&mut session, cursor);
291 upsert_session_tx(&tx, &session).map_err(session_failed)?;
292 }
293 tx.commit().map_err(session_failed)
294 }
295
296 async fn replay_stream_records(
297 &self,
298 session_id: &SessionId,
299 run_id: &RunId,
300 ) -> SessionStoreResult<Vec<AgentStreamRecord>> {
301 self.replay_stream_records_after(session_id, run_id, None)
302 .await
303 }
304
305 async fn replay_stream_records_after(
306 &self,
307 session_id: &SessionId,
308 run_id: &RunId,
309 after_sequence: Option<usize>,
310 ) -> SessionStoreResult<Vec<AgentStreamRecord>> {
311 let after = after_sequence.map_or(-1_i64, |value| i64::try_from(value).unwrap_or(i64::MAX));
312 let store = self.open_store()?;
313 let mut stmt = store
314 .conn
315 .prepare(
316 "SELECT record_json FROM raw_stream_records
317 WHERE session_id = ?1 AND run_id = ?2 AND sequence_no > ?3
318 ORDER BY sequence_no ASC",
319 )
320 .map_err(session_failed)?;
321 let rows = stmt
322 .query_map(
323 params![session_id.as_str(), run_id.as_str(), after],
324 |row| row.get::<_, String>(0),
325 )
326 .map_err(session_failed)?;
327 let records = collect_json_records(rows)?;
328 Ok(records)
329 }
330
331 async fn save_stream_cursor(
332 &self,
333 session_id: &SessionId,
334 run_id: &RunId,
335 cursor: StreamCursorRef,
336 ) -> SessionStoreResult<()> {
337 let mut store = self.open_store()?;
338 let tx = store
339 .conn
340 .transaction_with_behavior(TransactionBehavior::Immediate)
341 .map_err(session_failed)?;
342 let mut run = load_run_tx(&tx, session_id.as_str(), run_id.as_str())?;
343 run.stream_cursors
344 .retain(|existing| existing.family != cursor.family || existing.scope != cursor.scope);
345 run.stream_cursors.push(cursor.clone());
346 run.updated_at = Utc::now();
347 upsert_run_tx(&tx, &run).map_err(session_failed)?;
348 let mut session = load_session_tx(&tx, session_id.as_str()).map_err(session_failed)?;
349 upsert_session_cursor(&mut session, cursor.clone());
350 upsert_session_tx(&tx, &session).map_err(session_failed)?;
351 insert_stream_cursor_tx(&tx, &run, &cursor).map_err(session_failed)?;
352 tx.commit().map_err(session_failed)
353 }
354
355 async fn append_approval(&self, approval: ApprovalRecord) -> SessionStoreResult<()> {
356 let mut store = self.open_store()?;
357 let tx = store
358 .conn
359 .transaction_with_behavior(TransactionBehavior::Immediate)
360 .map_err(session_failed)?;
361 insert_approval_records_tx(&tx, &[approval]).map_err(session_failed)?;
362 tx.commit().map_err(session_failed)
363 }
364
365 async fn load_approvals(
366 &self,
367 session_id: &SessionId,
368 run_id: &RunId,
369 ) -> SessionStoreResult<Vec<ApprovalRecord>> {
370 self.open_store()?
371 .list_approvals(Some(session_id.as_str()), Some(run_id.as_str()))
372 .map_err(session_failed_cli)
373 }
374
375 async fn append_deferred_tool(&self, record: DeferredToolRecord) -> SessionStoreResult<()> {
376 let mut store = self.open_store()?;
377 let tx = store
378 .conn
379 .transaction_with_behavior(TransactionBehavior::Immediate)
380 .map_err(session_failed)?;
381 insert_deferred_tool_records_tx(&tx, &[record]).map_err(session_failed)?;
382 tx.commit().map_err(session_failed)
383 }
384
385 async fn load_deferred_tools(
386 &self,
387 session_id: &SessionId,
388 run_id: &RunId,
389 ) -> SessionStoreResult<Vec<DeferredToolRecord>> {
390 self.open_store()?
391 .list_deferred_tools(Some(session_id.as_str()), Some(run_id.as_str()))
392 .map_err(session_failed_cli)
393 }
394
395 async fn compact_run_trace(
396 &self,
397 session_id: &SessionId,
398 run_id: &RunId,
399 ) -> SessionStoreResult<CompactRunTrace> {
400 let store = self.open_store()?;
401 let run = store
402 .load_run(session_id.as_str(), run_id.as_str())
403 .map_err(session_failed_cli)?;
404 Ok(CompactRunTrace {
405 session_id: Some(session_id.clone()),
406 run_id: Some(run_id.clone()),
407 status: run.status,
408 parent_run_id: run.parent_run_id.clone(),
409 parent_task_id: run.parent_task_id.clone(),
410 checkpoints: checkpoint_ids(&store, session_id.as_str(), run_id.as_str())?,
411 approvals: pending_approval_count(&store, session_id.as_str(), run_id.as_str())?,
412 deferred_tools: pending_deferred_count(&store, session_id.as_str(), run_id.as_str())?,
413 latest_checkpoint: run
414 .latest_checkpoint
415 .as_ref()
416 .map(|checkpoint| checkpoint.checkpoint_id.clone()),
417 stream_cursor: latest_raw_sequence_ref(&store, session_id.as_str(), run_id.as_str())?,
418 stream_cursors: run.stream_cursors,
419 output_preview: run.output_preview,
420 trace_context: run.trace_context,
421 updated_at: Some(run.updated_at),
422 metadata: run.metadata,
423 })
424 }
425
426 async fn compact_session_trace(
427 &self,
428 session_id: &SessionId,
429 ) -> SessionStoreResult<CompactSessionTrace> {
430 let session = self.load_session(session_id).await?;
431 let runs = self.list_runs(session_id).await?;
432 let latest_run = runs.last();
433 Ok(CompactSessionTrace {
434 session_id: session.session_id,
435 title: session.title,
436 workspace: session.workspace,
437 profile: session.profile,
438 status: session.status,
439 runs: runs.len(),
440 latest_run_id: latest_run.map(|run| run.run_id.clone()),
441 last_output_preview: latest_run.and_then(|run| run.output_preview.clone()),
442 stream_cursors: session.stream_cursors,
443 trace_context: session.trace_context,
444 created_at: session.created_at,
445 updated_at: session.updated_at,
446 metadata: session.metadata,
447 })
448 }
449}
450
451fn load_run_tx(
452 tx: &rusqlite::Transaction<'_>,
453 session_id: &str,
454 run_id: &str,
455) -> SessionStoreResult<RunRecord> {
456 tx.query_row(
457 "SELECT record_json FROM runs WHERE session_id = ?1 AND run_id = ?2",
458 params![session_id, run_id],
459 |row| row.get::<_, String>(0),
460 )
461 .optional()
462 .map_err(session_failed)?
463 .map(|json| serde_json::from_str(&json).map_err(session_failed))
464 .transpose()?
465 .ok_or_else(|| SessionStoreError::NotFound(format!("{session_id}:{run_id}")))
466}
467
468fn existing_run_sequence(
469 tx: &rusqlite::Transaction<'_>,
470 session_id: &str,
471 run_id: &str,
472) -> SessionStoreResult<Option<usize>> {
473 tx.query_row(
474 "SELECT sequence_no FROM runs WHERE session_id = ?1 AND run_id = ?2",
475 params![session_id, run_id],
476 |row| row.get::<_, i64>(0),
477 )
478 .optional()
479 .map_err(session_failed)?
480 .map(|value| usize::try_from(value).map_err(session_failed))
481 .transpose()
482}
483
484fn sequence_exists(
485 tx: &rusqlite::Transaction<'_>,
486 session_id: &str,
487 sequence_no: usize,
488) -> SessionStoreResult<bool> {
489 let count = tx
490 .query_row(
491 "SELECT COUNT(*) FROM runs WHERE session_id = ?1 AND sequence_no = ?2",
492 params![
493 session_id,
494 i64::try_from(sequence_no).map_err(session_failed)?
495 ],
496 |row| row.get::<_, i64>(0),
497 )
498 .map_err(session_failed)?;
499 Ok(count > 0)
500}
501
502fn apply_run_to_session(session: &mut SessionRecord, run: &RunRecord) {
503 session.profile.clone_from(&run.profile);
504 session.head_run_id = Some(run.run_id.clone());
505 if run.status == RunStatus::Completed {
506 session.head_success_run_id = Some(run.run_id.clone());
507 }
508 if matches!(
509 run.status,
510 RunStatus::Queued | RunStatus::Running | RunStatus::Waiting
511 ) {
512 session.active_run_id = Some(run.run_id.clone());
513 } else if session.active_run_id.as_ref() == Some(&run.run_id) {
514 session.active_run_id = None;
515 }
516 session.updated_at = run.updated_at;
517}
518
519fn upsert_session_cursor(session: &mut SessionRecord, cursor: StreamCursorRef) {
520 session
521 .stream_cursors
522 .retain(|existing| existing.family != cursor.family || existing.scope != cursor.scope);
523 session.stream_cursors.push(cursor);
524 session.updated_at = Utc::now();
525}
526
527fn latest_raw_sequence(
528 tx: &rusqlite::Transaction<'_>,
529 session_id: &str,
530 run_id: &str,
531) -> SessionStoreResult<Option<usize>> {
532 tx.query_row(
533 "SELECT MAX(sequence_no) FROM raw_stream_records WHERE session_id = ?1 AND run_id = ?2",
534 params![session_id, run_id],
535 |row| row.get::<_, Option<i64>>(0),
536 )
537 .map_err(session_failed)?
538 .map(|value| usize::try_from(value).map_err(session_failed))
539 .transpose()
540}
541
542fn latest_raw_sequence_ref(
543 store: &LocalStore,
544 session_id: &str,
545 run_id: &str,
546) -> SessionStoreResult<Option<usize>> {
547 store
548 .conn
549 .query_row(
550 "SELECT MAX(sequence_no) FROM raw_stream_records WHERE session_id = ?1 AND run_id = ?2",
551 params![session_id, run_id],
552 |row| row.get::<_, Option<i64>>(0),
553 )
554 .map_err(session_failed)?
555 .map(|value| usize::try_from(value).map_err(session_failed))
556 .transpose()
557}
558
559fn checkpoint_ids(
560 store: &LocalStore,
561 session_id: &str,
562 run_id: &str,
563) -> SessionStoreResult<Vec<CheckpointId>> {
564 let mut stmt = store
565 .conn
566 .prepare(
567 "SELECT checkpoint_id FROM checkpoints
568 WHERE session_id = ?1 AND run_id = ?2
569 ORDER BY sequence_no ASC, checkpoint_id ASC",
570 )
571 .map_err(session_failed)?;
572 let rows = stmt
573 .query_map(params![session_id, run_id], |row| row.get::<_, String>(0))
574 .map_err(session_failed)?;
575 rows.collect::<Result<Vec<_>, _>>()
576 .map_err(session_failed)
577 .map(|ids| ids.into_iter().map(CheckpointId::from_string).collect())
578}
579
580fn pending_approval_count(
581 store: &LocalStore,
582 session_id: &str,
583 run_id: &str,
584) -> SessionStoreResult<usize> {
585 count_rows(
586 store,
587 "SELECT COUNT(*) FROM approvals WHERE session_id = ?1 AND run_id = ?2 AND status = 'pending'",
588 session_id,
589 run_id,
590 )
591}
592
593fn pending_deferred_count(
594 store: &LocalStore,
595 session_id: &str,
596 run_id: &str,
597) -> SessionStoreResult<usize> {
598 count_rows(
599 store,
600 "SELECT COUNT(*) FROM deferred_tools
601 WHERE session_id = ?1 AND run_id = ?2
602 AND status IN ('pending', 'running', 'waiting')",
603 session_id,
604 run_id,
605 )
606}
607
608fn count_rows(
609 store: &LocalStore,
610 sql: &str,
611 session_id: &str,
612 run_id: &str,
613) -> SessionStoreResult<usize> {
614 let count = store
615 .conn
616 .query_row(sql, params![session_id, run_id], |row| row.get::<_, i64>(0))
617 .map_err(session_failed)?;
618 usize::try_from(count).map_err(session_failed)
619}
620
621fn collect_json_records<T>(
622 rows: rusqlite::MappedRows<'_, impl FnMut(&rusqlite::Row<'_>) -> rusqlite::Result<String>>,
623) -> SessionStoreResult<Vec<T>>
624where
625 T: serde::de::DeserializeOwned,
626{
627 rows.collect::<Result<Vec<_>, _>>()
628 .map_err(session_failed)?
629 .into_iter()
630 .map(|json| serde_json::from_str(&json).map_err(session_failed))
631 .collect()
632}
633
634fn session_failed(error: impl std::fmt::Display) -> SessionStoreError {
635 SessionStoreError::Failed(error.to_string())
636}
637
638fn session_failed_cli(error: CliError) -> SessionStoreError {
639 match error {
640 CliError::NotFound(id) => SessionStoreError::NotFound(id),
641 error => SessionStoreError::Failed(error.to_string()),
642 }
643}