1use rusqlite::params;
2
3use crate::error::{Error, Result};
4use crate::store::Store;
5use crate::types::Session;
6
7impl Store {
8 pub fn session_start(&self, project: &str, directory: Option<&str>) -> Result<Session> {
9 let existing: Option<Session> = self
11 .conn()
12 .query_row(
13 "SELECT id, project, directory, started_at, ended_at, summary, status
14 FROM sessions WHERE project = ?1 AND directory IS ?2 AND status = 'active'
15 ORDER BY started_at DESC LIMIT 1",
16 params![project, directory],
17 |row| {
18 Ok(Session {
19 id: row.get(0)?,
20 project: row.get(1)?,
21 directory: row.get(2)?,
22 started_at: row.get(3)?,
23 ended_at: row.get(4)?,
24 summary: row.get(5)?,
25 status: row.get(6)?,
26 })
27 },
28 )
29 .ok();
30
31 if let Some(session) = existing {
32 return Ok(session);
33 }
34
35 let completed: Option<String> = self
37 .conn()
38 .query_row(
39 "SELECT id FROM sessions
40 WHERE project = ?1 AND directory IS ?2 AND status = 'completed'
41 ORDER BY started_at DESC LIMIT 1",
42 params![project, directory],
43 |row| row.get(0),
44 )
45 .ok();
46
47 if let Some(existing_id) = completed {
48 self.conn().execute(
49 "UPDATE sessions SET status = 'active', ended_at = NULL, summary = NULL
50 WHERE id = ?1",
51 params![existing_id],
52 )?;
53 return self
54 .conn()
55 .query_row(
56 "SELECT id, project, directory, started_at, ended_at, summary, status
57 FROM sessions WHERE id = ?1",
58 params![existing_id],
59 |row| {
60 Ok(Session {
61 id: row.get(0)?,
62 project: row.get(1)?,
63 directory: row.get(2)?,
64 started_at: row.get(3)?,
65 ended_at: row.get(4)?,
66 summary: row.get(5)?,
67 status: row.get(6)?,
68 })
69 },
70 )
71 .map_err(Error::Database);
72 }
73
74 let id = generate_session_id();
75 self.conn().execute(
76 "INSERT INTO sessions (id, project, directory) VALUES (?1, ?2, ?3)",
77 params![id, project, directory],
78 )?;
79
80 self.conn()
81 .query_row(
82 "SELECT id, project, directory, started_at, ended_at, summary, status
83 FROM sessions WHERE id = ?1",
84 params![id],
85 |row| {
86 Ok(Session {
87 id: row.get(0)?,
88 project: row.get(1)?,
89 directory: row.get(2)?,
90 started_at: row.get(3)?,
91 ended_at: row.get(4)?,
92 summary: row.get(5)?,
93 status: row.get(6)?,
94 })
95 },
96 )
97 .map_err(Error::Database)
98 }
99
100 pub fn session_end(
101 &self,
102 session_id: &str,
103 summary: Option<&str>,
104 tokens_input: Option<i32>,
105 tokens_output: Option<i32>,
106 ) -> Result<Session> {
107 let exists: bool = self
109 .conn()
110 .query_row(
111 "SELECT COUNT(*) FROM sessions WHERE id = ?1",
112 params![session_id],
113 |row| row.get::<_, i64>(0),
114 )
115 .map(|n| n > 0)
116 .unwrap_or(false);
117 if !exists {
118 return Err(Error::SessionNotFound(session_id.to_string()));
119 }
120
121 let rows_changed = self.conn().execute(
123 "UPDATE sessions SET ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
124 summary = ?1, status = 'completed',
125 tokens_used_input = ?3, tokens_used_output = ?4
126 WHERE id = ?2 AND status = 'active'",
127 params![summary, session_id, tokens_input, tokens_output],
128 )?;
129
130 if rows_changed == 0 {
131 return Err(Error::SessionAlreadyEnded(session_id.to_string()));
132 }
133
134 self.conn()
135 .query_row(
136 "SELECT id, project, directory, started_at, ended_at, summary, status
137 FROM sessions WHERE id = ?1",
138 params![session_id],
139 |row| {
140 Ok(Session {
141 id: row.get(0)?,
142 project: row.get(1)?,
143 directory: row.get(2)?,
144 started_at: row.get(3)?,
145 ended_at: row.get(4)?,
146 summary: row.get(5)?,
147 status: row.get(6)?,
148 })
149 },
150 )
151 .map_err(Error::Database)
152 }
153
154 pub fn most_recent_active_session_id(&self) -> Result<Option<String>> {
158 Ok(self
159 .conn()
160 .query_row(
161 "SELECT id FROM sessions WHERE status = 'active' ORDER BY started_at DESC LIMIT 1",
162 [],
163 |row| row.get(0),
164 )
165 .ok())
166 }
167
168 pub fn recent_sessions(&self, limit: i32) -> Result<Vec<Session>> {
169 let mut stmt = self.conn().prepare(
170 "SELECT id, project, directory, started_at, ended_at, summary, status
171 FROM sessions ORDER BY started_at DESC LIMIT ?1",
172 )?;
173
174 let results = stmt
175 .query_map(params![limit], |row| {
176 Ok(Session {
177 id: row.get(0)?,
178 project: row.get(1)?,
179 directory: row.get(2)?,
180 started_at: row.get(3)?,
181 ended_at: row.get(4)?,
182 summary: row.get(5)?,
183 status: row.get(6)?,
184 })
185 })?
186 .collect::<std::result::Result<Vec<_>, _>>()?;
187
188 Ok(results)
189 }
190
191 pub fn end_active_sessions(
194 &self,
195 project: Option<&str>,
196 summary: Option<&str>,
197 ) -> Result<usize> {
198 let active = self.recent_sessions(10000)?;
199 let mut ended = 0;
200 for session in &active {
201 if session.status != "active" {
202 continue;
203 }
204 if let Some(proj) = project {
205 if session.project != proj {
206 continue;
207 }
208 }
209 self.session_end(&session.id, summary, None, None)?;
210 ended += 1;
211 }
212 Ok(ended)
213 }
214}
215
216fn generate_session_id() -> String {
217 use std::time::{SystemTime, UNIX_EPOCH};
218 let ts = SystemTime::now()
219 .duration_since(UNIX_EPOCH)
220 .unwrap()
221 .as_nanos();
222 format!("ses_{ts:x}")
223}