Skip to main content

offline_intelligence/memory_db/
session_summaries_store.rs

1//! Storage for the single cumulative pre-clear summary per session.
2//!
3//! There is exactly ONE row per session in `session_summaries`.
4//! On every KV cache clear the row is replaced — never appended — so the summary
5//! always covers the full conversation history from session start to the last clear.
6
7use std::sync::Arc;
8use r2d2::Pool;
9use r2d2_sqlite::SqliteConnectionManager;
10use tracing::debug;
11
12use crate::memory_db::schema::SessionSummary;
13
14pub struct SessionSummariesStore {
15    pool: Arc<Pool<SqliteConnectionManager>>,
16}
17
18impl SessionSummariesStore {
19    pub fn new(pool: Arc<Pool<SqliteConnectionManager>>) -> Self {
20        Self { pool }
21    }
22
23    /// Replace the session's summary with an updated one.
24    /// Uses INSERT OR REPLACE so there is always exactly one row per session.
25    /// `clear_count` is incremented by fetching the current value first.
26    pub fn upsert(
27        &self,
28        session_id: &str,
29        summary_text: &str,
30        token_count: i32,
31        total_message_count: i32,
32    ) -> anyhow::Result<()> {
33        let conn = self.pool.get()?;
34
35        // Read the current clear_count so we can increment it
36        let existing_clear_count: i32 = conn
37            .query_row(
38                "SELECT clear_count FROM session_summaries WHERE session_id = ?1",
39                [session_id],
40                |row| row.get(0),
41            )
42            .unwrap_or(0);
43
44        conn.execute(
45            "INSERT OR REPLACE INTO session_summaries
46             (session_id, summary_text, token_count, total_message_count, clear_count, last_updated)
47             VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP)",
48            rusqlite::params![
49                session_id,
50                summary_text,
51                token_count,
52                total_message_count,
53                existing_clear_count + 1,
54            ],
55        )?;
56
57        debug!(
58            "Upserted cumulative summary for session {} (clear #{}, {} tokens)",
59            session_id,
60            existing_clear_count + 1,
61            token_count
62        );
63        Ok(())
64    }
65
66    /// Retrieve the single summary for a session, if one exists.
67    pub fn get(&self, session_id: &str) -> anyhow::Result<Option<SessionSummary>> {
68        let conn = self.pool.get()?;
69        let mut stmt = conn.prepare(
70            "SELECT session_id, summary_text, token_count, total_message_count,
71                    clear_count, last_updated
72             FROM session_summaries
73             WHERE session_id = ?1",
74        )?;
75
76        let mut rows = stmt.query([session_id])?;
77        if let Some(row) = rows.next()? {
78            let last_updated_str: String = row.get(5)?;
79            let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
80                .map_err(|e| anyhow::anyhow!("Failed to parse timestamp: {}", e))?
81                .with_timezone(&chrono::Utc);
82
83            return Ok(Some(SessionSummary {
84                session_id: row.get(0)?,
85                summary_text: row.get(1)?,
86                token_count: row.get(2)?,
87                total_message_count: row.get(3)?,
88                clear_count: row.get(4)?,
89                last_updated,
90            }));
91        }
92
93        Ok(None)
94    }
95
96    /// Delete the summary for a session (used during full session cleanup).
97    pub fn delete_for_session(&self, session_id: &str) -> anyhow::Result<usize> {
98        let conn = self.pool.get()?;
99        let deleted = conn.execute(
100            "DELETE FROM session_summaries WHERE session_id = ?1",
101            [session_id],
102        )?;
103        Ok(deleted)
104    }
105}