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 status = 'active'
15                 ORDER BY started_at DESC LIMIT 1",
16                params![project],
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 id = generate_session_id();
36        self.conn().execute(
37            "INSERT INTO sessions (id, project, directory) VALUES (?1, ?2, ?3)",
38            params![id, project, directory],
39        )?;
40
41        self.conn()
42            .query_row(
43                "SELECT id, project, directory, started_at, ended_at, summary, status
44                 FROM sessions WHERE id = ?1",
45                params![id],
46                |row| {
47                    Ok(Session {
48                        id: row.get(0)?,
49                        project: row.get(1)?,
50                        directory: row.get(2)?,
51                        started_at: row.get(3)?,
52                        ended_at: row.get(4)?,
53                        summary: row.get(5)?,
54                        status: row.get(6)?,
55                    })
56                },
57            )
58            .map_err(Error::Database)
59    }
60
61    pub fn session_end(&self, session_id: &str, summary: Option<&str>) -> Result<Session> {
62        let session = self
63            .conn()
64            .query_row(
65                "SELECT id, status FROM sessions WHERE id = ?1",
66                params![session_id],
67                |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
68            )
69            .map_err(|_| Error::SessionNotFound(session_id.to_string()))?;
70
71        if session.1 != "active" {
72            return Err(Error::SessionAlreadyEnded(session_id.to_string()));
73        }
74
75        self.conn().execute(
76            "UPDATE sessions SET ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
77             summary = ?1, status = 'completed'
78             WHERE id = ?2",
79            params![summary, session_id],
80        )?;
81
82        self.conn()
83            .query_row(
84                "SELECT id, project, directory, started_at, ended_at, summary, status
85                 FROM sessions WHERE id = ?1",
86                params![session_id],
87                |row| {
88                    Ok(Session {
89                        id: row.get(0)?,
90                        project: row.get(1)?,
91                        directory: row.get(2)?,
92                        started_at: row.get(3)?,
93                        ended_at: row.get(4)?,
94                        summary: row.get(5)?,
95                        status: row.get(6)?,
96                    })
97                },
98            )
99            .map_err(Error::Database)
100    }
101
102    pub fn recent_sessions(&self, limit: i32) -> Result<Vec<Session>> {
103        let mut stmt = self.conn().prepare(
104            "SELECT id, project, directory, started_at, ended_at, summary, status
105             FROM sessions ORDER BY started_at DESC LIMIT ?1",
106        )?;
107
108        let results = stmt
109            .query_map(params![limit], |row| {
110                Ok(Session {
111                    id: row.get(0)?,
112                    project: row.get(1)?,
113                    directory: row.get(2)?,
114                    started_at: row.get(3)?,
115                    ended_at: row.get(4)?,
116                    summary: row.get(5)?,
117                    status: row.get(6)?,
118                })
119            })?
120            .collect::<std::result::Result<Vec<_>, _>>()?;
121
122        Ok(results)
123    }
124
125    /// End all active sessions, optionally filtered by project name.
126    /// Returns the number of sessions ended.
127    pub fn end_active_sessions(
128        &self,
129        project: Option<&str>,
130        summary: Option<&str>,
131    ) -> Result<usize> {
132        let active = self.recent_sessions(10000)?;
133        let mut ended = 0;
134        for session in &active {
135            if session.status != "active" {
136                continue;
137            }
138            if let Some(proj) = project {
139                if session.project != proj {
140                    continue;
141                }
142            }
143            self.session_end(&session.id, summary)?;
144            ended += 1;
145        }
146        Ok(ended)
147    }
148}
149
150fn generate_session_id() -> String {
151    use std::time::{SystemTime, UNIX_EPOCH};
152    let ts = SystemTime::now()
153        .duration_since(UNIX_EPOCH)
154        .unwrap()
155        .as_nanos();
156    format!("ses_{ts:x}")
157}