Skip to main content

kernex_memory/
memory_store.rs

1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
2
3//! Public trait surface for the SQLite-backed memory store.
4//!
5//! Mirrors the inherent method surface that downstream consumers
6//! (`kernex-runtime` composition, the sister-repo binary's REPL, future
7//! CLI/HTTP/MCP) call today, plus three new soft-delete methods on the
8//! `facts` table. Hard-delete inherent methods (`delete_fact`,
9//! `delete_facts`) stay on `Store` for emergency cleanup tooling and are
10//! deliberately NOT on the trait so the default consumer path uses
11//! recoverable soft-delete.
12//!
13//! `Runtime::store_handle()` returns `Arc<dyn MemoryStore>` so a binary
14//! consumer can share the runtime's composed `Store` instance instead of
15//! opening a second SQLite connection against the same database file.
16
17use std::sync::Arc;
18
19use async_trait::async_trait;
20
21use crate::error::MemoryError;
22use crate::store::{DueTask, Store, UsageSummary};
23
24/// Public trait surface over [`Store`].
25///
26/// Returned from `kernex-runtime::Runtime::store_handle()` as
27/// `Arc<dyn MemoryStore>`. Consumers should prefer this trait over the
28/// concrete `Store` type so future schema changes do not ripple into call
29/// sites.
30#[async_trait]
31pub trait MemoryStore: Send + Sync {
32    // --- conversations / messages ---
33
34    /// Mark the active conversation for `(channel, sender_id, project)` as
35    /// closed. Returns `true` if a row transitioned from active to closed.
36    async fn close_current_conversation(
37        &self,
38        channel: &str,
39        sender_id: &str,
40        project: &str,
41    ) -> Result<bool, MemoryError>;
42
43    /// Aggregate counters: `(conversation_count, message_count, fact_count)`.
44    async fn get_memory_stats(&self, sender_id: &str) -> Result<(i64, i64, i64), MemoryError>;
45
46    /// On-disk byte size of the SQLite database file.
47    async fn db_size(&self) -> Result<u64, MemoryError>;
48
49    /// Aggregate token usage across all sessions.
50    async fn get_total_usage(&self) -> Result<UsageSummary, MemoryError>;
51
52    /// Recent closed-conversation summaries for a given channel + sender,
53    /// newest first, capped at `limit`.
54    async fn get_history(
55        &self,
56        channel: &str,
57        sender_id: &str,
58        limit: i64,
59    ) -> Result<Vec<(String, String)>, MemoryError>;
60
61    /// FTS5 full-text search over user messages, excluding the live
62    /// conversation. Returns up to `limit` `(channel, role, content)` rows.
63    async fn search_messages(
64        &self,
65        query: &str,
66        exclude_conversation_id: &str,
67        sender_id: &str,
68        limit: i64,
69    ) -> Result<Vec<(String, String, String)>, MemoryError>;
70
71    // --- facts (write paths plus soft-only delete on the trait) ---
72
73    /// Upsert a fact for `(sender_id, key)`. If the row was previously
74    /// soft-deleted, this clears `deleted_at` so the value is visible
75    /// again to default-filtered reads.
76    async fn store_fact(&self, sender_id: &str, key: &str, value: &str) -> Result<(), MemoryError>;
77
78    /// Read a single active fact by `(sender_id, key)`. Returns `None` if
79    /// the row is soft-deleted, missing, or never existed.
80    async fn get_fact(&self, sender_id: &str, key: &str) -> Result<Option<String>, MemoryError>;
81
82    /// Active (not soft-deleted) facts for `sender_id`.
83    async fn get_facts(&self, sender_id: &str) -> Result<Vec<(String, String)>, MemoryError>;
84
85    /// Soft-delete a single fact by setting its `deleted_at` timestamp.
86    /// Returns `true` if a row transitioned from active to deleted; `false`
87    /// if the row was already deleted, missing, or never existed.
88    async fn soft_delete_fact(&self, sender_id: &str, key: &str) -> Result<bool, MemoryError>;
89
90    /// Soft-delete multiple facts. With `Some(key)`, soft-deletes that
91    /// specific key. With `None`, soft-deletes every active fact for the
92    /// sender. Returns the count of rows that transitioned from active to
93    /// deleted.
94    async fn soft_delete_facts(
95        &self,
96        sender_id: &str,
97        key: Option<&str>,
98    ) -> Result<u64, MemoryError>;
99
100    /// Read soft-deleted facts (debug / recovery helper). Returns
101    /// `(key, value, deleted_at)` rows for `sender_id`.
102    async fn list_soft_deleted_facts(
103        &self,
104        sender_id: &str,
105    ) -> Result<Vec<(String, String, String)>, MemoryError>;
106
107    // --- scheduled tasks ---
108
109    /// Insert a new scheduled task. Returns the new task id.
110    #[allow(clippy::too_many_arguments)]
111    async fn create_task(
112        &self,
113        channel: &str,
114        sender_id: &str,
115        reply_target: &str,
116        description: &str,
117        due_at: &str,
118        repeat: Option<&str>,
119        task_type: &str,
120        project: &str,
121    ) -> Result<String, MemoryError>;
122
123    /// Pending tasks for `sender_id` as raw `(id, description, due_at,
124    /// repeat, task_type, project)` rows, ordered by `due_at` ascending.
125    async fn get_tasks_for_sender(
126        &self,
127        sender_id: &str,
128    ) -> Result<Vec<(String, String, String, Option<String>, String, String)>, MemoryError>;
129
130    /// Mark a task as completed. With `Some("daily")` / `Some("weekly")` /
131    /// etc., reschedules the next occurrence; with `None` or `Some("once")`,
132    /// the task transitions to a terminal status.
133    async fn complete_task(&self, id: &str, repeat: Option<&str>) -> Result<(), MemoryError>;
134
135    /// Record a task failure. Increments retry counter; transitions to a
136    /// terminal failed status when retries exhaust. Returns `true` if the
137    /// task transitioned to a terminal state.
138    async fn fail_task(&self, id: &str, error: &str, max_retries: u32)
139        -> Result<bool, MemoryError>;
140
141    /// Cancel a pending task whose id starts with `id_prefix`, scoped to
142    /// `sender_id`. Returns `true` if a row was cancelled.
143    async fn cancel_task(&self, id_prefix: &str, sender_id: &str) -> Result<bool, MemoryError>;
144
145    /// All pending tasks whose `due_at` is in the past.
146    async fn get_due_tasks(&self) -> Result<Vec<DueTask>, MemoryError>;
147}
148
149#[async_trait]
150impl MemoryStore for Store {
151    async fn close_current_conversation(
152        &self,
153        channel: &str,
154        sender_id: &str,
155        project: &str,
156    ) -> Result<bool, MemoryError> {
157        Store::close_current_conversation(self, channel, sender_id, project).await
158    }
159
160    async fn get_memory_stats(&self, sender_id: &str) -> Result<(i64, i64, i64), MemoryError> {
161        Store::get_memory_stats(self, sender_id).await
162    }
163
164    async fn db_size(&self) -> Result<u64, MemoryError> {
165        Store::db_size(self).await
166    }
167
168    async fn get_total_usage(&self) -> Result<UsageSummary, MemoryError> {
169        Store::get_total_usage(self).await
170    }
171
172    async fn get_history(
173        &self,
174        channel: &str,
175        sender_id: &str,
176        limit: i64,
177    ) -> Result<Vec<(String, String)>, MemoryError> {
178        Store::get_history(self, channel, sender_id, limit).await
179    }
180
181    async fn search_messages(
182        &self,
183        query: &str,
184        exclude_conversation_id: &str,
185        sender_id: &str,
186        limit: i64,
187    ) -> Result<Vec<(String, String, String)>, MemoryError> {
188        Store::search_messages(self, query, exclude_conversation_id, sender_id, limit).await
189    }
190
191    async fn store_fact(&self, sender_id: &str, key: &str, value: &str) -> Result<(), MemoryError> {
192        Store::store_fact(self, sender_id, key, value).await
193    }
194
195    async fn get_fact(&self, sender_id: &str, key: &str) -> Result<Option<String>, MemoryError> {
196        Store::get_fact(self, sender_id, key).await
197    }
198
199    async fn get_facts(&self, sender_id: &str) -> Result<Vec<(String, String)>, MemoryError> {
200        Store::get_facts(self, sender_id).await
201    }
202
203    async fn soft_delete_fact(&self, sender_id: &str, key: &str) -> Result<bool, MemoryError> {
204        Store::soft_delete_fact(self, sender_id, key).await
205    }
206
207    async fn soft_delete_facts(
208        &self,
209        sender_id: &str,
210        key: Option<&str>,
211    ) -> Result<u64, MemoryError> {
212        Store::soft_delete_facts(self, sender_id, key).await
213    }
214
215    async fn list_soft_deleted_facts(
216        &self,
217        sender_id: &str,
218    ) -> Result<Vec<(String, String, String)>, MemoryError> {
219        Store::list_soft_deleted_facts(self, sender_id).await
220    }
221
222    async fn create_task(
223        &self,
224        channel: &str,
225        sender_id: &str,
226        reply_target: &str,
227        description: &str,
228        due_at: &str,
229        repeat: Option<&str>,
230        task_type: &str,
231        project: &str,
232    ) -> Result<String, MemoryError> {
233        Store::create_task(
234            self,
235            channel,
236            sender_id,
237            reply_target,
238            description,
239            due_at,
240            repeat,
241            task_type,
242            project,
243        )
244        .await
245    }
246
247    async fn get_tasks_for_sender(
248        &self,
249        sender_id: &str,
250    ) -> Result<Vec<(String, String, String, Option<String>, String, String)>, MemoryError> {
251        Store::get_tasks_for_sender(self, sender_id).await
252    }
253
254    async fn complete_task(&self, id: &str, repeat: Option<&str>) -> Result<(), MemoryError> {
255        Store::complete_task(self, id, repeat).await
256    }
257
258    async fn fail_task(
259        &self,
260        id: &str,
261        error: &str,
262        max_retries: u32,
263    ) -> Result<bool, MemoryError> {
264        Store::fail_task(self, id, error, max_retries).await
265    }
266
267    async fn cancel_task(&self, id_prefix: &str, sender_id: &str) -> Result<bool, MemoryError> {
268        Store::cancel_task(self, id_prefix, sender_id).await
269    }
270
271    async fn get_due_tasks(&self) -> Result<Vec<DueTask>, MemoryError> {
272        Store::get_due_tasks(self).await
273    }
274}
275
276/// Expose a [`Store`] through the [`MemoryStore`] trait surface.
277///
278/// `Store` already implements `Clone` (its `SqlitePool` is internally
279/// reference-counted); cloning here shares the same connection pool.
280pub fn into_handle(store: Store) -> Arc<dyn MemoryStore> {
281    Arc::new(store)
282}