Skip to main content

stynx_code_services/session_memory/
mod.rs

1use 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}