1use super::*;
2
3#[derive(Debug, Clone, Default)]
4pub struct SessionSettings {
5 pub label: Option<String>,
6 pub thinking_level: Option<String>,
7 pub verbose_level: Option<String>,
8 pub reasoning_level: Option<String>,
9}
10
11pub type SessionMetaRow = (String, String, Option<String>, Option<i64>);
12
13pub type SessionTreeRow = (i64, Option<String>, Option<i64>, String);
14
15impl Database {
16 pub fn save_session(&self, chat_id: i64, messages_json: &str) -> Result<(), MicroClawError> {
17 self.save_session_with_meta(chat_id, messages_json, None, None, None)
18 }
19
20 pub fn save_session_with_meta(
21 &self,
22 chat_id: i64,
23 messages_json: &str,
24 parent_session_key: Option<&str>,
25 fork_point: Option<i64>,
26 skill_envs_json: Option<&str>,
27 ) -> Result<(), MicroClawError> {
28 let conn = self.lock_conn();
29 let now = chrono::Utc::now().to_rfc3339();
30 conn.execute(
31 "INSERT INTO sessions (chat_id, messages_json, updated_at, parent_session_key, fork_point, skill_envs_json)
32 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
33 ON CONFLICT(chat_id) DO UPDATE SET
34 messages_json = ?2,
35 updated_at = ?3,
36 parent_session_key = COALESCE(?4, parent_session_key),
37 fork_point = COALESCE(?5, fork_point),
38 skill_envs_json = COALESCE(?6, skill_envs_json)",
39 params![chat_id, messages_json, now, parent_session_key, fork_point, skill_envs_json],
40 )?;
41 Ok(())
42 }
43
44 pub fn load_session(&self, chat_id: i64) -> Result<Option<(String, String)>, MicroClawError> {
45 let conn = self.lock_conn();
46 let result = conn.query_row(
47 "SELECT messages_json, updated_at FROM sessions WHERE chat_id = ?1",
48 params![chat_id],
49 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
50 );
51 match result {
52 Ok(pair) => Ok(Some(pair)),
53 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
54 Err(e) => Err(e.into()),
55 }
56 }
57
58 pub fn set_session_label(&self, chat_id: i64, label: &str) -> Result<(), MicroClawError> {
61 let conn = self.lock_conn();
62 conn.execute(
63 "UPDATE sessions SET label = ?1 WHERE chat_id = ?2",
64 params![label, chat_id],
65 )?;
66 Ok(())
67 }
68
69 pub fn get_session_label_and_length(
72 &self,
73 chat_id: i64,
74 ) -> Result<Option<(Option<String>, usize)>, MicroClawError> {
75 let conn = self.lock_conn();
76 let result = conn
77 .query_row(
78 "SELECT label, messages_json FROM sessions WHERE chat_id = ?1",
79 params![chat_id],
80 |row| {
81 let label: Option<String> = row.get(0)?;
82 let json: String = row.get(1)?;
83 Ok((label, json))
84 },
85 )
86 .optional()?;
87 let Some((label, json)) = result else {
88 return Ok(None);
89 };
90 let count = serde_json::from_str::<Vec<serde_json::Value>>(&json)
91 .map(|v| v.len())
92 .unwrap_or(0);
93 Ok(Some((label, count)))
94 }
95
96 pub fn save_session_settings(
97 &self,
98 chat_id: i64,
99 settings: &SessionSettings,
100 ) -> Result<(), MicroClawError> {
101 let conn = self.lock_conn();
102 let now = chrono::Utc::now().to_rfc3339();
103 conn.execute(
104 "INSERT INTO sessions (
105 chat_id,
106 messages_json,
107 updated_at,
108 label,
109 thinking_level,
110 verbose_level,
111 reasoning_level
112 )
113 VALUES (?1, '[]', ?2, ?3, ?4, ?5, ?6)
114 ON CONFLICT(chat_id) DO UPDATE SET
115 updated_at = excluded.updated_at,
116 label = COALESCE(excluded.label, sessions.label),
117 thinking_level = COALESCE(excluded.thinking_level, sessions.thinking_level),
118 verbose_level = COALESCE(excluded.verbose_level, sessions.verbose_level),
119 reasoning_level = COALESCE(excluded.reasoning_level, sessions.reasoning_level)",
120 params![
121 chat_id,
122 now,
123 settings.label,
124 settings.thinking_level,
125 settings.verbose_level,
126 settings.reasoning_level
127 ],
128 )?;
129 Ok(())
130 }
131
132 pub fn load_session_settings(
133 &self,
134 chat_id: i64,
135 ) -> Result<Option<SessionSettings>, MicroClawError> {
136 let conn = self.lock_conn();
137 let result = conn.query_row(
138 "SELECT label, thinking_level, verbose_level, reasoning_level
139 FROM sessions
140 WHERE chat_id = ?1",
141 params![chat_id],
142 |row| {
143 Ok(SessionSettings {
144 label: row.get::<_, Option<String>>(0)?,
145 thinking_level: row.get::<_, Option<String>>(1)?,
146 verbose_level: row.get::<_, Option<String>>(2)?,
147 reasoning_level: row.get::<_, Option<String>>(3)?,
148 })
149 },
150 );
151 match result {
152 Ok(settings) => Ok(Some(settings)),
153 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
154 Err(e) => Err(e.into()),
155 }
156 }
157
158 pub fn load_session_meta(
159 &self,
160 chat_id: i64,
161 ) -> Result<Option<SessionMetaRow>, MicroClawError> {
162 let conn = self.lock_conn();
163 let result = conn.query_row(
164 "SELECT messages_json, updated_at, parent_session_key, fork_point
165 FROM sessions WHERE chat_id = ?1",
166 params![chat_id],
167 |row| {
168 Ok((
169 row.get::<_, String>(0)?,
170 row.get::<_, String>(1)?,
171 row.get::<_, Option<String>>(2)?,
172 row.get::<_, Option<i64>>(3)?,
173 ))
174 },
175 );
176 match result {
177 Ok(v) => Ok(Some(v)),
178 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
179 Err(e) => Err(e.into()),
180 }
181 }
182
183 pub fn list_session_meta(&self, limit: usize) -> Result<Vec<SessionTreeRow>, MicroClawError> {
184 let conn = self.lock_conn();
185 let mut stmt = conn.prepare(
186 "SELECT chat_id, parent_session_key, fork_point, updated_at
187 FROM sessions
188 ORDER BY updated_at DESC
189 LIMIT ?1",
190 )?;
191 let rows = stmt
192 .query_map(params![limit as i64], |row| {
193 Ok((
194 row.get::<_, i64>(0)?,
195 row.get::<_, Option<String>>(1)?,
196 row.get::<_, Option<i64>>(2)?,
197 row.get::<_, String>(3)?,
198 ))
199 })?
200 .collect::<Result<Vec<_>, _>>()?;
201 Ok(rows)
202 }
203
204 pub fn delete_session(&self, chat_id: i64) -> Result<bool, MicroClawError> {
205 let conn = self.lock_conn();
206 let rows = conn.execute("DELETE FROM sessions WHERE chat_id = ?1", params![chat_id])?;
207 Ok(rows > 0)
208 }
209}
210
211impl Database {
212 pub fn save_session_skill_envs(
213 &self,
214 chat_id: i64,
215 skill_envs_json: &str,
216 ) -> Result<(), MicroClawError> {
217 let conn = self.lock_conn();
218 conn.execute(
219 "UPDATE sessions SET skill_envs_json = ?2 WHERE chat_id = ?1",
220 params![chat_id, skill_envs_json],
221 )?;
222 Ok(())
223 }
224
225 pub fn load_session_skill_envs(&self, chat_id: i64) -> Result<Option<String>, MicroClawError> {
226 let conn = self.lock_conn();
227 let result = conn.query_row(
228 "SELECT skill_envs_json FROM sessions WHERE chat_id = ?1",
229 params![chat_id],
230 |row| row.get::<_, Option<String>>(0),
231 );
232 match result {
233 Ok(v) => Ok(v),
234 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
235 Err(e) => Err(e.into()),
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::internal::storage::db::test_support::*;
244
245 #[test]
246 fn test_save_and_load_session() {
247 let (db, dir) = test_db();
248 let json = r#"[{"role":"user","content":"hello"}]"#;
249 db.save_session(100, json).unwrap();
250
251 let result = db.load_session(100).unwrap();
252 assert!(result.is_some());
253 let (loaded_json, updated_at) = result.unwrap();
254 assert_eq!(loaded_json, json);
255 assert!(!updated_at.is_empty());
256
257 let json2 = r#"[{"role":"user","content":"hello"},{"role":"assistant","content":"hi"}]"#;
259 db.save_session(100, json2).unwrap();
260 let (loaded_json2, _) = db.load_session(100).unwrap().unwrap();
261 assert_eq!(loaded_json2, json2);
262
263 cleanup(&dir);
264 }
265
266 #[test]
267 fn test_save_and_load_session_settings() {
268 let (db, dir) = test_db();
269 let settings = SessionSettings {
270 label: Some("Ops".into()),
271 thinking_level: Some("high".into()),
272 verbose_level: Some("full".into()),
273 reasoning_level: Some("stream".into()),
274 };
275 db.save_session_settings(100, &settings).unwrap();
276
277 let loaded = db.load_session_settings(100).unwrap().unwrap();
278 assert_eq!(loaded.label.as_deref(), Some("Ops"));
279 assert_eq!(loaded.thinking_level.as_deref(), Some("high"));
280 assert_eq!(loaded.verbose_level.as_deref(), Some("full"));
281 assert_eq!(loaded.reasoning_level.as_deref(), Some("stream"));
282
283 cleanup(&dir);
284 }
285
286 #[test]
287 fn test_load_session_nonexistent() {
288 let (db, dir) = test_db();
289 let result = db.load_session(999).unwrap();
290 assert!(result.is_none());
291 cleanup(&dir);
292 }
293
294 #[test]
295 fn test_delete_session() {
296 let (db, dir) = test_db();
297 db.save_session(100, "[]").unwrap();
298 assert!(db.delete_session(100).unwrap());
299 assert!(db.load_session(100).unwrap().is_none());
300 assert!(!db.delete_session(100).unwrap());
302 cleanup(&dir);
303 }
304}