stynx_code_services/session_memory/
mod.rs1use async_trait::async_trait;
2use stynx_code_errors::{AppError, AppResult};
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Memory {
8 pub id: String,
9 pub content: String,
10 pub created_at: u64,
11 pub tags: Vec<String>,
12}
13
14#[async_trait]
15pub trait SessionMemoryService: Send + Sync {
16 async fn extract_memories(&self, conversation: &str) -> Vec<Memory>;
17 async fn save_memory(&self, memory: &Memory) -> AppResult<()>;
18 async fn load_memories(&self) -> AppResult<Vec<Memory>>;
19}
20
21pub struct FileSessionMemory {
22 dir: PathBuf,
23}
24
25impl FileSessionMemory {
26 pub fn new() -> Self {
27 let dir = home_claude_dir().join("memories");
28 Self { dir }
29 }
30
31 pub fn with_dir(dir: PathBuf) -> Self {
32 Self { dir }
33 }
34}
35
36fn home_claude_dir() -> PathBuf {
37 stynx_code_config::home_dir()
38 .unwrap_or_else(|| PathBuf::from("."))
39 .join(".claude")
40}
41
42fn now_secs() -> u64 {
43 std::time::SystemTime::now()
44 .duration_since(std::time::UNIX_EPOCH)
45 .unwrap_or_default()
46 .as_secs()
47}
48
49#[async_trait]
50impl SessionMemoryService for FileSessionMemory {
51 async fn extract_memories(&self, conversation: &str) -> Vec<Memory> {
52
53 conversation
54 .lines()
55 .filter(|line| line.starts_with("REMEMBER:"))
56 .map(|line| {
57 let content = line.trim_start_matches("REMEMBER:").trim().to_string();
58 Memory {
59 id: format!("mem_{}", now_secs()),
60 content,
61 created_at: now_secs(),
62 tags: Vec::new(),
63 }
64 })
65 .collect()
66 }
67
68 async fn save_memory(&self, memory: &Memory) -> AppResult<()> {
69 tokio::fs::create_dir_all(&self.dir)
70 .await
71 .map_err(|e| -> AppError {
72 anyhow::anyhow!("failed to create memories dir: {e}").into()
73 })?;
74
75 let path = self.dir.join(format!("{}.json", memory.id));
76 let data = serde_json::to_string_pretty(memory)?;
77
78 tokio::fs::write(&path, data)
79 .await
80 .map_err(|e| -> AppError {
81 anyhow::anyhow!("failed to write memory file: {e}").into()
82 })?;
83
84 tracing::info!(id = %memory.id, "saved memory");
85 Ok(())
86 }
87
88 async fn load_memories(&self) -> AppResult<Vec<Memory>> {
89 let mut memories = Vec::new();
90
91 let dir_exists = tokio::fs::metadata(&self.dir).await.is_ok();
92 if !dir_exists {
93 return Ok(memories);
94 }
95
96 let mut entries = tokio::fs::read_dir(&self.dir)
97 .await
98 .map_err(|e| -> AppError {
99 anyhow::anyhow!("failed to read memories dir: {e}").into()
100 })?;
101
102 while let Some(entry) = entries
103 .next_entry()
104 .await
105 .map_err(|e| -> AppError {
106 anyhow::anyhow!("failed to read dir entry: {e}").into()
107 })?
108 {
109 let path = entry.path();
110 if path.extension().and_then(|e| e.to_str()) == Some("json") {
111 let data = tokio::fs::read_to_string(&path)
112 .await
113 .map_err(|e| -> AppError {
114 anyhow::anyhow!("failed to read memory file: {e}").into()
115 })?;
116 if let Ok(memory) = serde_json::from_str::<Memory>(&data) {
117 memories.push(memory);
118 } else {
119 tracing::warn!(path = %path.display(), "skipping malformed memory file");
120 }
121 }
122 }
123
124 memories.sort_by_key(|m| m.created_at);
125 Ok(memories)
126 }
127}