1use std::path::{Path, PathBuf};
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12
13use async_trait::async_trait;
14use rusqlite::{params, Connection, OptionalExtension};
15use uuid::Uuid;
16
17use super::event::{
18 now_ms_and_rfc3339, AppendEvent, EventId, EventSignature, SessionEventKind, StoredEvent,
19};
20use super::signing::{
21 chain_root_fold, chain_root_hash, chain_root_init, compute_record_hash, re_anchor_events,
22 verify_event_chain,
23};
24use super::store::{
25 CreateSession, EventPage, ForkResult, ListFilter, ReadRange, SessionId, SessionMeta,
26 SessionStatus, SessionStore, Snapshot, SnapshotId, StoreError, StoreHooks, StoreResult,
27 TruncateResult, VerifyFailure, VerifyReport, MAX_READ_BATCH,
28};
29
30const SCHEMA_VERSION: i64 = 1;
31const DEFAULT_BUSY_TIMEOUT: Duration = Duration::from_secs(5);
32
33#[derive(Clone)]
34pub struct SqliteSessionStore {
35 conn: Arc<Mutex<Connection>>,
36 hooks: Arc<StoreHooks>,
37 path: PathBuf,
38}
39
40impl SqliteSessionStore {
41 pub fn open(path: impl AsRef<Path>) -> StoreResult<Self> {
42 Self::open_with_hooks(path, StoreHooks::default())
43 }
44
45 pub fn open_in_memory() -> StoreResult<Self> {
46 let conn =
47 Connection::open_in_memory().map_err(|error| StoreError::Backend(error.to_string()))?;
48 Self::initialize(conn, PathBuf::from(":memory:"), StoreHooks::default())
49 }
50
51 pub fn open_with_hooks(path: impl AsRef<Path>, hooks: StoreHooks) -> StoreResult<Self> {
52 let path = path.as_ref().to_path_buf();
53 if let Some(parent) = path.parent() {
54 if !parent.as_os_str().is_empty() {
55 std::fs::create_dir_all(parent)
56 .map_err(|error| StoreError::Backend(error.to_string()))?;
57 }
58 }
59 let conn =
60 Connection::open(&path).map_err(|error| StoreError::Backend(error.to_string()))?;
61 Self::initialize(conn, path, hooks)
62 }
63
64 fn initialize(conn: Connection, path: PathBuf, hooks: StoreHooks) -> StoreResult<Self> {
65 conn.busy_timeout(DEFAULT_BUSY_TIMEOUT)
66 .map_err(|error| StoreError::Backend(error.to_string()))?;
67 conn.pragma_update(None, "journal_mode", "WAL")
68 .map_err(|error| StoreError::Backend(error.to_string()))?;
69 conn.pragma_update(None, "synchronous", "NORMAL")
70 .map_err(|error| StoreError::Backend(error.to_string()))?;
71 conn.execute_batch(
72 "
73 CREATE TABLE IF NOT EXISTS schema_version (
74 version INTEGER NOT NULL PRIMARY KEY
75 );
76 CREATE TABLE IF NOT EXISTS sessions (
77 id TEXT PRIMARY KEY,
78 tenant_id TEXT,
79 persona TEXT,
80 parent_session_id TEXT,
81 created_at_ms INTEGER NOT NULL,
82 created_at TEXT NOT NULL,
83 updated_at_ms INTEGER NOT NULL,
84 updated_at TEXT NOT NULL,
85 status TEXT NOT NULL,
86 event_count INTEGER NOT NULL DEFAULT 0,
87 last_event_id INTEGER,
88 chain_root_hash TEXT,
89 closed_at_ms INTEGER,
90 closed_at TEXT,
91 soft_deleted_at_ms INTEGER,
92 ttl_seconds INTEGER,
93 tags_json TEXT NOT NULL DEFAULT '[]',
94 attributes_json TEXT NOT NULL DEFAULT '{}',
95 next_event_id INTEGER NOT NULL DEFAULT 1
96 );
97 CREATE INDEX IF NOT EXISTS sessions_tenant_created
98 ON sessions(tenant_id, created_at_ms);
99 CREATE INDEX IF NOT EXISTS sessions_status
100 ON sessions(status);
101 CREATE TABLE IF NOT EXISTS session_events (
102 session_id TEXT NOT NULL,
103 event_id INTEGER NOT NULL,
104 tenant_id TEXT,
105 parent_event_id INTEGER,
106 actor TEXT,
107 kind TEXT NOT NULL,
108 custom_kind TEXT,
109 payload_json TEXT NOT NULL,
110 tags_json TEXT NOT NULL DEFAULT '[]',
111 headers_json TEXT NOT NULL DEFAULT '{}',
112 ts_ms INTEGER NOT NULL,
113 ts TEXT NOT NULL,
114 record_hash TEXT NOT NULL,
115 prev_hash TEXT,
116 signature_json TEXT,
117 PRIMARY KEY (session_id, event_id),
118 FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
119 );
120 CREATE INDEX IF NOT EXISTS session_events_ts
121 ON session_events(session_id, ts_ms);
122 CREATE TABLE IF NOT EXISTS session_tags (
123 session_id TEXT NOT NULL,
124 tag TEXT NOT NULL,
125 PRIMARY KEY (session_id, tag),
126 FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
127 );
128 CREATE INDEX IF NOT EXISTS session_tags_by_tag
129 ON session_tags(tag, session_id);
130 CREATE TABLE IF NOT EXISTS session_snapshots (
131 id TEXT PRIMARY KEY,
132 session_id TEXT NOT NULL,
133 captured_at_ms INTEGER NOT NULL,
134 captured_at TEXT NOT NULL,
135 body_json TEXT NOT NULL,
136 FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
137 );
138 ",
139 )
140 .map_err(|error| StoreError::Backend(error.to_string()))?;
141 conn.execute(
142 "INSERT OR IGNORE INTO schema_version(version) VALUES (?1)",
143 params![SCHEMA_VERSION],
144 )
145 .map_err(|error| StoreError::Backend(error.to_string()))?;
146 Ok(Self {
147 conn: Arc::new(Mutex::new(conn)),
148 hooks: Arc::new(hooks),
149 path,
150 })
151 }
152
153 pub fn path(&self) -> &Path {
154 &self.path
155 }
156
157 fn lock(&self) -> std::sync::MutexGuard<'_, Connection> {
158 self.conn.lock().unwrap_or_else(|e| e.into_inner())
159 }
160}
161
162fn map_sql(error: rusqlite::Error) -> StoreError {
163 StoreError::Backend(error.to_string())
164}
165
166fn kind_to_sql(kind: &SessionEventKind) -> (String, Option<String>) {
167 match kind {
168 SessionEventKind::Custom { custom_type } => {
169 ("custom".to_string(), Some(custom_type.clone()))
170 }
171 other => (other.discriminator().to_string(), None),
172 }
173}
174
175fn kind_from_sql(kind: &str, custom_kind: Option<String>) -> StoreResult<SessionEventKind> {
176 Ok(match kind {
177 "message" => SessionEventKind::Message,
178 "tool_call" => SessionEventKind::ToolCall,
179 "tool_result" => SessionEventKind::ToolResult,
180 "plan" => SessionEventKind::Plan,
181 "compaction" => SessionEventKind::Compaction,
182 "system_reminder" => SessionEventKind::SystemReminder,
183 "hypothesis" => SessionEventKind::Hypothesis,
184 "receipt" => SessionEventKind::Receipt,
185 "reminder" => SessionEventKind::Reminder,
186 "permission_decision" => SessionEventKind::PermissionDecision,
187 "custom" => SessionEventKind::Custom {
188 custom_type: custom_kind.unwrap_or_default(),
189 },
190 other => {
191 return Err(StoreError::Backend(format!(
192 "unknown event kind '{other}' in storage"
193 )))
194 }
195 })
196}
197
198fn status_to_sql(status: SessionStatus) -> &'static str {
199 match status {
200 SessionStatus::Open => "open",
201 SessionStatus::Closed => "closed",
202 SessionStatus::SoftDeleted => "soft_deleted",
203 SessionStatus::HardDeleted => "hard_deleted",
204 }
205}
206
207fn status_from_sql(value: &str) -> StoreResult<SessionStatus> {
208 Ok(match value {
209 "open" => SessionStatus::Open,
210 "closed" => SessionStatus::Closed,
211 "soft_deleted" => SessionStatus::SoftDeleted,
212 "hard_deleted" => SessionStatus::HardDeleted,
213 other => {
214 return Err(StoreError::Backend(format!(
215 "unknown session status '{other}' in storage"
216 )))
217 }
218 })
219}
220
221fn insert_session_tags(conn: &Connection, session_id: &str, tags: &[String]) -> StoreResult<()> {
222 if tags.is_empty() {
223 return Ok(());
224 }
225 let mut stmt = conn
226 .prepare("INSERT OR IGNORE INTO session_tags (session_id, tag) VALUES (?1, ?2)")
227 .map_err(map_sql)?;
228 for tag in tags {
229 stmt.execute(params![session_id, tag]).map_err(map_sql)?;
230 }
231 Ok(())
232}
233
234fn insert_session(
235 conn: &Connection,
236 meta: &SessionMeta,
237 next_event_id: EventId,
238) -> StoreResult<()> {
239 let tags_json = serde_json::to_string(&meta.tags).unwrap_or_else(|_| "[]".into());
240 let attrs_json = serde_json::to_string(&meta.attributes).unwrap_or_else(|_| "{}".into());
241 conn.execute(
242 "INSERT INTO sessions (
243 id, tenant_id, persona, parent_session_id, created_at_ms, created_at,
244 updated_at_ms, updated_at, status, event_count, last_event_id,
245 chain_root_hash, closed_at_ms, closed_at, soft_deleted_at_ms,
246 ttl_seconds, tags_json, attributes_json, next_event_id
247 ) VALUES (
248 ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14,
249 ?15, ?16, ?17, ?18, ?19
250 )",
251 params![
252 meta.id,
253 meta.tenant_id,
254 meta.persona,
255 meta.parent_session_id,
256 meta.created_at_ms,
257 meta.created_at,
258 meta.updated_at_ms,
259 meta.updated_at,
260 status_to_sql(meta.status),
261 meta.event_count as i64,
262 meta.last_event_id.map(|value| value as i64),
263 meta.chain_root_hash,
264 meta.closed_at_ms,
265 meta.closed_at,
266 meta.soft_deleted_at_ms,
267 meta.ttl_seconds.map(|value| value as i64),
268 tags_json,
269 attrs_json,
270 next_event_id as i64,
271 ],
272 )
273 .map_err(map_sql)?;
274 insert_session_tags(conn, &meta.id, &meta.tags)?;
275 Ok(())
276}
277
278fn read_session_meta(conn: &Connection, session_id: &str) -> StoreResult<(SessionMeta, EventId)> {
279 let row = conn
280 .query_row(
281 "SELECT tenant_id, persona, parent_session_id, created_at_ms, created_at,
282 updated_at_ms, updated_at, status, event_count, last_event_id,
283 chain_root_hash, closed_at_ms, closed_at, soft_deleted_at_ms,
284 ttl_seconds, tags_json, attributes_json, next_event_id
285 FROM sessions WHERE id = ?1",
286 params![session_id],
287 |row| {
288 Ok((
289 row.get::<_, Option<String>>(0)?,
290 row.get::<_, Option<String>>(1)?,
291 row.get::<_, Option<String>>(2)?,
292 row.get::<_, i64>(3)?,
293 row.get::<_, String>(4)?,
294 row.get::<_, i64>(5)?,
295 row.get::<_, String>(6)?,
296 row.get::<_, String>(7)?,
297 row.get::<_, i64>(8)?,
298 row.get::<_, Option<i64>>(9)?,
299 row.get::<_, Option<String>>(10)?,
300 row.get::<_, Option<i64>>(11)?,
301 row.get::<_, Option<String>>(12)?,
302 row.get::<_, Option<i64>>(13)?,
303 row.get::<_, Option<i64>>(14)?,
304 row.get::<_, String>(15)?,
305 row.get::<_, String>(16)?,
306 row.get::<_, i64>(17)?,
307 ))
308 },
309 )
310 .optional()
311 .map_err(map_sql)?
312 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
313 let (
314 tenant_id,
315 persona,
316 parent_session_id,
317 created_at_ms,
318 created_at,
319 updated_at_ms,
320 updated_at,
321 status,
322 event_count,
323 last_event_id,
324 chain_root_hash,
325 closed_at_ms,
326 closed_at,
327 soft_deleted_at_ms,
328 ttl_seconds,
329 tags_json,
330 attrs_json,
331 next_event_id,
332 ) = row;
333 let tags = serde_json::from_str(&tags_json).unwrap_or_default();
334 let attributes = serde_json::from_str(&attrs_json).unwrap_or_default();
335 let meta = SessionMeta {
336 id: session_id.to_string(),
337 tenant_id,
338 persona,
339 parent_session_id,
340 created_at_ms,
341 created_at,
342 updated_at_ms,
343 updated_at,
344 status: status_from_sql(&status)?,
345 event_count: event_count as usize,
346 last_event_id: last_event_id.map(|value| value as EventId),
347 chain_root_hash,
348 closed_at_ms,
349 closed_at,
350 soft_deleted_at_ms,
351 ttl_seconds: ttl_seconds.map(|value| value as u64),
352 tags,
353 attributes,
354 };
355 Ok((meta, next_event_id as EventId))
356}
357
358fn insert_event(conn: &Connection, event: &StoredEvent) -> StoreResult<()> {
359 let (kind, custom_kind) = kind_to_sql(&event.kind);
360 let payload_json = serde_json::to_string(&event.payload)
361 .map_err(|error| StoreError::Backend(error.to_string()))?;
362 let tags_json = serde_json::to_string(&event.tags).unwrap_or_else(|_| "[]".into());
363 let headers_json = serde_json::to_string(&event.headers).unwrap_or_else(|_| "{}".into());
364 let signature_json = event
365 .signed_by
366 .as_ref()
367 .map(|sig| serde_json::to_string(sig).unwrap_or_else(|_| "null".into()));
368 conn.execute(
369 "INSERT INTO session_events (
370 session_id, event_id, tenant_id, parent_event_id, actor, kind, custom_kind,
371 payload_json, tags_json, headers_json, ts_ms, ts, record_hash, prev_hash,
372 signature_json
373 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
374 params![
375 event.session_id,
376 event.event_id as i64,
377 event.tenant_id,
378 event.parent_event_id.map(|value| value as i64),
379 event.actor,
380 kind,
381 custom_kind,
382 payload_json,
383 tags_json,
384 headers_json,
385 event.ts_ms,
386 event.ts,
387 event.record_hash,
388 event.prev_hash,
389 signature_json,
390 ],
391 )
392 .map_err(map_sql)?;
393 Ok(())
394}
395
396fn read_event(row: &rusqlite::Row) -> Result<StoredEvent, rusqlite::Error> {
397 let session_id: String = row.get(0)?;
398 let event_id: i64 = row.get(1)?;
399 let tenant_id: Option<String> = row.get(2)?;
400 let parent_event_id: Option<i64> = row.get(3)?;
401 let actor: Option<String> = row.get(4)?;
402 let kind: String = row.get(5)?;
403 let custom_kind: Option<String> = row.get(6)?;
404 let payload_json: String = row.get(7)?;
405 let tags_json: String = row.get(8)?;
406 let headers_json: String = row.get(9)?;
407 let ts_ms: i64 = row.get(10)?;
408 let ts: String = row.get(11)?;
409 let record_hash: String = row.get(12)?;
410 let prev_hash: Option<String> = row.get(13)?;
411 let signature_json: Option<String> = row.get(14)?;
412 let resolved_kind = kind_from_sql(&kind, custom_kind).map_err(|error| {
413 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, error.into())
414 })?;
415 let payload = serde_json::from_str(&payload_json).map_err(|error| {
416 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, error.into())
417 })?;
418 let tags = serde_json::from_str(&tags_json).unwrap_or_default();
419 let headers = serde_json::from_str(&headers_json).unwrap_or_default();
420 let signed_by = signature_json
421 .as_ref()
422 .map(|value| serde_json::from_str::<EventSignature>(value))
423 .transpose()
424 .map_err(|error| {
425 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, error.into())
426 })?;
427 Ok(StoredEvent {
428 event_id: event_id as EventId,
429 session_id,
430 tenant_id,
431 parent_event_id: parent_event_id.map(|value| value as EventId),
432 actor,
433 kind: resolved_kind,
434 payload,
435 tags,
436 headers,
437 ts_ms,
438 ts,
439 record_hash,
440 prev_hash,
441 signed_by,
442 })
443}
444
445fn load_all_events(conn: &Connection, session_id: &str) -> StoreResult<Vec<StoredEvent>> {
446 let mut stmt = conn
447 .prepare(
448 "SELECT session_id, event_id, tenant_id, parent_event_id, actor, kind,
449 custom_kind, payload_json, tags_json, headers_json, ts_ms, ts,
450 record_hash, prev_hash, signature_json
451 FROM session_events WHERE session_id = ?1 ORDER BY event_id ASC",
452 )
453 .map_err(map_sql)?;
454 let rows = stmt
455 .query_map(params![session_id], read_event)
456 .map_err(map_sql)?;
457 let mut out = Vec::new();
458 for row in rows {
459 out.push(row.map_err(map_sql)?);
460 }
461 Ok(out)
462}
463
464fn apply_redaction(hooks: &StoreHooks, event: &mut AppendEvent) {
465 let Some(policy) = hooks.redaction.as_ref() else {
466 return;
467 };
468 policy.redact_json_in_place(&mut event.payload);
469 event.headers = policy.redact_headers(&event.headers);
470}
471
472fn append_in_tx(
479 conn: &Connection,
480 hooks: &StoreHooks,
481 session_id: &str,
482 mut event: AppendEvent,
483) -> StoreResult<StoredEvent> {
484 apply_redaction(hooks, &mut event);
485 let (mut meta, next_event_id) = read_session_meta(conn, session_id)?;
486 super::memory_helpers::validate_open(&meta)?;
487 if let Some(parent_event_id) = event.parent_event_id {
488 let exists: bool = conn
489 .query_row(
490 "SELECT 1 FROM session_events WHERE session_id = ?1 AND event_id = ?2",
491 params![session_id, parent_event_id as i64],
492 |_| Ok(true),
493 )
494 .optional()
495 .map_err(map_sql)?
496 .unwrap_or(false);
497 if !exists {
498 return Err(StoreError::InvalidInput(format!(
499 "parent_event_id {parent_event_id} not present in session"
500 )));
501 }
502 }
503 let prev_hash: Option<String> = conn
504 .query_row(
505 "SELECT record_hash FROM session_events
506 WHERE session_id = ?1 ORDER BY event_id DESC LIMIT 1",
507 params![session_id],
508 |row| row.get(0),
509 )
510 .optional()
511 .map_err(map_sql)?;
512 let (ts_ms, ts) = now_ms_and_rfc3339();
513 let mut stored = StoredEvent {
514 event_id: next_event_id,
515 session_id: session_id.to_string(),
516 tenant_id: meta.tenant_id.clone(),
517 parent_event_id: event.parent_event_id,
518 actor: event.actor,
519 kind: event.kind,
520 payload: event.payload,
521 tags: event.tags,
522 headers: event.headers,
523 ts_ms,
524 ts: ts.clone(),
525 record_hash: String::new(),
526 prev_hash,
527 signed_by: None,
528 };
529 stored.record_hash = compute_record_hash(&stored);
530 if let Some(signer) = hooks.event_signer.as_ref() {
531 stored.signed_by = Some(signer.sign_event(&stored));
532 }
533 insert_event(conn, &stored)?;
534 let prev_root = meta.chain_root_hash.clone().unwrap_or_else(chain_root_init);
535 let chain_root = chain_root_fold(&prev_root, &stored.record_hash);
536 meta.event_count = meta.event_count.saturating_add(1);
537 meta.last_event_id = Some(next_event_id);
538 meta.chain_root_hash = Some(chain_root);
539 meta.updated_at_ms = ts_ms;
540 meta.updated_at = ts;
541 conn.execute(
542 "UPDATE sessions SET event_count = ?1, last_event_id = ?2,
543 chain_root_hash = ?3, updated_at_ms = ?4,
544 updated_at = ?5, next_event_id = ?6 WHERE id = ?7",
545 params![
546 meta.event_count as i64,
547 meta.last_event_id.map(|value| value as i64),
548 meta.chain_root_hash,
549 meta.updated_at_ms,
550 meta.updated_at,
551 (next_event_id + 1) as i64,
552 session_id,
553 ],
554 )
555 .map_err(map_sql)?;
556 Ok(stored)
557}
558
559#[async_trait]
560impl SessionStore for SqliteSessionStore {
561 fn hooks(&self) -> &StoreHooks {
562 &self.hooks
563 }
564
565 async fn create(&self, request: CreateSession) -> StoreResult<SessionMeta> {
566 let meta = super::memory_helpers::meta_for_create(request);
567 let conn = self.lock();
568 if conn
569 .query_row(
570 "SELECT 1 FROM sessions WHERE id = ?1",
571 params![meta.id],
572 |_| Ok(()),
573 )
574 .optional()
575 .map_err(map_sql)?
576 .is_some()
577 {
578 return Err(StoreError::AlreadyExists(meta.id));
579 }
580 insert_session(&conn, &meta, 1)?;
581 Ok(meta)
582 }
583
584 async fn describe(&self, session_id: &str) -> StoreResult<SessionMeta> {
585 let conn = self.lock();
586 let (meta, _) = read_session_meta(&conn, session_id)?;
587 Ok(meta)
588 }
589
590 async fn list(&self, filter: ListFilter) -> StoreResult<Vec<SessionMeta>> {
591 let conn = self.lock();
592 let limit = filter.limit.unwrap_or(MAX_READ_BATCH).min(MAX_READ_BATCH) as i64;
593 let cursor_anchor: Option<(i64, String)> = filter
597 .cursor
598 .as_ref()
599 .map(|id| {
600 conn.query_row(
601 "SELECT created_at_ms, id FROM sessions WHERE id = ?1",
602 params![id],
603 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
604 )
605 .optional()
606 .map_err(map_sql)
607 })
608 .transpose()?
609 .flatten();
610
611 let mut sql = String::from("SELECT s.id FROM sessions s");
612 if filter.tag.is_some() {
613 sql.push_str(" INNER JOIN session_tags t ON t.session_id = s.id AND t.tag = :tag");
614 }
615 sql.push_str(" WHERE 1=1");
616 let mut args: Vec<(&'static str, rusqlite::types::Value)> = Vec::new();
617 if let Some(tag) = filter.tag {
618 args.push((":tag", tag.into()));
619 }
620 if let Some(tenant) = filter.tenant_id {
621 sql.push_str(" AND s.tenant_id = :tenant");
622 args.push((":tenant", tenant.into()));
623 }
624 if let Some(persona) = filter.persona {
625 sql.push_str(" AND s.persona = :persona");
626 args.push((":persona", persona.into()));
627 }
628 if let Some(status) = filter.status {
629 sql.push_str(" AND s.status = :status");
630 args.push((":status", status_to_sql(status).to_string().into()));
631 }
632 if let Some(after) = filter.created_after_ms {
633 sql.push_str(" AND s.created_at_ms >= :after");
634 args.push((":after", after.into()));
635 }
636 if let Some(before) = filter.created_before_ms {
637 sql.push_str(" AND s.created_at_ms <= :before");
638 args.push((":before", before.into()));
639 }
640 if let Some((anchor_ms, anchor_id)) = cursor_anchor {
641 sql.push_str(
642 " AND (s.created_at_ms > :cursor_ms OR (s.created_at_ms = :cursor_ms AND s.id > :cursor_id))",
643 );
644 args.push((":cursor_ms", anchor_ms.into()));
645 args.push((":cursor_id", anchor_id.into()));
646 }
647 sql.push_str(" ORDER BY s.created_at_ms ASC, s.id ASC LIMIT :limit");
648 args.push((":limit", limit.into()));
649
650 let named_args: Vec<(&str, &dyn rusqlite::ToSql)> = args
651 .iter()
652 .map(|(name, value)| (*name, value as &dyn rusqlite::ToSql))
653 .collect();
654 let mut stmt = conn.prepare(&sql).map_err(map_sql)?;
655 let ids: Vec<String> = stmt
656 .query_map(named_args.as_slice(), |row| row.get(0))
657 .map_err(map_sql)?
658 .collect::<Result<_, _>>()
659 .map_err(map_sql)?;
660 let mut metas = Vec::with_capacity(ids.len());
661 for id in ids {
662 let (meta, _) = read_session_meta(&conn, &id)?;
663 metas.push(meta);
664 }
665 Ok(metas)
666 }
667
668 async fn append(&self, session_id: &str, event: AppendEvent) -> StoreResult<StoredEvent> {
669 let mut conn = self.lock();
670 let tx = conn.transaction().map_err(map_sql)?;
671 let stored = append_in_tx(&tx, &self.hooks, session_id, event)?;
672 tx.commit().map_err(map_sql)?;
673 Ok(stored)
674 }
675
676 async fn read(&self, session_id: &str, range: ReadRange) -> StoreResult<EventPage> {
677 let conn = self.lock();
678 let from = range.from_event_id.unwrap_or(1) as i64;
679 let to = range
683 .to_event_id
684 .map(|value| value as i64)
685 .unwrap_or(i64::MAX);
686 let limit = range.limit.unwrap_or(MAX_READ_BATCH).min(MAX_READ_BATCH) as i64;
687 let mut stmt = conn
688 .prepare(
689 "SELECT session_id, event_id, tenant_id, parent_event_id, actor, kind,
690 custom_kind, payload_json, tags_json, headers_json, ts_ms, ts,
691 record_hash, prev_hash, signature_json
692 FROM session_events
693 WHERE session_id = ?1 AND event_id >= ?2 AND event_id <= ?3
694 ORDER BY event_id ASC LIMIT ?4",
695 )
696 .map_err(map_sql)?;
697 let rows = stmt
698 .query_map(params![session_id, from, to, limit], read_event)
699 .map_err(map_sql)?;
700 let mut events = Vec::new();
701 for row in rows {
702 events.push(row.map_err(map_sql)?);
703 }
704 if let Some(policy) = self.hooks.redaction.as_ref() {
705 for event in events.iter_mut() {
706 policy.redact_json_in_place(&mut event.payload);
707 }
708 }
709 let next_cursor = if events.len() as i64 == limit {
710 events.last().map(|tail| tail.event_id + 1)
711 } else {
712 None
713 };
714 Ok(EventPage {
715 events,
716 next_cursor,
717 })
718 }
719
720 async fn fork(
721 &self,
722 session_id: &str,
723 at_event_id: EventId,
724 child_id: Option<SessionId>,
725 ) -> StoreResult<ForkResult> {
726 let mut conn = self.lock();
727 let tx = conn.transaction().map_err(map_sql)?;
728 let (parent_meta, _) = read_session_meta(&tx, session_id)?;
729 let parent_events = load_all_events(&tx, session_id)?;
730 if !parent_events
731 .iter()
732 .any(|event| event.event_id == at_event_id)
733 {
734 return Err(StoreError::InvalidInput(format!(
735 "event {at_event_id} not found in session '{session_id}'"
736 )));
737 }
738 let new_id = child_id.unwrap_or_else(|| Uuid::now_v7().to_string());
739 let exists: bool = tx
740 .query_row(
741 "SELECT 1 FROM sessions WHERE id = ?1",
742 params![new_id],
743 |_| Ok(true),
744 )
745 .optional()
746 .map_err(map_sql)?
747 .unwrap_or(false);
748 if exists {
749 return Err(StoreError::AlreadyExists(new_id));
750 }
751 let (ms, text) = now_ms_and_rfc3339();
752 let mut child_meta = parent_meta.clone();
753 child_meta.id = new_id.clone();
754 child_meta.parent_session_id = Some(parent_meta.id);
755 child_meta.created_at_ms = ms;
756 child_meta.created_at = text.clone();
757 child_meta.updated_at_ms = ms;
758 child_meta.updated_at = text;
759 child_meta.status = SessionStatus::Open;
760 child_meta.closed_at_ms = None;
761 child_meta.closed_at = None;
762 child_meta.soft_deleted_at_ms = None;
763 let inherited: Vec<StoredEvent> = parent_events
764 .into_iter()
765 .filter(|event| event.event_id <= at_event_id)
766 .collect();
767 let copied = re_anchor_events(&inherited, &new_id);
768 child_meta.event_count = copied.len();
769 child_meta.last_event_id = copied.last().map(|tail| tail.event_id);
770 child_meta.chain_root_hash = Some(chain_root_hash(&copied));
771 let next_event_id = copied.last().map(|tail| tail.event_id + 1).unwrap_or(1);
772 insert_session(&tx, &child_meta, next_event_id)?;
773 for event in &copied {
774 insert_event(&tx, event)?;
775 }
776 tx.commit().map_err(map_sql)?;
777 Ok(ForkResult {
778 child_session_id: new_id,
779 forked_from_event_id: at_event_id,
780 copied_event_count: copied.len(),
781 })
782 }
783
784 async fn truncate(
785 &self,
786 session_id: &str,
787 at_event_id: EventId,
788 ) -> StoreResult<TruncateResult> {
789 let mut conn = self.lock();
790 let tx = conn.transaction().map_err(map_sql)?;
791 let (mut meta, _) = read_session_meta(&tx, session_id)?;
792 let exists: bool = tx
793 .query_row(
794 "SELECT 1 FROM session_events WHERE session_id = ?1 AND event_id = ?2",
795 params![session_id, at_event_id as i64],
796 |_| Ok(true),
797 )
798 .optional()
799 .map_err(map_sql)?
800 .unwrap_or(false);
801 if !exists {
802 return Err(StoreError::InvalidInput(format!(
803 "event {at_event_id} not found in session '{session_id}'"
804 )));
805 }
806 let removed: i64 = tx
807 .query_row(
808 "SELECT COUNT(*) FROM session_events
809 WHERE session_id = ?1 AND event_id > ?2",
810 params![session_id, at_event_id as i64],
811 |row| row.get(0),
812 )
813 .map_err(map_sql)?;
814 tx.execute(
815 "DELETE FROM session_events WHERE session_id = ?1 AND event_id > ?2",
816 params![session_id, at_event_id as i64],
817 )
818 .map_err(map_sql)?;
819 let remaining_hashes: Vec<String> = {
820 let mut stmt = tx
821 .prepare(
822 "SELECT record_hash FROM session_events
823 WHERE session_id = ?1 ORDER BY event_id ASC",
824 )
825 .map_err(map_sql)?;
826 let rows = stmt
827 .query_map(params![session_id], |row| row.get::<_, String>(0))
828 .map_err(map_sql)?;
829 let mut out = Vec::new();
830 for row in rows {
831 out.push(row.map_err(map_sql)?);
832 }
833 out
834 };
835 let new_root = remaining_hashes
836 .iter()
837 .fold(chain_root_init(), |root, hash| chain_root_fold(&root, hash));
838 let (ms, text) = now_ms_and_rfc3339();
839 meta.event_count = remaining_hashes.len();
840 meta.last_event_id = Some(at_event_id);
841 meta.chain_root_hash = Some(new_root);
842 meta.updated_at_ms = ms;
843 meta.updated_at = text;
844 tx.execute(
845 "UPDATE sessions SET event_count = ?1, last_event_id = ?2,
846 chain_root_hash = ?3, updated_at_ms = ?4,
847 updated_at = ?5, next_event_id = ?6 WHERE id = ?7",
848 params![
849 meta.event_count as i64,
850 meta.last_event_id.map(|value| value as i64),
851 meta.chain_root_hash,
852 meta.updated_at_ms,
853 meta.updated_at,
854 (at_event_id + 1) as i64,
855 session_id,
856 ],
857 )
858 .map_err(map_sql)?;
859 tx.commit().map_err(map_sql)?;
860 Ok(TruncateResult {
861 kept_event_count: meta.event_count,
862 removed_event_count: removed as usize,
863 new_tip_event_id: meta.last_event_id,
864 })
865 }
866
867 async fn snapshot(&self, session_id: &str) -> StoreResult<Snapshot> {
868 let conn = self.lock();
869 let (meta, _) = read_session_meta(&conn, session_id)?;
870 let events = load_all_events(&conn, session_id)?;
871 let (ms, text) = now_ms_and_rfc3339();
872 let snapshot = Snapshot {
873 id: SnapshotId(format!("snap-{}", Uuid::now_v7())),
874 session: meta,
875 events,
876 captured_at_ms: ms,
877 captured_at: text,
878 };
879 let body = serde_json::to_string(&snapshot)
880 .map_err(|error| StoreError::Backend(error.to_string()))?;
881 conn.execute(
882 "INSERT INTO session_snapshots (id, session_id, captured_at_ms, captured_at, body_json)
883 VALUES (?1, ?2, ?3, ?4, ?5)",
884 params![
885 snapshot.id.0,
886 snapshot.session.id,
887 snapshot.captured_at_ms,
888 snapshot.captured_at,
889 body,
890 ],
891 )
892 .map_err(map_sql)?;
893 Ok(snapshot)
894 }
895
896 async fn replay(&self, snapshot_id: &SnapshotId) -> StoreResult<Snapshot> {
897 let conn = self.lock();
898 let body: Option<String> = conn
899 .query_row(
900 "SELECT body_json FROM session_snapshots WHERE id = ?1",
901 params![snapshot_id.0],
902 |row| row.get(0),
903 )
904 .optional()
905 .map_err(map_sql)?;
906 let body = body.ok_or_else(|| StoreError::NotFound(snapshot_id.0.clone()))?;
907 serde_json::from_str(&body).map_err(|error| StoreError::Backend(error.to_string()))
908 }
909
910 async fn close(&self, session_id: &str) -> StoreResult<StoredEvent> {
911 let mut conn = self.lock();
912 let tx = conn.transaction().map_err(map_sql)?;
913 let (meta, _) = read_session_meta(&tx, session_id)?;
917 super::memory_helpers::validate_open(&meta)?;
918 let record_root = match meta.chain_root_hash.clone() {
919 Some(root) => root,
920 None => chain_root_hash(&load_all_events(&tx, session_id)?),
921 };
922 let last_event_id = meta.last_event_id.unwrap_or(0);
923 let payload =
924 super::signing::canonical_receipt_payload(session_id, last_event_id, &record_root);
925 let mut append = AppendEvent::new(SessionEventKind::Receipt, payload);
926 append.actor = Some("session_store".into());
927 let mut stored = append_in_tx(&tx, &self.hooks, session_id, append)?;
928 if let Some(signer) = self
934 .hooks
935 .receipt_signer
936 .as_ref()
937 .or(self.hooks.event_signer.as_ref())
938 {
939 let signature = signer.sign_receipt(&record_root);
940 let signature_json =
941 serde_json::to_string(&signature).unwrap_or_else(|_| "null".into());
942 tx.execute(
943 "UPDATE session_events SET signature_json = ?1
944 WHERE session_id = ?2 AND event_id = ?3",
945 params![signature_json, session_id, stored.event_id as i64],
946 )
947 .map_err(map_sql)?;
948 stored.signed_by = Some(signature);
949 }
950 let (ms, text) = now_ms_and_rfc3339();
951 tx.execute(
952 "UPDATE sessions SET status = ?1, closed_at_ms = ?2, closed_at = ?3,
953 updated_at_ms = ?2, updated_at = ?3 WHERE id = ?4",
954 params![status_to_sql(SessionStatus::Closed), ms, text, session_id,],
955 )
956 .map_err(map_sql)?;
957 tx.commit().map_err(map_sql)?;
958 Ok(stored)
959 }
960
961 async fn soft_delete(&self, session_id: &str) -> StoreResult<SessionMeta> {
962 let conn = self.lock();
963 let (mut meta, _) = read_session_meta(&conn, session_id)?;
964 match meta.status {
965 SessionStatus::HardDeleted => return Err(StoreError::NotFound(session_id.to_string())),
966 SessionStatus::SoftDeleted => return Ok(meta),
967 _ => {}
968 }
969 let (ms, text) = now_ms_and_rfc3339();
970 conn.execute(
971 "UPDATE sessions SET status = ?1, soft_deleted_at_ms = ?2,
972 updated_at_ms = ?2, updated_at = ?3 WHERE id = ?4",
973 params![
974 status_to_sql(SessionStatus::SoftDeleted),
975 ms,
976 text,
977 session_id,
978 ],
979 )
980 .map_err(map_sql)?;
981 meta.status = SessionStatus::SoftDeleted;
982 meta.soft_deleted_at_ms = Some(ms);
983 meta.updated_at_ms = ms;
984 meta.updated_at = text;
985 Ok(meta)
986 }
987
988 async fn hard_delete(&self, session_id: &str) -> StoreResult<()> {
989 let conn = self.lock();
990 let removed = conn
991 .execute("DELETE FROM sessions WHERE id = ?1", params![session_id])
992 .map_err(map_sql)?;
993 if removed == 0 {
994 return Err(StoreError::NotFound(session_id.to_string()));
995 }
996 Ok(())
997 }
998
999 async fn verify(&self, session_id: &str) -> StoreResult<VerifyReport> {
1000 let conn = self.lock();
1001 let events = load_all_events(&conn, session_id)?;
1002 let chain_root = chain_root_hash(&events);
1003 let event_verifier = self
1004 .hooks
1005 .event_signer
1006 .as_ref()
1007 .map(|signer| signer.verifying_key());
1008 let receipt_verifier = self
1009 .hooks
1010 .receipt_signer
1011 .as_ref()
1012 .or(self.hooks.event_signer.as_ref())
1013 .map(|signer| signer.verifying_key());
1014 let (signed, failures) =
1015 verify_event_chain(&events, event_verifier.as_ref(), receipt_verifier.as_ref());
1016 Ok(VerifyReport {
1017 session_id: session_id.to_string(),
1018 chain_root_hash: chain_root,
1019 event_count: events.len(),
1020 signed_event_count: signed,
1021 failures: failures
1022 .into_iter()
1023 .map(|(event_id, reason)| VerifyFailure { event_id, reason })
1024 .collect(),
1025 })
1026 }
1027}