1use std::path::Path;
20use std::sync::Arc;
21
22use async_trait::async_trait;
23use cel_memory::{
24 AccessEntry, AgingReport, CallerScope, ChunkKind, ChunkSource, EvictionEntry, EvictionReason,
25 ExportBundle, ExportFilter, MemoryChunk, MemoryError, MemoryPredicate, MemoryProvider,
26 MemoryQuery, MemorySession, MemoryStats, MemoryTier, NewMemoryChunk, NewMemorySession,
27 PurgeReport, ReEmbedReport, Result as MemoryResult, SessionFilter, SessionOutcome,
28};
29use chrono::{DateTime, NaiveDate, Utc};
30use rusqlite::{params, Connection, OptionalExtension};
31use tokio::sync::Mutex;
32use uuid::Uuid;
33
34use crate::cache::RetrieveCache;
35use crate::embedder::Embedder;
36use crate::error::SqliteMemoryError;
37use crate::migrations;
38
39const RETRIEVE_CACHE_CAPACITY: usize = 256;
43const RETRIEVE_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(30);
47
48pub struct SqliteMemoryProvider {
50 conn: Arc<Mutex<Connection>>,
51 embedder: Arc<dyn Embedder>,
52 write_hook: Option<Arc<dyn cel_memory::MemoryWriteHook>>,
57 summarizer: Option<Arc<dyn cel_memory::Summarizer>>,
62 retrieve_cache: Arc<RetrieveCache<Vec<MemoryChunk>>>,
65}
66
67impl SqliteMemoryProvider {
68 pub async fn open(
78 path: impl AsRef<Path>,
79 embedder: Arc<dyn Embedder>,
80 ) -> Result<Self, SqliteMemoryError> {
81 let path = path.as_ref().to_path_buf();
82 crate::vec_extension::register();
83 let conn = tokio::task::spawn_blocking(move || -> Result<Connection, SqliteMemoryError> {
84 let mut c = Connection::open(&path)?;
85 c.pragma_update(None, "journal_mode", "WAL")?;
87 c.pragma_update(None, "synchronous", "NORMAL")?;
88 migrations::run(&mut c)?;
89 Ok(c)
90 })
91 .await
92 .map_err(|e| SqliteMemoryError::BlockingJoin(e.to_string()))??;
93 Ok(Self {
94 conn: Arc::new(Mutex::new(conn)),
95 embedder,
96 write_hook: None,
97 summarizer: None,
98 retrieve_cache: Arc::new(RetrieveCache::new(
99 RETRIEVE_CACHE_CAPACITY,
100 RETRIEVE_CACHE_TTL,
101 )),
102 })
103 }
104
105 pub fn with_write_hook(mut self, hook: Arc<dyn cel_memory::MemoryWriteHook>) -> Self {
109 self.write_hook = Some(hook);
110 self
111 }
112
113 pub fn with_summarizer(mut self, summarizer: Arc<dyn cel_memory::Summarizer>) -> Self {
124 self.summarizer = Some(summarizer);
125 self
126 }
127
128 #[doc(hidden)]
134 pub fn conn_for_test(&self) -> Arc<tokio::sync::Mutex<rusqlite::Connection>> {
135 Arc::clone(&self.conn)
136 }
137
138 fn invalidate_retrieve_cache(&self) {
141 self.retrieve_cache.clear();
142 }
143
144 pub async fn open_in_memory(embedder: Arc<dyn Embedder>) -> Result<Self, SqliteMemoryError> {
146 crate::vec_extension::register();
147 let conn = tokio::task::spawn_blocking(|| -> Result<Connection, SqliteMemoryError> {
148 let mut c = Connection::open_in_memory()?;
149 migrations::run(&mut c)?;
150 Ok(c)
151 })
152 .await
153 .map_err(|e| SqliteMemoryError::BlockingJoin(e.to_string()))??;
154 Ok(Self {
155 conn: Arc::new(Mutex::new(conn)),
156 embedder,
157 write_hook: None,
158 summarizer: None,
159 retrieve_cache: Arc::new(RetrieveCache::new(
160 RETRIEVE_CACHE_CAPACITY,
161 RETRIEVE_CACHE_TTL,
162 )),
163 })
164 }
165
166 async fn fetch_session_chunks(&self, session_id: &str) -> MemoryResult<Vec<MemoryChunk>> {
178 let conn = Arc::clone(&self.conn);
179 let sid = session_id.to_string();
180 tokio::task::spawn_blocking(move || -> Result<Vec<MemoryChunk>, MemoryError> {
181 let guard = conn.blocking_lock();
182 let mut stmt = guard
183 .prepare(
184 "SELECT id, created_at, kind, tier, source, session_id,
185 project_root, caller_id, content, metadata,
186 importance, pinned, shareable, superseded_by,
187 embedding_model, embedding_dim
188 FROM memory_chunks
189 WHERE session_id = ?
190 AND kind != 'job_summary'
191 ORDER BY created_at ASC",
192 )
193 .map_err(|e| MemoryError::Storage(e.to_string()))?;
194 let rows = stmt
195 .query_map(params![sid], row_to_chunk)
196 .map_err(|e| MemoryError::Storage(e.to_string()))?;
197 let mut out = Vec::new();
198 for r in rows {
199 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
200 }
201 Ok(out)
202 })
203 .await
204 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
205 }
206
207 async fn fetch_day_chunks(&self, date: NaiveDate) -> MemoryResult<Vec<MemoryChunk>> {
213 let start = date
214 .and_hms_opt(0, 0, 0)
215 .ok_or_else(|| MemoryError::InvalidArgument(format!("invalid date: {date}")))?
216 .and_utc()
217 .timestamp_millis();
218 let end = date
219 .succ_opt()
220 .ok_or_else(|| MemoryError::InvalidArgument(format!("date overflow: {date}")))?
221 .and_hms_opt(0, 0, 0)
222 .ok_or_else(|| MemoryError::InvalidArgument(format!("invalid date: {date}")))?
223 .and_utc()
224 .timestamp_millis();
225 let conn = Arc::clone(&self.conn);
226 tokio::task::spawn_blocking(move || -> Result<Vec<MemoryChunk>, MemoryError> {
227 let guard = conn.blocking_lock();
228 let mut stmt = guard
229 .prepare(
230 "SELECT id, created_at, kind, tier, source, session_id,
231 project_root, caller_id, content, metadata,
232 importance, pinned, superseded_by,
233 embedding_model, embedding_dim
234 FROM memory_chunks
235 WHERE created_at >= ?
236 AND created_at < ?
237 AND kind != 'rollup'
238 ORDER BY created_at ASC",
239 )
240 .map_err(|e| MemoryError::Storage(e.to_string()))?;
241 let rows = stmt
242 .query_map(params![start, end], row_to_chunk)
243 .map_err(|e| MemoryError::Storage(e.to_string()))?;
244 let mut out = Vec::new();
245 for r in rows {
246 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
247 }
248 Ok(out)
249 })
250 .await
251 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
252 }
253
254 async fn fetch_rule_week_chunks(
261 &self,
262 rule_id: &str,
263 week_start: NaiveDate,
264 ) -> MemoryResult<Vec<MemoryChunk>> {
265 let start = week_start
266 .and_hms_opt(0, 0, 0)
267 .ok_or_else(|| MemoryError::InvalidArgument(format!("invalid date: {week_start}")))?
268 .and_utc()
269 .timestamp_millis();
270 let end_date = week_start
271 .checked_add_days(chrono::Days::new(7))
272 .ok_or_else(|| MemoryError::InvalidArgument(format!("week overflow: {week_start}")))?;
273 let end = end_date
274 .and_hms_opt(0, 0, 0)
275 .ok_or_else(|| MemoryError::InvalidArgument(format!("invalid date: {end_date}")))?
276 .and_utc()
277 .timestamp_millis();
278 let conn = Arc::clone(&self.conn);
279 let rid = rule_id.to_string();
280 tokio::task::spawn_blocking(move || -> Result<Vec<MemoryChunk>, MemoryError> {
281 let guard = conn.blocking_lock();
282 let mut stmt = guard
283 .prepare(
284 "SELECT id, created_at, kind, tier, source, session_id,
285 project_root, caller_id, content, metadata,
286 importance, pinned, superseded_by,
287 embedding_model, embedding_dim
288 FROM memory_chunks
289 WHERE kind = 'fire'
290 AND created_at >= ?
291 AND created_at < ?
292 AND json_extract(metadata, '$.rule_id') = ?
293 ORDER BY created_at ASC",
294 )
295 .map_err(|e| MemoryError::Storage(e.to_string()))?;
296 let rows = stmt
297 .query_map(params![start, end, rid], row_to_chunk)
298 .map_err(|e| MemoryError::Storage(e.to_string()))?;
299 let mut out = Vec::new();
300 for r in rows {
301 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
302 }
303 Ok(out)
304 })
305 .await
306 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
307 }
308
309 async fn day_rollup_exists(&self, date: NaiveDate) -> MemoryResult<bool> {
313 let date_s = date.to_string();
314 let conn = Arc::clone(&self.conn);
315 tokio::task::spawn_blocking(move || -> Result<bool, MemoryError> {
316 let guard = conn.blocking_lock();
317 let count: i64 = guard
318 .query_row(
319 "SELECT COUNT(*) FROM memory_chunks
320 WHERE kind = 'rollup'
321 AND json_extract(metadata, '$.rollup_date') = ?",
322 params![date_s],
323 |r| r.get(0),
324 )
325 .map_err(|e| MemoryError::Storage(e.to_string()))?;
326 Ok(count > 0)
327 })
328 .await
329 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
330 }
331
332 async fn rule_week_rollup_exists(
336 &self,
337 rule_id: &str,
338 week_start: NaiveDate,
339 ) -> MemoryResult<bool> {
340 let week_s = week_start.to_string();
341 let rid = rule_id.to_string();
342 let conn = Arc::clone(&self.conn);
343 tokio::task::spawn_blocking(move || -> Result<bool, MemoryError> {
344 let guard = conn.blocking_lock();
345 let count: i64 = guard
346 .query_row(
347 "SELECT COUNT(*) FROM memory_chunks
348 WHERE kind = 'rollup'
349 AND json_extract(metadata, '$.rollup_rule_id') = ?
350 AND json_extract(metadata, '$.rollup_week_start') = ?",
351 params![rid, week_s],
352 |r| r.get(0),
353 )
354 .map_err(|e| MemoryError::Storage(e.to_string()))?;
355 Ok(count > 0)
356 })
357 .await
358 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
359 }
360
361 async fn link_rollup_members(
365 &self,
366 rollup_id: &str,
367 member_ids: &[String],
368 ) -> MemoryResult<()> {
369 let rid = rollup_id.to_string();
370 let members: Vec<String> = member_ids.to_vec();
371 let conn = Arc::clone(&self.conn);
372 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
373 let mut guard = conn.blocking_lock();
374 let tx = guard
375 .transaction()
376 .map_err(|e| MemoryError::Storage(e.to_string()))?;
377 for mid in &members {
378 tx.execute(
379 "INSERT OR IGNORE INTO memory_summary_members(rollup_id, member_id)
380 VALUES (?, ?)",
381 params![rid, mid],
382 )
383 .map_err(|e| MemoryError::Storage(e.to_string()))?;
384 }
385 tx.commit()
386 .map_err(|e| MemoryError::Storage(e.to_string()))?;
387 Ok(())
388 })
389 .await
390 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
391 }
392
393 async fn rollup_day_inner(
406 &self,
407 date: NaiveDate,
408 force: bool,
409 ) -> MemoryResult<Vec<MemoryChunk>> {
410 let summarizer = self.summarizer.clone().ok_or(MemoryError::NotImplemented(
411 "SqliteMemoryProvider::rollup_day — no summarizer attached \
412 (call `with_summarizer` first)",
413 ))?;
414
415 if !force && self.day_rollup_exists(date).await? {
416 tracing::debug!(date = %date, "rollup_day: existing rollup found, skipping");
417 return Ok(Vec::new());
418 }
419
420 let members = self.fetch_day_chunks(date).await?;
421 if members.is_empty() {
422 tracing::debug!(date = %date, "rollup_day: no chunks to summarise, no-op");
423 return Ok(Vec::new());
424 }
425 let member_ids: Vec<String> = members.iter().map(|c| c.id.clone()).collect();
426
427 let kind_label = format!("day {date}");
428 let ctx = cel_memory::SummaryContext {
429 kind_label: Some(kind_label.clone()),
430 note: Some(format!(
431 "Daily rollup for {date} ({} chunks)",
432 members.len()
433 )),
434 max_words: None,
435 };
436 let summary_text = summarizer
437 .summarize(&members, &ctx)
438 .await
439 .map_err(|e| match e {
440 cel_memory::SummarizerError::NoInput => MemoryError::InvalidArgument(
441 "summarizer received no input despite day having chunks".into(),
442 ),
443 other => MemoryError::Provider(format!(
444 "summarizer {} failed: {other}",
445 summarizer.name()
446 )),
447 })?;
448
449 let new_chunk = NewMemoryChunk {
453 kind: ChunkKind::Rollup,
454 source: ChunkSource::System,
455 session_id: None,
456 project_root: None,
457 caller_id: "system".to_string(),
458 content: summary_text,
459 metadata: serde_json::json!({
460 "rollup_kind": "day",
461 "rollup_date": date.to_string(),
462 "member_count": member_ids.len(),
463 "summarizer": summarizer.name(),
464 }),
465 importance: None,
466 shareable: false,
467 pinned: false,
468 };
469 let written = self.write(new_chunk).await?;
470 self.link_rollup_members(&written.id, &member_ids).await?;
471 Ok(vec![written])
472 }
473
474 async fn rollup_rule_week_inner(
485 &self,
486 rule_id: &str,
487 week_start: NaiveDate,
488 force: bool,
489 ) -> MemoryResult<MemoryChunk> {
490 let summarizer = self.summarizer.clone().ok_or(MemoryError::NotImplemented(
491 "SqliteMemoryProvider::rollup_rule_week — no summarizer attached \
492 (call `with_summarizer` first)",
493 ))?;
494
495 if !force && self.rule_week_rollup_exists(rule_id, week_start).await? {
496 return Err(MemoryError::InvalidArgument(format!(
497 "rollup already exists for rule {rule_id} week {week_start}"
498 )));
499 }
500
501 let members = self.fetch_rule_week_chunks(rule_id, week_start).await?;
502 if members.is_empty() {
503 return Err(MemoryError::NotFound(format!(
504 "no fires for rule {rule_id} in week of {week_start}"
505 )));
506 }
507 let member_ids: Vec<String> = members.iter().map(|c| c.id.clone()).collect();
508
509 let kind_label = format!("week of {week_start} for rule {rule_id}");
510 let ctx = cel_memory::SummaryContext {
511 kind_label: Some(kind_label.clone()),
512 note: Some(format!(
513 "Weekly rollup for rule {rule_id} ({} fires)",
514 members.len()
515 )),
516 max_words: None,
517 };
518 let summary_text = summarizer
519 .summarize(&members, &ctx)
520 .await
521 .map_err(|e| match e {
522 cel_memory::SummarizerError::NoInput => MemoryError::InvalidArgument(
523 "summarizer received no input despite rule-week having chunks".into(),
524 ),
525 other => MemoryError::Provider(format!(
526 "summarizer {} failed: {other}",
527 summarizer.name()
528 )),
529 })?;
530
531 let new_chunk = NewMemoryChunk {
532 kind: ChunkKind::Rollup,
533 source: ChunkSource::System,
534 session_id: None,
535 project_root: None,
536 caller_id: "system".to_string(),
537 content: summary_text,
538 metadata: serde_json::json!({
539 "rollup_kind": "rule_week",
540 "rollup_rule_id": rule_id,
541 "rollup_week_start": week_start.to_string(),
542 "member_count": member_ids.len(),
543 "summarizer": summarizer.name(),
544 }),
545 importance: None,
546 shareable: false,
547 pinned: false,
548 };
549 let written = self.write(new_chunk).await?;
550 self.link_rollup_members(&written.id, &member_ids).await?;
551 Ok(written)
552 }
553}
554
555fn now_ms() -> i64 {
556 Utc::now().timestamp_millis()
557}
558
559fn cache_key_for_query(query: &MemoryQuery) -> Option<u64> {
565 use std::collections::hash_map::DefaultHasher;
566 use std::hash::{Hash, Hasher};
567 let s = serde_json::to_string(query).ok()?;
568 let mut h = DefaultHasher::new();
569 s.hash(&mut h);
570 Some(h.finish())
571}
572
573fn dt_to_ms(t: DateTime<Utc>) -> i64 {
574 t.timestamp_millis()
575}
576
577fn ms_to_dt(ms: i64) -> DateTime<Utc> {
578 chrono::DateTime::<Utc>::from_timestamp_millis(ms)
579 .unwrap_or_else(|| DateTime::<Utc>::from_timestamp_millis(0).unwrap())
580}
581
582fn kind_str(k: ChunkKind) -> &'static str {
583 match k {
584 ChunkKind::Chat => "chat",
585 ChunkKind::Action => "action",
586 ChunkKind::Fire => "fire",
587 ChunkKind::Observation => "observation",
588 ChunkKind::Correction => "correction",
589 ChunkKind::JobSummary => "job_summary",
590 ChunkKind::Context => "context",
591 ChunkKind::Rollup => "rollup",
592 }
593}
594
595fn str_to_kind(s: &str) -> Result<ChunkKind, MemoryError> {
596 Ok(match s {
597 "chat" => ChunkKind::Chat,
598 "action" => ChunkKind::Action,
599 "fire" => ChunkKind::Fire,
600 "observation" => ChunkKind::Observation,
601 "correction" => ChunkKind::Correction,
602 "job_summary" => ChunkKind::JobSummary,
603 "context" => ChunkKind::Context,
604 "rollup" => ChunkKind::Rollup,
605 other => return Err(MemoryError::Storage(format!("unknown kind: {other}"))),
606 })
607}
608
609fn source_str(s: ChunkSource) -> &'static str {
610 match s {
611 ChunkSource::Embedded => "embedded",
612 ChunkSource::Mcp => "mcp",
613 ChunkSource::Gateway => "gateway",
614 ChunkSource::Matcher => "matcher",
615 ChunkSource::Perception => "perception",
616 ChunkSource::System => "system",
617 }
618}
619
620const LEGACY_PERCEPTION_SOURCE: &str = concat!("cor", "tex");
621
622fn str_to_source(s: &str) -> Result<ChunkSource, MemoryError> {
623 Ok(match s {
624 "embedded" => ChunkSource::Embedded,
625 "mcp" => ChunkSource::Mcp,
626 "gateway" => ChunkSource::Gateway,
627 "matcher" => ChunkSource::Matcher,
628 "perception" | LEGACY_PERCEPTION_SOURCE => ChunkSource::Perception,
629 "system" => ChunkSource::System,
630 other => return Err(MemoryError::Storage(format!("unknown source: {other}"))),
631 })
632}
633
634fn tier_str(t: MemoryTier) -> &'static str {
635 match t {
636 MemoryTier::Session => "session",
637 MemoryTier::LongTerm => "long_term",
638 }
639}
640
641fn str_to_tier(s: &str) -> Result<MemoryTier, MemoryError> {
642 Ok(match s {
643 "session" => MemoryTier::Session,
644 "long_term" => MemoryTier::LongTerm,
645 other => return Err(MemoryError::Storage(format!("unknown tier: {other}"))),
646 })
647}
648
649fn outcome_str(o: SessionOutcome) -> &'static str {
650 match o {
651 SessionOutcome::Open => "open",
652 SessionOutcome::Success => "success",
653 SessionOutcome::Failure => "failure",
654 SessionOutcome::Aborted => "aborted",
655 }
656}
657
658fn str_to_outcome(s: &str) -> Result<SessionOutcome, MemoryError> {
659 Ok(match s {
660 "open" => SessionOutcome::Open,
661 "success" => SessionOutcome::Success,
662 "failure" => SessionOutcome::Failure,
663 "aborted" => SessionOutcome::Aborted,
664 other => return Err(MemoryError::Storage(format!("unknown outcome: {other}"))),
665 })
666}
667
668fn row_to_chunk(row: &rusqlite::Row<'_>) -> rusqlite::Result<MemoryChunk> {
669 let metadata_str: String = row.get("metadata")?;
670 let metadata: serde_json::Value =
671 serde_json::from_str(&metadata_str).unwrap_or(serde_json::Value::Null);
672 let shareable = row.get::<_, i64>("shareable").unwrap_or(0) != 0;
676 Ok(MemoryChunk {
677 id: row.get("id")?,
678 created_at: ms_to_dt(row.get::<_, i64>("created_at")?),
679 kind: str_to_kind(&row.get::<_, String>("kind")?).unwrap_or(ChunkKind::Chat),
680 tier: str_to_tier(&row.get::<_, String>("tier")?).unwrap_or(MemoryTier::Session),
681 source: str_to_source(&row.get::<_, String>("source")?).unwrap_or(ChunkSource::System),
682 session_id: row.get("session_id")?,
683 project_root: row.get("project_root")?,
684 caller_id: row.get("caller_id")?,
685 content: row.get("content")?,
686 metadata,
687 importance: row.get::<_, f64>("importance")? as f32,
688 pinned: row.get::<_, i64>("pinned")? != 0,
689 shareable,
690 superseded_by: row.get("superseded_by")?,
691 embedding_model: row.get("embedding_model")?,
692 embedding_dim: row.get::<_, i64>("embedding_dim")? as u32,
693 })
694}
695
696#[async_trait]
697impl MemoryProvider for SqliteMemoryProvider {
698 async fn retrieve(&self, query: MemoryQuery) -> MemoryResult<Vec<MemoryChunk>> {
701 if query.text.trim().is_empty() {
719 return Err(MemoryError::InvalidArgument(
720 "query.text must not be empty".into(),
721 ));
722 }
723
724 let cache_key = cache_key_for_query(&query);
727 if let Some(key) = cache_key {
728 if let Some(hit) = self.retrieve_cache.get(key) {
729 return Ok(hit);
730 }
731 }
732 let k = query.k.max(1);
733 let candidate_k = (3 * k).max(16);
736
737 let (w_vec, w_fts, w_rec, half_life_secs) = retrieval_weights(query.profile);
738
739 let q_embedding = self
740 .embedder
741 .embed(&query.text)
742 .await
743 .map_err(|e| MemoryError::Storage(e.to_string()))?;
744 let q_text = query.text.clone();
745 let conn = Arc::clone(&self.conn);
746
747 let compute =
748 tokio::task::spawn_blocking(move || -> Result<Vec<MemoryChunk>, MemoryError> {
749 let guard = conn.blocking_lock();
750
751 let v_json = serde_json::to_string(&q_embedding)
757 .map_err(|e| MemoryError::Storage(e.to_string()))?;
758 let vec_ids: Vec<String> = {
759 let sql = "SELECT v.chunk_id FROM memory_vec v
760 WHERE v.embedding MATCH ?
761 AND k = ?
762 ORDER BY distance";
763 let mut stmt = guard
764 .prepare(sql)
765 .map_err(|e| MemoryError::Storage(e.to_string()))?;
766 let rows = stmt
767 .query_map(params![v_json, candidate_k as i64], |r| {
768 r.get::<_, String>(0)
769 })
770 .map_err(|e| MemoryError::Storage(e.to_string()))?;
771 let mut out = Vec::new();
772 for r in rows {
773 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
774 }
775 out
776 };
777
778 let fts_ids: Vec<String> = {
781 let sql = "SELECT chunk_id FROM memory_fts
782 WHERE memory_fts MATCH ?
783 ORDER BY rank
784 LIMIT ?";
785 let mut stmt = guard
786 .prepare(sql)
787 .map_err(|e| MemoryError::Storage(e.to_string()))?;
788 let rows = stmt
789 .query_map(
790 params![fts_query_escape(&q_text), candidate_k as i64],
791 |r| r.get::<_, String>(0),
792 )
793 .map_err(|e| MemoryError::Storage(e.to_string()))?;
794 let mut out = Vec::new();
795 for r in rows {
796 match r {
800 Ok(id) => out.push(id),
801 Err(_) => break,
802 }
803 }
804 out
805 };
806
807 let recency_ids: Vec<String> = {
811 let sql = "SELECT id FROM memory_chunks
812 ORDER BY created_at DESC LIMIT ?";
813 let mut stmt = guard
814 .prepare(sql)
815 .map_err(|e| MemoryError::Storage(e.to_string()))?;
816 let rows = stmt
817 .query_map(params![candidate_k as i64], |r| r.get::<_, String>(0))
818 .map_err(|e| MemoryError::Storage(e.to_string()))?;
819 let mut out = Vec::new();
820 for r in rows {
821 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
822 }
823 out
824 };
825
826 const RRF_K: f32 = 60.0;
828 let mut scores: std::collections::HashMap<String, f32> =
829 std::collections::HashMap::new();
830 for (rank, id) in vec_ids.iter().enumerate() {
831 *scores.entry(id.clone()).or_insert(0.0) += w_vec / (RRF_K + (rank + 1) as f32);
832 }
833 for (rank, id) in fts_ids.iter().enumerate() {
834 *scores.entry(id.clone()).or_insert(0.0) += w_fts / (RRF_K + (rank + 1) as f32);
835 }
836 let now_ms_val = now_ms();
840 let recency_rows: std::collections::HashMap<String, i64> = {
841 if recency_ids.is_empty() {
842 Default::default()
843 } else {
844 let placeholders = vec!["?"; recency_ids.len()].join(",");
845 let sql = format!(
846 "SELECT id, created_at FROM memory_chunks WHERE id IN ({placeholders})"
847 );
848 let p: Vec<&dyn rusqlite::ToSql> = recency_ids
849 .iter()
850 .map(|s| s as &dyn rusqlite::ToSql)
851 .collect();
852 let mut stmt = guard
853 .prepare(&sql)
854 .map_err(|e| MemoryError::Storage(e.to_string()))?;
855 let rows = stmt
856 .query_map(p.as_slice(), |r| {
857 Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?))
858 })
859 .map_err(|e| MemoryError::Storage(e.to_string()))?;
860 let mut map = std::collections::HashMap::new();
861 for r in rows {
862 let (id, t) = r.map_err(|e| MemoryError::Storage(e.to_string()))?;
863 map.insert(id, t);
864 }
865 map
866 }
867 };
868 for (id, created_ms) in &recency_rows {
869 let dt_secs = ((now_ms_val - created_ms).max(0) / 1000) as f32;
870 let recency = (-dt_secs / half_life_secs).exp();
871 *scores.entry(id.clone()).or_insert(0.0) += w_rec * recency;
872 }
873
874 let mut id_set: std::collections::HashSet<String> =
876 std::collections::HashSet::new();
877 id_set.extend(vec_ids);
878 id_set.extend(fts_ids);
879 id_set.extend(recency_ids);
880 if id_set.is_empty() {
881 return Ok(Vec::new());
882 }
883 let placeholders = vec!["?"; id_set.len()].join(",");
884 let select_sql = format!(
885 "SELECT id, created_at, kind, tier, source, session_id,
886 project_root, caller_id, content, metadata,
887 importance, pinned, shareable, superseded_by,
888 embedding_model, embedding_dim
889 FROM memory_chunks WHERE id IN ({placeholders})"
890 );
891 let ids_vec: Vec<String> = id_set.into_iter().collect();
892 let candidates: Vec<MemoryChunk> = {
893 let p: Vec<&dyn rusqlite::ToSql> =
894 ids_vec.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
895 let mut stmt = guard
896 .prepare(&select_sql)
897 .map_err(|e| MemoryError::Storage(e.to_string()))?;
898 let rows = stmt
899 .query_map(p.as_slice(), row_to_chunk)
900 .map_err(|e| MemoryError::Storage(e.to_string()))?;
901 let mut out = Vec::new();
902 for r in rows {
903 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
904 }
905 out
906 };
907
908 let mut filtered: Vec<MemoryChunk> = candidates
911 .into_iter()
912 .filter(|c| chunk_matches_query(c, &query))
913 .collect();
914
915 filtered.sort_by(|a, b| {
917 let sa = scores.get(&a.id).copied().unwrap_or(0.0);
918 let sb = scores.get(&b.id).copied().unwrap_or(0.0);
919 sb.partial_cmp(&sa).unwrap_or(std::cmp::Ordering::Equal)
920 });
921 filtered.truncate(k);
922 Ok(filtered)
923 });
924 let result = compute
925 .await
926 .map_err(|e| MemoryError::Internal(format!("join: {e}")))??;
927
928 if let Some(key) = cache_key {
932 self.retrieve_cache.insert(key, result.clone());
933 }
934 Ok(result)
935 }
936
937 async fn get(&self, chunk_id: &str) -> MemoryResult<Option<MemoryChunk>> {
938 let conn = Arc::clone(&self.conn);
939 let chunk_id = chunk_id.to_string();
940 let res: Result<Option<MemoryChunk>, MemoryError> =
941 tokio::task::spawn_blocking(move || -> Result<Option<MemoryChunk>, MemoryError> {
942 let guard = conn.blocking_lock();
943 let mut stmt = guard
944 .prepare(
945 "SELECT id, created_at, kind, tier, source, session_id,
946 project_root, caller_id, content, metadata,
947 importance, pinned, shareable, superseded_by,
948 embedding_model, embedding_dim
949 FROM memory_chunks WHERE id = ?",
950 )
951 .map_err(|e| MemoryError::Storage(e.to_string()))?;
952 let row = stmt
953 .query_row(params![chunk_id], row_to_chunk)
954 .optional()
955 .map_err(|e| MemoryError::Storage(e.to_string()))?;
956 Ok(row)
957 })
958 .await
959 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?;
960 res
961 }
962
963 async fn get_session(&self, session_id: &str) -> MemoryResult<Option<MemorySession>> {
964 let conn = Arc::clone(&self.conn);
965 let session_id = session_id.to_string();
966 tokio::task::spawn_blocking(move || -> Result<Option<MemorySession>, MemoryError> {
967 let guard = conn.blocking_lock();
968 let mut stmt = guard
969 .prepare(
970 "SELECT id, started_at, ended_at, caller_id, title, summary,
971 outcome, metadata
972 FROM memory_sessions WHERE id = ?",
973 )
974 .map_err(|e| MemoryError::Storage(e.to_string()))?;
975 let row = stmt
976 .query_row(params![session_id], |r| {
977 let metadata_str: String = r.get("metadata")?;
978 let metadata: serde_json::Value =
979 serde_json::from_str(&metadata_str).unwrap_or(serde_json::Value::Null);
980 Ok(MemorySession {
981 id: r.get("id")?,
982 started_at: ms_to_dt(r.get::<_, i64>("started_at")?),
983 ended_at: r.get::<_, Option<i64>>("ended_at")?.map(ms_to_dt),
984 caller_id: r.get("caller_id")?,
985 title: r.get("title")?,
986 summary: r.get("summary")?,
987 outcome: str_to_outcome(&r.get::<_, String>("outcome")?)
988 .unwrap_or(SessionOutcome::Aborted),
989 metadata,
990 })
991 })
992 .optional()
993 .map_err(|e| MemoryError::Storage(e.to_string()))?;
994 Ok(row)
995 })
996 .await
997 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
998 }
999
1000 async fn list_sessions(&self, filter: SessionFilter) -> MemoryResult<Vec<MemorySession>> {
1001 let conn = Arc::clone(&self.conn);
1002 tokio::task::spawn_blocking(move || -> Result<Vec<MemorySession>, MemoryError> {
1003 let guard = conn.blocking_lock();
1004 let mut sql = String::from(
1008 "SELECT id, started_at, ended_at, caller_id, title, summary,
1009 outcome, metadata
1010 FROM memory_sessions WHERE 1=1",
1011 );
1012 let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
1013 if let Some(c) = &filter.caller_id {
1014 sql.push_str(" AND caller_id = ?");
1015 params_vec.push(Box::new(c.clone()));
1016 }
1017 if let Some(o) = filter.outcome {
1018 sql.push_str(" AND outcome = ?");
1019 params_vec.push(Box::new(outcome_str(o).to_string()));
1020 } else if filter.open_only {
1021 sql.push_str(" AND outcome = ?");
1022 params_vec.push(Box::new("open".to_string()));
1023 }
1024 if let Some(since) = filter.since {
1025 sql.push_str(" AND started_at >= ?");
1026 params_vec.push(Box::new(dt_to_ms(since)));
1027 }
1028 if let Some(until) = filter.until {
1029 sql.push_str(" AND started_at <= ?");
1030 params_vec.push(Box::new(dt_to_ms(until)));
1031 }
1032 sql.push_str(" ORDER BY started_at DESC");
1033 if let Some(n) = filter.limit {
1034 sql.push_str(" LIMIT ?");
1035 params_vec.push(Box::new(n as i64));
1036 }
1037
1038 let mut stmt = guard
1039 .prepare(&sql)
1040 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1041 let p: Vec<&dyn rusqlite::ToSql> = params_vec.iter().map(|b| b.as_ref()).collect();
1042 let rows = stmt
1043 .query_map(p.as_slice(), |r| {
1044 let metadata_str: String = r.get("metadata")?;
1045 let metadata: serde_json::Value =
1046 serde_json::from_str(&metadata_str).unwrap_or(serde_json::Value::Null);
1047 Ok(MemorySession {
1048 id: r.get("id")?,
1049 started_at: ms_to_dt(r.get::<_, i64>("started_at")?),
1050 ended_at: r.get::<_, Option<i64>>("ended_at")?.map(ms_to_dt),
1051 caller_id: r.get("caller_id")?,
1052 title: r.get("title")?,
1053 summary: r.get("summary")?,
1054 outcome: str_to_outcome(&r.get::<_, String>("outcome")?)
1055 .unwrap_or(SessionOutcome::Aborted),
1056 metadata,
1057 })
1058 })
1059 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1060 let mut out = Vec::new();
1061 for row in rows {
1062 out.push(row.map_err(|e| MemoryError::Storage(e.to_string()))?);
1063 }
1064 Ok(out)
1065 })
1066 .await
1067 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1068 }
1069
1070 async fn write(&self, new_chunk: NewMemoryChunk) -> MemoryResult<MemoryChunk> {
1073 if new_chunk.content.trim().is_empty() {
1074 return Err(MemoryError::InvalidArgument(
1075 "content must not be empty".into(),
1076 ));
1077 }
1078 self.invalidate_retrieve_cache();
1082
1083 if let Some(hook) = &self.write_hook {
1088 match hook.before_write(&new_chunk).await? {
1089 cel_memory::WriteDecision::Allow => {}
1090 cel_memory::WriteDecision::Redact { reason } => {
1091 return Ok(MemoryChunk {
1092 id: Uuid::now_v7().to_string(),
1093 created_at: Utc::now(),
1094 kind: new_chunk.kind,
1095 tier: MemoryTier::Session,
1096 source: new_chunk.source,
1097 session_id: new_chunk.session_id,
1098 project_root: new_chunk.project_root,
1099 caller_id: new_chunk.caller_id,
1100 content: format!("<redacted: {reason}>"),
1101 metadata: serde_json::json!({"redacted": true, "reason": reason}),
1102 importance: 0.0,
1103 pinned: false,
1104 shareable: false,
1105 superseded_by: None,
1106 embedding_model: "none".into(),
1107 embedding_dim: 0,
1108 });
1109 }
1110 }
1111 }
1112
1113 let id = Uuid::now_v7().to_string();
1114 let created_at_ms = now_ms();
1115 let importance = cel_memory::importance::score(&new_chunk);
1119 let embedder_dim = self.embedder.dim();
1120 let embedder_name = self.embedder.model_name().to_string();
1121 let embedding = self
1124 .embedder
1125 .embed(&new_chunk.content)
1126 .await
1127 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1128 if embedding.len() != embedder_dim {
1129 return Err(MemoryError::Internal(format!(
1130 "embedder produced dim {}, declared {}",
1131 embedding.len(),
1132 embedder_dim
1133 )));
1134 }
1135
1136 let chunk = MemoryChunk {
1137 id: id.clone(),
1138 created_at: ms_to_dt(created_at_ms),
1139 kind: new_chunk.kind,
1140 tier: MemoryTier::Session,
1141 source: new_chunk.source,
1142 session_id: new_chunk.session_id.clone(),
1143 project_root: new_chunk.project_root.clone(),
1144 caller_id: new_chunk.caller_id.clone(),
1145 content: new_chunk.content.clone(),
1146 metadata: new_chunk.metadata.clone(),
1147 importance,
1148 pinned: new_chunk.pinned,
1149 shareable: new_chunk.shareable,
1150 superseded_by: None,
1151 embedding_model: embedder_name.clone(),
1152 embedding_dim: embedder_dim as u32,
1153 };
1154
1155 let conn = Arc::clone(&self.conn);
1156 let chunk_for_blocking = chunk.clone();
1157 let embedding_clone = embedding.clone();
1158 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1159 let mut guard = conn.blocking_lock();
1160 let tx = guard
1161 .transaction()
1162 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1163 tx.execute(
1164 "INSERT INTO memory_chunks(
1165 id, created_at, kind, tier, source, session_id, project_root,
1166 caller_id, content, metadata, importance, pinned, shareable,
1167 superseded_by, embedding_model, embedding_dim
1168 ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
1169 params![
1170 chunk_for_blocking.id,
1171 created_at_ms,
1172 kind_str(chunk_for_blocking.kind),
1173 tier_str(chunk_for_blocking.tier),
1174 source_str(chunk_for_blocking.source),
1175 chunk_for_blocking.session_id,
1176 chunk_for_blocking.project_root,
1177 chunk_for_blocking.caller_id,
1178 chunk_for_blocking.content,
1179 serde_json::to_string(&chunk_for_blocking.metadata)
1180 .unwrap_or_else(|_| "null".into()),
1181 chunk_for_blocking.importance as f64,
1182 if chunk_for_blocking.pinned { 1 } else { 0 },
1183 if new_chunk.shareable { 1 } else { 0 },
1184 Option::<String>::None,
1185 chunk_for_blocking.embedding_model,
1186 chunk_for_blocking.embedding_dim as i64,
1187 ],
1188 )
1189 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1190 let v_json = serde_json::to_string(&embedding_clone)
1194 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1195 tx.execute(
1196 "INSERT INTO memory_vec(chunk_id, embedding) VALUES (?, ?)",
1197 params![chunk_for_blocking.id, v_json],
1198 )
1199 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1200 tx.commit()
1201 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1202 Ok(())
1203 })
1204 .await
1205 .map_err(|e| MemoryError::Internal(format!("join: {e}")))??;
1206
1207 Ok(chunk)
1208 }
1209
1210 async fn write_batch(&self, chunks: Vec<NewMemoryChunk>) -> MemoryResult<Vec<MemoryChunk>> {
1211 if chunks.is_empty() {
1216 return Ok(Vec::new());
1217 }
1218 if chunks.len() == 1 {
1219 let nc = chunks.into_iter().next().expect("len == 1");
1220 return Ok(vec![self.write(nc).await?]);
1221 }
1222 self.invalidate_retrieve_cache();
1223
1224 if self.write_hook.is_some() {
1229 let mut out = Vec::with_capacity(chunks.len());
1230 for nc in chunks {
1231 out.push(self.write(nc).await?);
1232 }
1233 return Ok(out);
1234 }
1235
1236 for (i, nc) in chunks.iter().enumerate() {
1239 if nc.content.trim().is_empty() {
1240 return Err(MemoryError::InvalidArgument(format!(
1241 "chunks[{i}].content must not be empty"
1242 )));
1243 }
1244 }
1245
1246 let embedder_dim = self.embedder.dim();
1247 let embedder_name = self.embedder.model_name().to_string();
1248
1249 let texts: Vec<String> = chunks.iter().map(|c| c.content.clone()).collect();
1254 let embeddings = self
1255 .embedder
1256 .embed_batch(&texts)
1257 .await
1258 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1259 if embeddings.len() != chunks.len() {
1260 return Err(MemoryError::Internal(format!(
1261 "embedder returned {} vectors for {} inputs",
1262 embeddings.len(),
1263 chunks.len()
1264 )));
1265 }
1266 for (i, v) in embeddings.iter().enumerate() {
1267 if v.len() != embedder_dim {
1268 return Err(MemoryError::Internal(format!(
1269 "embedder produced dim {} for chunk {i}, declared {}",
1270 v.len(),
1271 embedder_dim
1272 )));
1273 }
1274 }
1275
1276 let now_ms_val = now_ms();
1279 let mut owned: Vec<(MemoryChunk, Vec<f32>, bool, String)> =
1280 Vec::with_capacity(chunks.len());
1281 for (nc, embedding) in chunks.into_iter().zip(embeddings) {
1282 let id = Uuid::now_v7().to_string();
1283 let importance = cel_memory::importance::score(&nc);
1284 let metadata_json =
1285 serde_json::to_string(&nc.metadata).unwrap_or_else(|_| "null".into());
1286 let mc = MemoryChunk {
1287 id,
1288 created_at: ms_to_dt(now_ms_val),
1289 kind: nc.kind,
1290 tier: MemoryTier::Session,
1291 source: nc.source,
1292 session_id: nc.session_id,
1293 project_root: nc.project_root,
1294 caller_id: nc.caller_id,
1295 content: nc.content,
1296 metadata: nc.metadata,
1297 importance,
1298 pinned: nc.pinned,
1299 shareable: nc.shareable,
1300 superseded_by: None,
1301 embedding_model: embedder_name.clone(),
1302 embedding_dim: embedder_dim as u32,
1303 };
1304 owned.push((mc, embedding, nc.shareable, metadata_json));
1305 }
1306
1307 let conn = Arc::clone(&self.conn);
1308 let inserted =
1309 tokio::task::spawn_blocking(move || -> Result<Vec<MemoryChunk>, MemoryError> {
1310 let mut guard = conn.blocking_lock();
1311 let tx = guard
1312 .transaction()
1313 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1314 let mut out = Vec::with_capacity(owned.len());
1315 for (mc, embedding, shareable, metadata_json) in &owned {
1316 tx.execute(
1317 "INSERT INTO memory_chunks(
1318 id, created_at, kind, tier, source, session_id, project_root,
1319 caller_id, content, metadata, importance, pinned, shareable,
1320 superseded_by, embedding_model, embedding_dim
1321 ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
1322 params![
1323 mc.id,
1324 now_ms_val,
1325 kind_str(mc.kind),
1326 tier_str(mc.tier),
1327 source_str(mc.source),
1328 mc.session_id,
1329 mc.project_root,
1330 mc.caller_id,
1331 mc.content,
1332 metadata_json,
1333 mc.importance as f64,
1334 if mc.pinned { 1 } else { 0 },
1335 if *shareable { 1 } else { 0 },
1336 Option::<String>::None,
1337 mc.embedding_model,
1338 mc.embedding_dim as i64,
1339 ],
1340 )
1341 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1342 let v_json = serde_json::to_string(embedding)
1343 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1344 tx.execute(
1345 "INSERT INTO memory_vec(chunk_id, embedding) VALUES (?, ?)",
1346 params![mc.id, v_json],
1347 )
1348 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1349 out.push(mc.clone());
1350 }
1351 tx.commit()
1352 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1353 Ok(out)
1354 })
1355 .await
1356 .map_err(|e| MemoryError::Internal(format!("join: {e}")))??;
1357
1358 Ok(inserted)
1359 }
1360
1361 async fn open_session(&self, init: NewMemorySession) -> MemoryResult<MemorySession> {
1362 let session = MemorySession {
1363 id: Uuid::now_v7().to_string(),
1364 started_at: Utc::now(),
1365 ended_at: None,
1366 caller_id: init.caller_id.clone(),
1367 title: init.title.clone(),
1368 summary: None,
1369 outcome: SessionOutcome::Open,
1370 metadata: init.metadata.clone(),
1371 };
1372 let conn = Arc::clone(&self.conn);
1373 let s = session.clone();
1374 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1375 let guard = conn.blocking_lock();
1376 guard
1377 .execute(
1378 "INSERT INTO memory_sessions(
1379 id, started_at, ended_at, caller_id, title, summary,
1380 outcome, metadata
1381 ) VALUES (?,?,?,?,?,?,?,?)",
1382 params![
1383 s.id,
1384 dt_to_ms(s.started_at),
1385 Option::<i64>::None,
1386 s.caller_id,
1387 s.title,
1388 Option::<String>::None,
1389 outcome_str(s.outcome),
1390 serde_json::to_string(&s.metadata).unwrap_or_else(|_| "{}".into()),
1391 ],
1392 )
1393 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1394 Ok(())
1395 })
1396 .await
1397 .map_err(|e| MemoryError::Internal(format!("join: {e}")))??;
1398 Ok(session)
1399 }
1400
1401 async fn close_session(&self, session_id: &str, outcome: SessionOutcome) -> MemoryResult<()> {
1402 let resolved = match outcome {
1403 SessionOutcome::Open => SessionOutcome::Aborted,
1404 other => other,
1405 };
1406 let conn = Arc::clone(&self.conn);
1407 let sid = session_id.to_string();
1408 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1409 let guard = conn.blocking_lock();
1410 let n = guard
1411 .execute(
1412 "UPDATE memory_sessions SET ended_at = ?, outcome = ? WHERE id = ?",
1413 params![now_ms(), outcome_str(resolved), sid],
1414 )
1415 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1416 if n == 0 {
1417 return Err(MemoryError::NotFound(format!("session {sid}")));
1418 }
1419 Ok(())
1420 })
1421 .await
1422 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1423 }
1424
1425 async fn rename_session(&self, session_id: &str, title: &str) -> MemoryResult<()> {
1426 let conn = Arc::clone(&self.conn);
1427 let sid = session_id.to_string();
1428 let new_title = title.to_string();
1429 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1430 let guard = conn.blocking_lock();
1431 let n = guard
1432 .execute(
1433 "UPDATE memory_sessions SET title = ? WHERE id = ?",
1434 params![new_title, sid],
1435 )
1436 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1437 if n == 0 {
1438 return Err(MemoryError::NotFound(format!("session {sid}")));
1439 }
1440 Ok(())
1441 })
1442 .await
1443 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1444 }
1445
1446 async fn pin(&self, chunk_id: &str, pinned: bool) -> MemoryResult<()> {
1449 self.invalidate_retrieve_cache();
1450 let conn = Arc::clone(&self.conn);
1451 let id = chunk_id.to_string();
1452 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1453 let guard = conn.blocking_lock();
1454 let n = guard
1455 .execute(
1456 "UPDATE memory_chunks SET pinned = ? WHERE id = ?",
1457 params![if pinned { 1 } else { 0 }, id],
1458 )
1459 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1460 if n == 0 {
1461 return Err(MemoryError::NotFound(format!("chunk {id}")));
1462 }
1463 Ok(())
1464 })
1465 .await
1466 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1467 }
1468
1469 async fn update_importance(&self, chunk_id: &str, importance: f32) -> MemoryResult<()> {
1470 self.invalidate_retrieve_cache();
1471 let conn = Arc::clone(&self.conn);
1472 let id = chunk_id.to_string();
1473 let clamped = importance.clamp(0.0, 1.0) as f64;
1474 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1475 let guard = conn.blocking_lock();
1476 let n = guard
1477 .execute(
1478 "UPDATE memory_chunks SET importance = ? WHERE id = ?",
1479 params![clamped, id],
1480 )
1481 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1482 if n == 0 {
1483 return Err(MemoryError::NotFound(format!("chunk {id}")));
1484 }
1485 Ok(())
1486 })
1487 .await
1488 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1489 }
1490
1491 async fn supersede(&self, old_id: &str, new_id: &str) -> MemoryResult<()> {
1492 self.invalidate_retrieve_cache();
1493 let conn = Arc::clone(&self.conn);
1494 let old = old_id.to_string();
1495 let new = new_id.to_string();
1496 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1497 let guard = conn.blocking_lock();
1498 let new_exists: i64 = guard
1502 .query_row(
1503 "SELECT COUNT(*) FROM memory_chunks WHERE id = ?",
1504 params![new],
1505 |r| r.get(0),
1506 )
1507 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1508 if new_exists == 0 {
1509 return Err(MemoryError::NotFound(format!("new chunk {new}")));
1510 }
1511 let n = guard
1512 .execute(
1513 "UPDATE memory_chunks SET superseded_by = ? WHERE id = ?",
1514 params![new, old],
1515 )
1516 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1517 if n == 0 {
1518 return Err(MemoryError::NotFound(format!("old chunk {old}")));
1519 }
1520 Ok(())
1521 })
1522 .await
1523 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1524 }
1525
1526 async fn record_access(
1527 &self,
1528 chunk_id: &str,
1529 retrieved_by: &str,
1530 used: bool,
1531 ) -> MemoryResult<()> {
1532 let conn = Arc::clone(&self.conn);
1533 let id = chunk_id.to_string();
1534 let by = retrieved_by.to_string();
1535 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1536 let guard = conn.blocking_lock();
1537 let exists: i64 = guard
1538 .query_row(
1539 "SELECT COUNT(*) FROM memory_chunks WHERE id = ?",
1540 params![id],
1541 |r| r.get(0),
1542 )
1543 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1544 if exists == 0 {
1545 return Err(MemoryError::NotFound(format!("chunk {id}")));
1546 }
1547 guard
1548 .execute(
1549 "INSERT INTO memory_access_log(ts, chunk_id, retrieved_by, query_hash, rank, used)
1550 VALUES (?, ?, ?, '', 0, ?)",
1551 params![now_ms(), id, by, if used { 1 } else { 0 }],
1552 )
1553 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1554 Ok(())
1555 })
1556 .await
1557 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1558 }
1559
1560 async fn delete(&self, chunk_id: &str, reason: EvictionReason) -> MemoryResult<()> {
1563 self.invalidate_retrieve_cache();
1564 let conn = Arc::clone(&self.conn);
1565 let id = chunk_id.to_string();
1566 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1567 let mut guard = conn.blocking_lock();
1568 let tx = guard
1569 .transaction()
1570 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1571 let n = tx
1572 .execute("DELETE FROM memory_chunks WHERE id = ?", params![id])
1573 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1574 if n == 0 {
1575 return Err(MemoryError::NotFound(format!("chunk {id}")));
1576 }
1577 tx.execute("DELETE FROM memory_vec WHERE chunk_id = ?", params![id])
1578 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1579 tx.execute(
1580 "INSERT INTO memory_eviction_log(ts, chunk_id, reason, metadata)
1581 VALUES (?,?,?, '{}')",
1582 params![now_ms(), id, eviction_reason_str(reason)],
1583 )
1584 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1585 tx.commit()
1586 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1587 Ok(())
1588 })
1589 .await
1590 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1591 }
1592
1593 async fn delete_matching(
1594 &self,
1595 predicate: MemoryPredicate,
1596 reason: EvictionReason,
1597 ) -> MemoryResult<usize> {
1598 if predicate.is_empty() {
1603 return Ok(0);
1604 }
1605 self.invalidate_retrieve_cache();
1606 let conn = Arc::clone(&self.conn);
1607 tokio::task::spawn_blocking(move || -> Result<usize, MemoryError> {
1608 let (where_clause, params_vec) = build_chunk_predicate(&predicate);
1609 let select_sql = format!("SELECT id FROM memory_chunks WHERE {}", where_clause);
1610 let mut guard = conn.blocking_lock();
1611 let tx = guard
1612 .transaction()
1613 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1614 let ids: Vec<String> = {
1615 let p: Vec<&dyn rusqlite::ToSql> = params_vec.iter().map(|b| b.as_ref()).collect();
1616 let mut stmt = tx
1617 .prepare(&select_sql)
1618 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1619 let rows = stmt
1620 .query_map(p.as_slice(), |r| r.get::<_, String>(0))
1621 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1622 let mut out = Vec::new();
1623 for r in rows {
1624 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
1625 }
1626 out
1627 };
1628 let now = now_ms();
1629 let reason_s = eviction_reason_str(reason);
1630 for id in &ids {
1631 tx.execute("DELETE FROM memory_chunks WHERE id = ?", params![id])
1632 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1633 tx.execute("DELETE FROM memory_vec WHERE chunk_id = ?", params![id])
1634 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1635 tx.execute(
1636 "INSERT INTO memory_eviction_log(ts, chunk_id, reason, metadata)
1637 VALUES (?, ?, ?, '{}')",
1638 params![now, id, reason_s],
1639 )
1640 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1641 }
1642 tx.commit()
1643 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1644 Ok(ids.len())
1645 })
1646 .await
1647 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1648 }
1649
1650 async fn purge_all(&self) -> MemoryResult<PurgeReport> {
1651 self.invalidate_retrieve_cache();
1652 let conn = Arc::clone(&self.conn);
1653 tokio::task::spawn_blocking(move || -> Result<PurgeReport, MemoryError> {
1654 let mut guard = conn.blocking_lock();
1655 let tx = guard
1656 .transaction()
1657 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1658 let chunks: i64 = tx
1659 .query_row("SELECT COUNT(*) FROM memory_chunks", [], |r| r.get(0))
1660 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1661 let sessions: i64 = tx
1662 .query_row("SELECT COUNT(*) FROM memory_sessions", [], |r| r.get(0))
1663 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1664 let access: i64 = tx
1665 .query_row("SELECT COUNT(*) FROM memory_access_log", [], |r| r.get(0))
1666 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1667 let evictions: i64 = tx
1668 .query_row("SELECT COUNT(*) FROM memory_eviction_log", [], |r| r.get(0))
1669 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1670
1671 tx.execute("DELETE FROM memory_chunks", [])
1672 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1673 tx.execute("DELETE FROM memory_vec", [])
1674 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1675 tx.execute("DELETE FROM memory_sessions", [])
1676 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1677 tx.execute("DELETE FROM memory_access_log", [])
1678 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1679 tx.execute("DELETE FROM memory_eviction_log", [])
1680 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1681 tx.execute("DELETE FROM memory_summary_members", [])
1682 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1683 tx.commit()
1684 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1685
1686 Ok(PurgeReport {
1687 chunks_deleted: chunks as usize,
1688 sessions_deleted: sessions as usize,
1689 access_log_deleted: access as usize,
1690 eviction_log_deleted: evictions as usize,
1691 })
1692 })
1693 .await
1694 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1695 }
1696
1697 async fn summarize_session(&self, session_id: &str) -> MemoryResult<MemoryChunk> {
1700 let summarizer = self.summarizer.clone().ok_or(MemoryError::NotImplemented(
1703 "SqliteMemoryProvider::summarize_session — no summarizer attached \
1704 (call `with_summarizer` first)",
1705 ))?;
1706
1707 let session = self
1711 .get_session(session_id)
1712 .await?
1713 .ok_or_else(|| MemoryError::NotFound(format!("session {session_id}")))?;
1714
1715 let members = self.fetch_session_chunks(session_id).await?;
1719 if members.is_empty() {
1720 return Err(MemoryError::InvalidArgument(format!(
1725 "session {session_id} has no chunks to summarize"
1726 )));
1727 }
1728 let member_ids: Vec<String> = members.iter().map(|c| c.id.clone()).collect();
1729
1730 let ctx = cel_memory::SummaryContext {
1733 kind_label: Some("session".into()),
1734 note: session
1735 .title
1736 .as_ref()
1737 .map(|t| format!("session title: {t}")),
1738 max_words: None,
1739 };
1740 let summary_text = summarizer
1741 .summarize(&members, &ctx)
1742 .await
1743 .map_err(|e| match e {
1744 cel_memory::SummarizerError::NoInput => MemoryError::InvalidArgument(
1745 "summarizer received no input despite session having chunks".into(),
1746 ),
1747 other => MemoryError::Provider(format!(
1748 "summarizer {} failed: {other}",
1749 summarizer.name()
1750 )),
1751 })?;
1752
1753 let new_chunk = NewMemoryChunk {
1760 kind: ChunkKind::JobSummary,
1761 source: ChunkSource::Embedded,
1762 session_id: Some(session_id.to_string()),
1763 project_root: members.iter().find_map(|c| c.project_root.clone()),
1764 caller_id: session.caller_id.clone(),
1765 content: summary_text,
1766 metadata: serde_json::json!({
1767 "session_id": session_id,
1768 "member_count": member_ids.len(),
1769 "summarizer": summarizer.name(),
1770 }),
1771 importance: None,
1775 shareable: false,
1776 pinned: false,
1777 };
1778 let written = self.write(new_chunk).await?;
1779
1780 let conn = Arc::clone(&self.conn);
1785 let summary_id = written.id.clone();
1786 let member_ids_for_insert = member_ids.clone();
1787 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1788 let mut guard = conn.blocking_lock();
1789 let tx = guard
1790 .transaction()
1791 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1792 for mid in &member_ids_for_insert {
1793 tx.execute(
1794 "INSERT OR IGNORE INTO memory_summary_members(rollup_id, member_id)
1795 VALUES (?, ?)",
1796 params![summary_id, mid],
1797 )
1798 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1799 }
1800 tx.commit()
1801 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1802 Ok(())
1803 })
1804 .await
1805 .map_err(|e| MemoryError::Internal(format!("join: {e}")))??;
1806
1807 let conn = Arc::clone(&self.conn);
1811 let sid = session_id.to_string();
1812 let stored_summary = written.content.clone();
1813 tokio::task::spawn_blocking(move || -> Result<(), MemoryError> {
1814 let guard = conn.blocking_lock();
1815 guard
1816 .execute(
1817 "UPDATE memory_sessions SET summary = ? WHERE id = ?",
1818 params![stored_summary, sid],
1819 )
1820 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1821 Ok(())
1822 })
1823 .await
1824 .map_err(|e| MemoryError::Internal(format!("join: {e}")))??;
1825
1826 Ok(written)
1827 }
1828
1829 async fn rollup_day(&self, date: NaiveDate) -> MemoryResult<Vec<MemoryChunk>> {
1830 self.rollup_day_inner(date, false).await
1831 }
1832
1833 async fn rollup_day_forced(&self, date: NaiveDate) -> MemoryResult<Vec<MemoryChunk>> {
1834 self.rollup_day_inner(date, true).await
1835 }
1836
1837 async fn rollup_rule_week(
1838 &self,
1839 rule_id: &str,
1840 week_start: NaiveDate,
1841 ) -> MemoryResult<MemoryChunk> {
1842 self.rollup_rule_week_inner(rule_id, week_start, false)
1843 .await
1844 }
1845
1846 async fn rollup_rule_week_forced(
1847 &self,
1848 rule_id: &str,
1849 week_start: NaiveDate,
1850 ) -> MemoryResult<MemoryChunk> {
1851 self.rollup_rule_week_inner(rule_id, week_start, true).await
1852 }
1853
1854 async fn run_aging_sweep(&self) -> MemoryResult<AgingReport> {
1857 const RETENTION_DAYS: i64 = 30;
1862 let cutoff_ms =
1863 (chrono::Utc::now() - chrono::Duration::days(RETENTION_DAYS)).timestamp_millis();
1864 self.invalidate_retrieve_cache();
1865 let conn = Arc::clone(&self.conn);
1866 tokio::task::spawn_blocking(move || -> Result<AgingReport, MemoryError> {
1867 let mut guard = conn.blocking_lock();
1868 let tx = guard
1869 .transaction()
1870 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1871 let ids: Vec<String> = {
1872 let mut stmt = tx
1873 .prepare(
1874 "SELECT id FROM memory_chunks
1875 WHERE pinned = 0
1876 AND kind != 'correction'
1877 AND created_at < ?",
1878 )
1879 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1880 let rows = stmt
1881 .query_map(params![cutoff_ms], |r| r.get::<_, String>(0))
1882 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1883 let mut out = Vec::new();
1884 for r in rows {
1885 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
1886 }
1887 out
1888 };
1889 let now = now_ms();
1890 let reason_s = eviction_reason_str(EvictionReason::Aging);
1891 for id in &ids {
1892 tx.execute("DELETE FROM memory_chunks WHERE id = ?", params![id])
1893 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1894 tx.execute("DELETE FROM memory_vec WHERE chunk_id = ?", params![id])
1895 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1896 tx.execute(
1897 "INSERT INTO memory_eviction_log(ts, chunk_id, reason, metadata)
1898 VALUES (?, ?, ?, '{}')",
1899 params![now, id, reason_s],
1900 )
1901 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1902 }
1903 tx.commit()
1904 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1905 let deleted = ids.len();
1906 Ok(AgingReport {
1907 tier_promoted: 0, deleted,
1909 bytes_reclaimed: 0,
1910 deletions_by_reason: vec![(EvictionReason::Aging, deleted)],
1911 })
1912 })
1913 .await
1914 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
1915 }
1916 async fn re_embed_all(&self, _target_model: &str) -> MemoryResult<ReEmbedReport> {
1917 Err(MemoryError::NotImplemented(
1918 "SqliteMemoryProvider::re_embed_all — Phase 4",
1919 ))
1920 }
1921 async fn export(&self, filter: ExportFilter) -> MemoryResult<ExportBundle> {
1922 let conn = Arc::clone(&self.conn);
1923 tokio::task::spawn_blocking(move || -> Result<ExportBundle, MemoryError> {
1924 let guard = conn.blocking_lock();
1925
1926 let (where_clause, params_vec) = match &filter.predicate {
1928 Some(p) if !p.is_empty() => build_chunk_predicate(p),
1929 _ => ("1=1".to_string(), Vec::new()),
1930 };
1931 let select_sql = format!(
1932 "SELECT id, created_at, kind, tier, source, session_id,
1933 project_root, caller_id, content, metadata,
1934 importance, pinned, shareable, superseded_by,
1935 embedding_model, embedding_dim
1936 FROM memory_chunks WHERE {} ORDER BY created_at DESC",
1937 where_clause
1938 );
1939 let chunks: Vec<MemoryChunk> = {
1940 let p: Vec<&dyn rusqlite::ToSql> = params_vec.iter().map(|b| b.as_ref()).collect();
1941 let mut stmt = guard
1942 .prepare(&select_sql)
1943 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1944 let rows = stmt
1945 .query_map(p.as_slice(), row_to_chunk)
1946 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1947 let mut out = Vec::new();
1948 for r in rows {
1949 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
1950 }
1951 out
1952 };
1953
1954 let sessions = if filter.include_sessions {
1957 let session_ids: std::collections::HashSet<String> =
1958 chunks.iter().filter_map(|c| c.session_id.clone()).collect();
1959 if session_ids.is_empty() {
1960 Vec::new()
1961 } else {
1962 let placeholders = vec!["?"; session_ids.len()].join(",");
1963 let sql = format!(
1964 "SELECT id, started_at, ended_at, caller_id, title, summary,
1965 outcome, metadata
1966 FROM memory_sessions WHERE id IN ({placeholders})"
1967 );
1968 let ids: Vec<String> = session_ids.into_iter().collect();
1969 let p: Vec<&dyn rusqlite::ToSql> =
1970 ids.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
1971 let mut stmt = guard
1972 .prepare(&sql)
1973 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1974 let rows = stmt
1975 .query_map(p.as_slice(), |r| {
1976 let metadata_str: String = r.get("metadata")?;
1977 let metadata: serde_json::Value = serde_json::from_str(&metadata_str)
1978 .unwrap_or(serde_json::Value::Null);
1979 Ok(MemorySession {
1980 id: r.get("id")?,
1981 started_at: ms_to_dt(r.get::<_, i64>("started_at")?),
1982 ended_at: r.get::<_, Option<i64>>("ended_at")?.map(ms_to_dt),
1983 caller_id: r.get("caller_id")?,
1984 title: r.get("title")?,
1985 summary: r.get("summary")?,
1986 outcome: str_to_outcome(&r.get::<_, String>("outcome")?)
1987 .unwrap_or(SessionOutcome::Aborted),
1988 metadata,
1989 })
1990 })
1991 .map_err(|e| MemoryError::Storage(e.to_string()))?;
1992 let mut out = Vec::new();
1993 for r in rows {
1994 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
1995 }
1996 out
1997 }
1998 } else {
1999 Vec::new()
2000 };
2001
2002 let evictions = if filter.include_eviction_log {
2003 let mut stmt = guard
2004 .prepare(
2005 "SELECT ts, chunk_id, reason, metadata
2006 FROM memory_eviction_log ORDER BY ts DESC",
2007 )
2008 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2009 let rows = stmt
2010 .query_map([], |r| {
2011 let reason_s: String = r.get("reason")?;
2012 let metadata_s: String = r.get("metadata")?;
2013 let metadata: serde_json::Value =
2014 serde_json::from_str(&metadata_s).unwrap_or(serde_json::Value::Null);
2015 Ok(EvictionEntry {
2016 ts: ms_to_dt(r.get::<_, i64>("ts")?),
2017 chunk_id: r.get("chunk_id")?,
2018 reason: str_to_eviction_reason(&reason_s)
2019 .unwrap_or(EvictionReason::UserDelete),
2020 metadata,
2021 })
2022 })
2023 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2024 let mut out = Vec::new();
2025 for r in rows {
2026 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
2027 }
2028 out
2029 } else {
2030 Vec::new()
2031 };
2032
2033 let accesses = if filter.include_access_log {
2034 let mut stmt = guard
2035 .prepare(
2036 "SELECT ts, chunk_id, retrieved_by, query_hash, rank, used
2037 FROM memory_access_log ORDER BY ts DESC",
2038 )
2039 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2040 let rows = stmt
2041 .query_map([], |r| {
2042 Ok(AccessEntry {
2043 ts: ms_to_dt(r.get::<_, i64>("ts")?),
2044 chunk_id: r.get("chunk_id")?,
2045 retrieved_by: r.get("retrieved_by")?,
2046 query_hash: r.get("query_hash")?,
2047 rank: r.get::<_, i64>("rank")? as usize,
2048 used: r.get::<_, i64>("used")? != 0,
2049 })
2050 })
2051 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2052 let mut out = Vec::new();
2053 for r in rows {
2054 out.push(r.map_err(|e| MemoryError::Storage(e.to_string()))?);
2055 }
2056 out
2057 } else {
2058 Vec::new()
2059 };
2060
2061 Ok(ExportBundle {
2062 chunks,
2063 sessions,
2064 evictions,
2065 accesses,
2066 })
2067 })
2068 .await
2069 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
2070 }
2071
2072 async fn stats(&self) -> MemoryResult<MemoryStats> {
2073 let conn = Arc::clone(&self.conn);
2074 let model = self.embedder.model_name().to_string();
2075 tokio::task::spawn_blocking(move || -> Result<MemoryStats, MemoryError> {
2076 let guard = conn.blocking_lock();
2077 let total: i64 = guard
2078 .query_row("SELECT COUNT(*) FROM memory_chunks", [], |r| r.get(0))
2079 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2080 let session_tier: i64 = guard
2081 .query_row(
2082 "SELECT COUNT(*) FROM memory_chunks WHERE tier = 'session'",
2083 [],
2084 |r| r.get(0),
2085 )
2086 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2087 let lt_tier: i64 = guard
2088 .query_row(
2089 "SELECT COUNT(*) FROM memory_chunks WHERE tier = 'long_term'",
2090 [],
2091 |r| r.get(0),
2092 )
2093 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2094 let total_sessions: i64 = guard
2095 .query_row("SELECT COUNT(*) FROM memory_sessions", [], |r| r.get(0))
2096 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2097 let open: i64 = guard
2098 .query_row(
2099 "SELECT COUNT(*) FROM memory_sessions WHERE outcome = 'open'",
2100 [],
2101 |r| r.get(0),
2102 )
2103 .map_err(|e| MemoryError::Storage(e.to_string()))?;
2104 Ok(MemoryStats {
2105 total_chunks: total as usize,
2106 session_chunks: session_tier as usize,
2107 long_term_chunks: lt_tier as usize,
2108 total_sessions: total_sessions as usize,
2109 open_sessions: open as usize,
2110 db_bytes: 0, embedding_model: Some(model),
2112 })
2113 })
2114 .await
2115 .map_err(|e| MemoryError::Internal(format!("join: {e}")))?
2116 }
2117}
2118
2119fn retrieval_weights(profile: cel_memory::RetrievalProfile) -> (f32, f32, f32, f32) {
2123 use cel_memory::RetrievalProfile::*;
2124 match profile {
2125 AgentChatTurn => (0.55, 0.30, 0.15, 7.0 * 86400.0),
2128 AgentDelegatedJob => (0.55, 0.30, 0.15, 30.0 * 86400.0),
2130 NLCompilerSimilarRules => (0.40, 0.40, 0.20, 30.0 * 86400.0),
2133 NLCompilerSimilarFires => (0.40, 0.40, 0.20, 30.0 * 86400.0),
2136 AuditTimeline => (0.30, 0.50, 0.20, 90.0 * 86400.0),
2138 UserSearch => (0.40, 0.50, 0.10, 365.0 * 86400.0),
2140 }
2141}
2142
2143fn fts_query_escape(raw: &str) -> String {
2148 let escaped = raw.replace('"', "\"\"");
2149 format!("\"{escaped}\"")
2150}
2151
2152fn chunk_matches_query(c: &MemoryChunk, q: &MemoryQuery) -> bool {
2155 if let Some(kinds) = &q.kinds {
2157 if !kinds.contains(&c.kind) {
2158 return false;
2159 }
2160 }
2161 if !q.include_rollups && c.kind == cel_memory::ChunkKind::Rollup {
2163 return false;
2164 }
2165 if let Some(since) = q.since {
2167 if c.created_at < since {
2168 return false;
2169 }
2170 }
2171 if let Some(until) = q.until {
2172 if c.created_at > until {
2173 return false;
2174 }
2175 }
2176 if let Some(sid) = &q.session_id {
2178 if c.session_id.as_deref() != Some(sid.as_str()) {
2179 return false;
2180 }
2181 }
2182 if let Some(prefix) = &q.project_root_prefix {
2184 match &c.project_root {
2185 Some(root) if root.starts_with(prefix.as_str()) => {}
2186 _ => return false,
2187 }
2188 }
2189 if let Some(min) = q.min_importance {
2191 if c.importance < min {
2192 return false;
2193 }
2194 }
2195 match q.caller_scope {
2201 cel_memory::CallerScope::Own => {
2202 if c.caller_id != q.caller_id {
2203 return false;
2204 }
2205 }
2206 cel_memory::CallerScope::OwnPlusShared => {
2207 if c.caller_id != q.caller_id && !c.shareable {
2208 return false;
2209 }
2210 }
2211 cel_memory::CallerScope::Global => {}
2212 }
2213 true
2214}
2215
2216fn build_chunk_predicate(p: &MemoryPredicate) -> (String, Vec<Box<dyn rusqlite::ToSql>>) {
2223 let mut clauses: Vec<String> = vec!["1=1".to_string()];
2224 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
2225 if let Some(kinds) = &p.kinds {
2226 let placeholders = vec!["?"; kinds.len()].join(",");
2227 clauses.push(format!("kind IN ({placeholders})"));
2228 for k in kinds {
2229 params.push(Box::new(kind_str(*k).to_string()));
2230 }
2231 }
2232 if let Some(callers) = &p.callers {
2233 let placeholders = vec!["?"; callers.len()].join(",");
2234 clauses.push(format!("caller_id IN ({placeholders})"));
2235 for c in callers {
2236 params.push(Box::new(c.clone()));
2237 }
2238 }
2239 if let Some(sids) = &p.session_ids {
2240 let placeholders = vec!["?"; sids.len()].join(",");
2241 clauses.push(format!("session_id IN ({placeholders})"));
2242 for s in sids {
2243 params.push(Box::new(s.clone()));
2244 }
2245 }
2246 if let Some(prefix) = &p.project_root_prefix {
2247 clauses.push("project_root LIKE ?".to_string());
2248 params.push(Box::new(format!("{prefix}%")));
2249 }
2250 if let Some(before) = p.before {
2251 clauses.push("created_at < ?".to_string());
2252 params.push(Box::new(dt_to_ms(before)));
2253 }
2254 if let Some(after) = p.after {
2255 clauses.push("created_at > ?".to_string());
2256 params.push(Box::new(dt_to_ms(after)));
2257 }
2258 if let Some(pinned) = p.pinned {
2259 clauses.push("pinned = ?".to_string());
2260 params.push(Box::new(if pinned { 1_i64 } else { 0 }));
2261 }
2262 if let Some(below) = p.importance_below {
2263 clauses.push("importance < ?".to_string());
2264 params.push(Box::new(below as f64));
2265 }
2266 if let Some(needle) = &p.content_contains {
2267 clauses.push("LOWER(content) LIKE ?".to_string());
2268 params.push(Box::new(format!("%{}%", needle.to_lowercase())));
2269 }
2270 (clauses.join(" AND "), params)
2271}
2272
2273fn str_to_eviction_reason(s: &str) -> Result<EvictionReason, MemoryError> {
2277 Ok(match s {
2278 "user_delete" => EvictionReason::UserDelete,
2279 "aging" => EvictionReason::Aging,
2280 "low_importance" => EvictionReason::LowImportance,
2281 "redact_rule" => EvictionReason::RedactRule,
2282 "storage_cap" => EvictionReason::StorageCap,
2283 "purge_all" => EvictionReason::PurgeAll,
2284 other => {
2285 return Err(MemoryError::Storage(format!(
2286 "unknown eviction reason: {other}"
2287 )))
2288 }
2289 })
2290}
2291
2292fn eviction_reason_str(r: EvictionReason) -> &'static str {
2293 match r {
2294 EvictionReason::UserDelete => "user_delete",
2295 EvictionReason::Aging => "aging",
2296 EvictionReason::LowImportance => "low_importance",
2297 EvictionReason::RedactRule => "redact_rule",
2298 EvictionReason::StorageCap => "storage_cap",
2299 EvictionReason::PurgeAll => "purge_all",
2300 }
2301}
2302
2303#[allow(dead_code)]
2305fn _unused_imports_anchor(_: AccessEntry, _: EvictionEntry, _: CallerScope) {}