Skip to main content

offline_intelligence/memory_db/
session_file_contexts_store.rs

1//! Session file contexts store.
2//!
3//! Persists references to all files ever attached during a conversation session.
4//! This enables "sticky" file context: once a user attaches a file, the LLM
5//! sees its content for the entire conversation — matching ChatGPT, Gemini, etc.
6
7use anyhow::Result;
8use r2d2::Pool;
9use r2d2_sqlite::SqliteConnectionManager;
10use std::sync::Arc;
11use tracing::{debug, info};
12
13/// A single file attachment reference stored for a session.
14#[derive(Debug, Clone)]
15pub struct SessionFileContext {
16    pub id: i64,
17    pub session_id: String,
18    pub file_name: String,
19    /// "inline" or "local_storage"
20    pub source: String,
21    /// OS file path — set when source == "inline"
22    pub file_path: Option<String>,
23    /// all_files table ID — set when source == "local_storage"
24    pub all_files_id: Option<i64>,
25    pub size_bytes: Option<i64>,
26}
27
28/// Lightweight attachment reference passed to the store. Avoids a circular
29/// dependency between `memory_db` and `api::stream_api`.
30pub struct AttachmentRef<'a> {
31    pub name: &'a str,
32    pub source: &'a str,
33    pub file_path: Option<&'a str>,
34    pub all_files_id: Option<i64>,
35    pub size_bytes: Option<i64>,
36}
37
38pub struct SessionFileContextsStore {
39    pool: Arc<Pool<SqliteConnectionManager>>,
40}
41
42impl SessionFileContextsStore {
43    pub fn new(pool: Arc<Pool<SqliteConnectionManager>>) -> Self {
44        Self { pool }
45    }
46
47    /// Store a set of attachment references for a session.
48    /// Deduplicates by (session_id, file_name, source) — avoids duplicate rows
49    /// when the same file is attached multiple times in the same session.
50    pub fn store_attachments(
51        &self,
52        session_id: &str,
53        attachments: &[AttachmentRef<'_>],
54    ) -> Result<()> {
55        let conn = self.pool.get()?;
56        for att in attachments {
57            conn.execute(
58                "INSERT OR IGNORE INTO session_file_contexts
59                 (session_id, file_name, source, file_path, all_files_id, size_bytes)
60                 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
61                rusqlite::params![
62                    session_id,
63                    att.name,
64                    att.source,
65                    att.file_path,
66                    att.all_files_id,
67                    att.size_bytes,
68                ],
69            )?;
70        }
71        debug!(
72            "Stored {} attachment reference(s) for session {}",
73            attachments.len(),
74            session_id
75        );
76        Ok(())
77    }
78
79    /// Return all file references stored for a session, ordered by attach time.
80    pub fn get_for_session(&self, session_id: &str) -> Result<Vec<SessionFileContext>> {
81        let conn = self.pool.get()?;
82        let mut stmt = conn.prepare(
83            "SELECT id, session_id, file_name, source, file_path, all_files_id, size_bytes
84             FROM session_file_contexts
85             WHERE session_id = ?1
86             ORDER BY attached_at ASC",
87        )?;
88
89        let rows = stmt.query_map([session_id], |row| {
90            Ok(SessionFileContext {
91                id: row.get(0)?,
92                session_id: row.get(1)?,
93                file_name: row.get(2)?,
94                source: row.get(3)?,
95                file_path: row.get(4)?,
96                all_files_id: row.get(5)?,
97                size_bytes: row.get(6)?,
98            })
99        })?;
100
101        let mut results = Vec::new();
102        for row in rows {
103            results.push(row?);
104        }
105
106        info!(
107            "Retrieved {} historical attachment(s) for session {}",
108            results.len(),
109            session_id
110        );
111        Ok(results)
112    }
113
114    /// Remove all file contexts for a session (e.g., when the conversation is deleted).
115    pub fn delete_for_session(&self, session_id: &str) -> Result<usize> {
116        let conn = self.pool.get()?;
117        let n = conn.execute(
118            "DELETE FROM session_file_contexts WHERE session_id = ?1",
119            [session_id],
120        )?;
121        Ok(n)
122    }
123}