Skip to main content

codei_session/store/
mod.rs

1mod json;
2mod sqlite;
3
4use std::path::{Path, PathBuf};
5
6use codei_config::{expand_tilde, SessionConfig, SessionStorage};
7
8use crate::error::SessionError;
9use crate::model::Session;
10
11use json::JsonSessionStore;
12use sqlite::SqliteSessionStore;
13
14enum Backend {
15    Sqlite(SqliteSessionStore),
16    Json(JsonSessionStore),
17}
18
19/// Persistent session storage (SQLite or JSONL files, selected by config).
20pub struct SessionStore {
21    backend: Backend,
22}
23
24impl SessionStore {
25    /// Open storage using `[session]` config (`storage` + `dir`).
26    pub fn open_for_config(config: &SessionConfig) -> Result<Self, SessionError> {
27        let dir = expand_tilde(&config.dir);
28        let backend = match config.storage {
29            SessionStorage::Sqlite => {
30                Backend::Sqlite(SqliteSessionStore::open(&resolve_sqlite_path(&dir))?)
31            }
32            SessionStorage::Json => Backend::Json(JsonSessionStore::open(&dir)?),
33        };
34        Ok(Self { backend })
35    }
36
37    /// Open with default session config (SQLite under `~/.local/share/codei/sessions/`).
38    pub fn open_default() -> Result<Self, SessionError> {
39        Self::open_for_config(&SessionConfig::default())
40    }
41
42    /// Open a SQLite database at an explicit path (for tests and ephemeral sub-agents).
43    pub fn open(path: &Path) -> Result<Self, SessionError> {
44        Ok(Self {
45            backend: Backend::Sqlite(SqliteSessionStore::open(path)?),
46        })
47    }
48
49    pub fn save(&self, session: &Session) -> Result<(), SessionError> {
50        match &self.backend {
51            Backend::Sqlite(store) => store.save(session),
52            Backend::Json(store) => store.save(session),
53        }
54    }
55
56    pub fn load(&self, id: &str) -> Result<Session, SessionError> {
57        match &self.backend {
58            Backend::Sqlite(store) => store.load(id),
59            Backend::Json(store) => store.load(id),
60        }
61    }
62
63    pub fn list(&self, limit: usize) -> Result<Vec<Session>, SessionError> {
64        match &self.backend {
65            Backend::Sqlite(store) => store.list(limit),
66            Backend::Json(store) => store.list(limit),
67        }
68    }
69
70    pub fn latest(&self) -> Result<Option<Session>, SessionError> {
71        match &self.backend {
72            Backend::Sqlite(store) => store.latest(),
73            Backend::Json(store) => store.latest(),
74        }
75    }
76
77    pub fn delete(&self, id: &str) -> Result<(), SessionError> {
78        match &self.backend {
79            Backend::Sqlite(store) => store.delete(id),
80            Backend::Json(store) => store.delete(id),
81        }
82    }
83
84    pub fn export_jsonl(&self, id: &str) -> Result<String, SessionError> {
85        let session = self.load(id)?;
86        let mut lines = Vec::new();
87        for msg in &session.messages {
88            let line = serde_json::json!({
89                "id": msg.id,
90                "role": msg.role,
91                "content": msg.content,
92                "tool_calls": msg.tool_calls,
93                "tool_call_id": msg.tool_call_id,
94                "created_at": msg.created_at,
95            });
96            lines.push(serde_json::to_string(&line)?);
97        }
98        Ok(lines.join("\n"))
99    }
100}
101
102/// SQLite DB path: `{dir}/sessions.db`, with legacy fallback.
103fn resolve_sqlite_path(dir: &Path) -> PathBuf {
104    let primary = dir.join("sessions.db");
105    if primary.exists() {
106        return primary;
107    }
108    let legacy = expand_tilde("~/.local/share/codei/sessions.db");
109    if legacy.exists() {
110        return legacy;
111    }
112    primary
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::model::Session;
119    use codei_config::SessionStorage;
120
121    #[test]
122    fn open_json_backend() {
123        let dir = tempfile::tempdir().unwrap();
124        let config = SessionConfig {
125            storage: SessionStorage::Json,
126            dir: dir.path().to_string_lossy().into_owned(),
127        };
128        let store = SessionStore::open_for_config(&config).unwrap();
129        let mut session = Session::new(dir.path().to_path_buf());
130        session.push_user("hello");
131        store.save(&session).unwrap();
132        assert_eq!(store.load(&session.id).unwrap().messages.len(), 1);
133    }
134}