1pub mod claudemd;
12#[cfg(feature = "embed")]
13pub mod embedding_memory;
14pub mod graph;
15pub mod graph_migrate;
16pub mod manager;
17pub mod memdir;
18pub mod session_storage;
19
20use async_trait::async_trait;
21use cersei_types::*;
22use std::path::PathBuf;
23
24pub fn strip_frontmatter(content: &str) -> String {
26 if content.starts_with("---") {
27 if let Some(close_pos) = content[3..].find("\n---") {
28 return content[3 + close_pos + 4..]
29 .trim_start_matches('\n')
30 .to_string();
31 }
32 }
33 content.to_string()
34}
35
36#[async_trait]
39pub trait Memory: Send + Sync {
40 async fn store(&self, session_id: &str, messages: &[Message]) -> Result<()>;
42
43 async fn load(&self, session_id: &str) -> Result<Vec<Message>>;
45
46 async fn search(&self, query: &str, limit: usize) -> Result<Vec<MemoryEntry>>;
48
49 async fn sessions(&self) -> Result<Vec<SessionInfo>>;
51
52 async fn delete(&self, session_id: &str) -> Result<()>;
54}
55
56pub struct JsonlMemory {
61 dir: PathBuf,
62}
63
64impl JsonlMemory {
65 pub fn new(dir: impl Into<PathBuf>) -> Self {
66 Self { dir: dir.into() }
67 }
68
69 fn session_path(&self, session_id: &str) -> PathBuf {
70 self.dir.join(format!("{}.jsonl", session_id))
71 }
72}
73
74#[async_trait]
75impl Memory for JsonlMemory {
76 async fn store(&self, session_id: &str, messages: &[Message]) -> Result<()> {
77 tokio::fs::create_dir_all(&self.dir).await?;
78 let path = self.session_path(session_id);
79 let mut content = String::new();
80 for msg in messages {
81 let line = serde_json::to_string(msg)?;
82 content.push_str(&line);
83 content.push('\n');
84 }
85 tokio::fs::write(&path, content).await?;
86 Ok(())
87 }
88
89 async fn load(&self, session_id: &str) -> Result<Vec<Message>> {
90 let path = self.session_path(session_id);
91 if !path.exists() {
92 return Ok(Vec::new());
93 }
94 let content = tokio::fs::read_to_string(&path).await?;
95 let mut messages = Vec::new();
96 for line in content.lines() {
97 if line.trim().is_empty() {
98 continue;
99 }
100 let msg: Message = serde_json::from_str(line)?;
101 messages.push(msg);
102 }
103 Ok(messages)
104 }
105
106 async fn search(&self, _query: &str, _limit: usize) -> Result<Vec<MemoryEntry>> {
107 Ok(Vec::new())
109 }
110
111 async fn sessions(&self) -> Result<Vec<SessionInfo>> {
112 let mut sessions = Vec::new();
113 if !self.dir.exists() {
114 return Ok(sessions);
115 }
116 let mut entries = tokio::fs::read_dir(&self.dir).await?;
117 while let Some(entry) = entries.next_entry().await? {
118 let path = entry.path();
119 if path.extension().and_then(|e| e.to_str()) == Some("jsonl") {
120 let id = path
121 .file_stem()
122 .and_then(|s| s.to_str())
123 .unwrap_or("")
124 .to_string();
125 let metadata = tokio::fs::metadata(&path).await?;
126 let created_at = metadata
127 .created()
128 .ok()
129 .and_then(|t| {
130 let dur = t.duration_since(std::time::UNIX_EPOCH).ok()?;
131 chrono::DateTime::from_timestamp(dur.as_secs() as i64, 0)
132 })
133 .unwrap_or_else(chrono::Utc::now);
134 let content = tokio::fs::read_to_string(&path).await.unwrap_or_default();
135 let message_count = content.lines().filter(|l| !l.trim().is_empty()).count();
136 sessions.push(SessionInfo {
137 id,
138 created_at,
139 message_count,
140 model: None,
141 });
142 }
143 }
144 Ok(sessions)
145 }
146
147 async fn delete(&self, session_id: &str) -> Result<()> {
148 let path = self.session_path(session_id);
149 if path.exists() {
150 tokio::fs::remove_file(&path).await?;
151 }
152 Ok(())
153 }
154}
155
156pub struct InMemory {
160 store: std::sync::Arc<parking_lot::Mutex<std::collections::HashMap<String, Vec<Message>>>>,
161}
162
163impl InMemory {
164 pub fn new() -> Self {
165 Self {
166 store: std::sync::Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())),
167 }
168 }
169}
170
171impl Default for InMemory {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177#[async_trait]
178impl Memory for InMemory {
179 async fn store(&self, session_id: &str, messages: &[Message]) -> Result<()> {
180 self.store
181 .lock()
182 .insert(session_id.to_string(), messages.to_vec());
183 Ok(())
184 }
185
186 async fn load(&self, session_id: &str) -> Result<Vec<Message>> {
187 Ok(self
188 .store
189 .lock()
190 .get(session_id)
191 .cloned()
192 .unwrap_or_default())
193 }
194
195 async fn search(&self, _query: &str, _limit: usize) -> Result<Vec<MemoryEntry>> {
196 Ok(Vec::new())
197 }
198
199 async fn sessions(&self) -> Result<Vec<SessionInfo>> {
200 let store = self.store.lock();
201 Ok(store
202 .iter()
203 .map(|(id, msgs)| SessionInfo {
204 id: id.clone(),
205 created_at: chrono::Utc::now(),
206 message_count: msgs.len(),
207 model: None,
208 })
209 .collect())
210 }
211
212 async fn delete(&self, session_id: &str) -> Result<()> {
213 self.store.lock().remove(session_id);
214 Ok(())
215 }
216}