Skip to main content

memory_core/store/
session.rs

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        // Reuse existing active session for this project (handles --resume, --continue, Ctrl+C)
10        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        // Reactivate a completed session for the same project+directory
36        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(&self, session_id: &str, summary: Option<&str>) -> Result<Session> {
101        let session = self
102            .conn()
103            .query_row(
104                "SELECT id, status FROM sessions WHERE id = ?1",
105                params![session_id],
106                |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
107            )
108            .map_err(|_| Error::SessionNotFound(session_id.to_string()))?;
109
110        if session.1 != "active" {
111            return Err(Error::SessionAlreadyEnded(session_id.to_string()));
112        }
113
114        self.conn().execute(
115            "UPDATE sessions SET ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
116             summary = ?1, status = 'completed'
117             WHERE id = ?2",
118            params![summary, session_id],
119        )?;
120
121        self.conn()
122            .query_row(
123                "SELECT id, project, directory, started_at, ended_at, summary, status
124                 FROM sessions WHERE id = ?1",
125                params![session_id],
126                |row| {
127                    Ok(Session {
128                        id: row.get(0)?,
129                        project: row.get(1)?,
130                        directory: row.get(2)?,
131                        started_at: row.get(3)?,
132                        ended_at: row.get(4)?,
133                        summary: row.get(5)?,
134                        status: row.get(6)?,
135                    })
136                },
137            )
138            .map_err(Error::Database)
139    }
140
141    pub fn recent_sessions(&self, limit: i32) -> Result<Vec<Session>> {
142        let mut stmt = self.conn().prepare(
143            "SELECT id, project, directory, started_at, ended_at, summary, status
144             FROM sessions ORDER BY started_at DESC LIMIT ?1",
145        )?;
146
147        let results = stmt
148            .query_map(params![limit], |row| {
149                Ok(Session {
150                    id: row.get(0)?,
151                    project: row.get(1)?,
152                    directory: row.get(2)?,
153                    started_at: row.get(3)?,
154                    ended_at: row.get(4)?,
155                    summary: row.get(5)?,
156                    status: row.get(6)?,
157                })
158            })?
159            .collect::<std::result::Result<Vec<_>, _>>()?;
160
161        Ok(results)
162    }
163
164    /// End all active sessions, optionally filtered by project name.
165    /// Returns the number of sessions ended.
166    pub fn end_active_sessions(
167        &self,
168        project: Option<&str>,
169        summary: Option<&str>,
170    ) -> Result<usize> {
171        let active = self.recent_sessions(10000)?;
172        let mut ended = 0;
173        for session in &active {
174            if session.status != "active" {
175                continue;
176            }
177            if let Some(proj) = project {
178                if session.project != proj {
179                    continue;
180                }
181            }
182            self.session_end(&session.id, summary)?;
183            ended += 1;
184        }
185        Ok(ended)
186    }
187}
188
189fn generate_session_id() -> String {
190    use std::time::{SystemTime, UNIX_EPOCH};
191    let ts = SystemTime::now()
192        .duration_since(UNIX_EPOCH)
193        .unwrap()
194        .as_nanos();
195    format!("ses_{ts:x}")
196}