zeph_memory/store/
session_digest.rs1use crate::error::MemoryError;
7use crate::store::SqliteStore;
8use crate::store::compression_guidelines::redact_sensitive;
9use crate::types::ConversationId;
10#[allow(unused_imports)]
11use zeph_db::sql;
12
13#[derive(Debug, Clone)]
15pub struct SessionDigest {
16 pub id: i64,
17 pub conversation_id: ConversationId,
18 pub digest: String,
19 pub token_count: i64,
20 pub updated_at: String,
21}
22
23impl SqliteStore {
24 pub async fn save_session_digest(
33 &self,
34 conversation_id: ConversationId,
35 digest: &str,
36 token_count: i64,
37 ) -> Result<(), MemoryError> {
38 let safe_digest = redact_sensitive(digest);
39 zeph_db::query(sql!(
40 "INSERT INTO session_digest (conversation_id, digest, token_count, updated_at) \
41 VALUES (?, ?, ?, CURRENT_TIMESTAMP) \
42 ON CONFLICT(conversation_id) DO UPDATE SET \
43 digest = excluded.digest, \
44 token_count = excluded.token_count, \
45 updated_at = excluded.updated_at"
46 ))
47 .bind(conversation_id.0)
48 .bind(safe_digest.as_ref())
49 .bind(token_count)
50 .execute(&self.pool)
51 .await?;
52 Ok(())
53 }
54
55 pub async fn load_session_digest(
61 &self,
62 conversation_id: ConversationId,
63 ) -> Result<Option<SessionDigest>, MemoryError> {
64 let row = zeph_db::query_as::<_, (i64, i64, String, i64, String)>(sql!(
65 "SELECT id, conversation_id, digest, token_count, updated_at \
66 FROM session_digest WHERE conversation_id = ?"
67 ))
68 .bind(conversation_id.0)
69 .fetch_optional(&self.pool)
70 .await?;
71
72 Ok(row.map(
73 |(id, conv_id, digest, token_count, updated_at)| SessionDigest {
74 id,
75 conversation_id: ConversationId(conv_id),
76 digest,
77 token_count,
78 updated_at,
79 },
80 ))
81 }
82
83 pub async fn delete_session_digest(
89 &self,
90 conversation_id: ConversationId,
91 ) -> Result<(), MemoryError> {
92 zeph_db::query(sql!("DELETE FROM session_digest WHERE conversation_id = ?"))
93 .bind(conversation_id.0)
94 .execute(&self.pool)
95 .await?;
96 Ok(())
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::store::SqliteStore;
104
105 async fn make_store() -> SqliteStore {
106 SqliteStore::with_pool_size(":memory:", 1)
107 .await
108 .expect("in-memory store")
109 }
110
111 async fn insert_conversation(store: &SqliteStore) -> ConversationId {
112 zeph_db::query_scalar::<_, i64>(sql!(
113 "INSERT INTO conversations (created_at) VALUES (CURRENT_TIMESTAMP) RETURNING id"
114 ))
115 .fetch_one(&store.pool)
116 .await
117 .map(ConversationId)
118 .expect("insert conversation")
119 }
120
121 #[tokio::test]
122 async fn save_and_load_digest() {
123 let store = make_store().await;
124 let conv_id = insert_conversation(&store).await;
125
126 store
127 .save_session_digest(conv_id, "Key facts from session.", 5)
128 .await
129 .unwrap();
130
131 let digest = store
132 .load_session_digest(conv_id)
133 .await
134 .unwrap()
135 .expect("digest should exist");
136
137 assert_eq!(digest.conversation_id, conv_id);
138 assert_eq!(digest.digest, "Key facts from session.");
139 assert_eq!(digest.token_count, 5);
140 }
141
142 #[tokio::test]
143 async fn upsert_preserves_id_and_created_at() {
144 let store = make_store().await;
145 let conv_id = insert_conversation(&store).await;
146
147 store
148 .save_session_digest(conv_id, "first", 3)
149 .await
150 .unwrap();
151 let first = store.load_session_digest(conv_id).await.unwrap().unwrap();
152
153 store
154 .save_session_digest(conv_id, "updated", 7)
155 .await
156 .unwrap();
157 let second = store.load_session_digest(conv_id).await.unwrap().unwrap();
158
159 assert_eq!(first.id, second.id);
161 assert_eq!(second.digest, "updated");
162 assert_eq!(second.token_count, 7);
163 }
164
165 #[tokio::test]
166 async fn load_nonexistent_returns_none() {
167 let store = make_store().await;
168 let result = store
169 .load_session_digest(ConversationId(9999))
170 .await
171 .unwrap();
172 assert!(result.is_none());
173 }
174
175 #[tokio::test]
176 async fn delete_digest() {
177 let store = make_store().await;
178 let conv_id = insert_conversation(&store).await;
179
180 store
181 .save_session_digest(conv_id, "to delete", 2)
182 .await
183 .unwrap();
184 store.delete_session_digest(conv_id).await.unwrap();
185
186 let result = store.load_session_digest(conv_id).await.unwrap();
187 assert!(result.is_none());
188 }
189}