Skip to main content

matrixcode_core/memory/
entry.rs

1//! Memory entry types and categories.
2//!
3//! Supports memory linking with `[[name]]` syntax for cross-referencing
4//! related memories. Links are parsed and can be resolved during retrieval.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9
10use super::config::*;
11use crate::truncate::{find_boundary, truncate_with_suffix};
12
13// ============================================================================
14// Helper Functions
15// ============================================================================
16
17/// Truncate string with "..." suffix, respecting UTF-8 boundaries.
18pub(crate) fn truncate_str(s: &str, max_len: usize) -> String {
19    truncate_with_suffix(s, max_len)
20}
21
22/// Truncate string without suffix, respecting UTF-8 boundaries.
23pub(crate) fn truncate(s: &str, max_len: usize) -> String {
24    if s.len() <= max_len {
25        s.to_string()
26    } else {
27        let end = find_boundary(s, max_len);
28        s[..end].to_string()
29    }
30}
31
32/// Parse `[[name]]` link syntax from content.
33/// Returns a set of linked memory names/IDs.
34pub fn parse_memory_links(content: &str) -> HashSet<String> {
35    // Match [[name]] pattern
36    let re = regex::Regex::new(r"\[\[([^\]]+)\]\]").unwrap();
37    re.captures_iter(content)
38        .map(|c| c[1].trim().to_string())
39        .collect()
40}
41
42/// Check if content contains memory links.
43pub fn has_memory_links(content: &str) -> bool {
44    content.contains("[[") && content.contains("]]")
45}
46
47// ============================================================================
48// Memory Categories
49// ============================================================================
50
51/// Categories for memory entries.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
53#[serde(rename_all = "snake_case")]
54pub enum MemoryCategory {
55    /// User preferences (e.g., "I prefer vim over nano")
56    Preference,
57    /// Project decisions (e.g., "Decided to use PostgreSQL")
58    Decision,
59    /// Key findings (e.g., "API endpoint is at /api/v2")
60    Finding,
61    /// Problem solutions (e.g., "Fixed auth bug by adding token refresh")
62    Solution,
63    /// Technical notes (e.g., "React Query is used for data fetching")
64    Technical,
65    /// Project structure (e.g., "src/index.ts is entry point")
66    Structure,
67    /// Key decisions made during task execution
68    KeyDecision,
69    /// Failed approaches to avoid repeating
70    FailedApproach,
71    /// User intent patterns learned from interactions
72    UserIntentPattern,
73    /// Task completion patterns
74    TaskPattern,
75}
76
77impl MemoryCategory {
78    /// Get display name for the category.
79    pub fn display_name(&self) -> &'static str {
80        match self {
81            MemoryCategory::Preference => "偏好",
82            MemoryCategory::Decision => "决策",
83            MemoryCategory::Finding => "发现",
84            MemoryCategory::Solution => "解决方案",
85            MemoryCategory::Technical => "技术",
86            MemoryCategory::Structure => "结构",
87            MemoryCategory::KeyDecision => "关键决策",
88            MemoryCategory::FailedApproach => "失败方案",
89            MemoryCategory::UserIntentPattern => "意图模式",
90            MemoryCategory::TaskPattern => "任务模式",
91        }
92    }
93
94    /// Get icon for the category.
95    pub fn icon(&self) -> &'static str {
96        match self {
97            MemoryCategory::Preference => "👤",
98            MemoryCategory::Decision => "🎯",
99            MemoryCategory::Finding => "💡",
100            MemoryCategory::Solution => "🔧",
101            MemoryCategory::Technical => "📚",
102            MemoryCategory::Structure => "🏗️",
103            MemoryCategory::KeyDecision => "⚡",
104            MemoryCategory::FailedApproach => "❌",
105            MemoryCategory::UserIntentPattern => "🧠",
106            MemoryCategory::TaskPattern => "📋",
107        }
108    }
109
110    /// Get default importance score for the category.
111    pub fn default_importance(&self) -> f64 {
112        match self {
113            MemoryCategory::Decision => DEFAULT_IMPORTANCE_DECISION,
114            MemoryCategory::Solution => DEFAULT_IMPORTANCE_SOLUTION,
115            MemoryCategory::Preference => DEFAULT_IMPORTANCE_PREF,
116            MemoryCategory::Finding => DEFAULT_IMPORTANCE_FINDING,
117            MemoryCategory::Technical => DEFAULT_IMPORTANCE_TECH,
118            MemoryCategory::Structure => DEFAULT_IMPORTANCE_STRUCTURE,
119            MemoryCategory::KeyDecision => 85.0,
120            MemoryCategory::FailedApproach => 70.0,
121            MemoryCategory::UserIntentPattern => 80.0,
122            MemoryCategory::TaskPattern => 75.0,
123        }
124    }
125}
126
127// ============================================================================
128// Memory Entry
129// ============================================================================
130
131/// A single memory entry.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct MemoryEntry {
134    /// Unique identifier.
135    pub id: String,
136    /// Short name for linking (optional). Used in `[[name]]` references.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub name: Option<String>,
139    /// When the memory was created.
140    pub created_at: DateTime<Utc>,
141    /// When the memory was last accessed/referenced.
142    pub last_referenced: DateTime<Utc>,
143    /// Category of the memory.
144    pub category: MemoryCategory,
145    /// The memory content.
146    pub content: String,
147    /// Source session ID (where this memory was created).
148    pub source_session: Option<String>,
149    /// Project path where this memory was created.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub project_path: Option<String>,
152    /// Number of times this memory has been referenced.
153    pub reference_count: u32,
154    /// Importance score (0-100, higher = more important).
155    pub importance: f64,
156    /// Tags for searching/filtering.
157    pub tags: Vec<String>,
158    /// Whether this memory was manually added by user.
159    pub is_manual: bool,
160    /// Related memory IDs/names (extracted from `[[name]]` syntax).
161    #[serde(default, skip_serializing_if = "HashSet::is_empty")]
162    pub related_memories: HashSet<String>,
163}
164
165impl MemoryEntry {
166    /// Create a new memory entry.
167    pub fn new(
168        category: MemoryCategory,
169        content: String,
170        source_session: Option<String>,
171        project_path: Option<String>,
172    ) -> Self {
173        let id = uuid::Uuid::new_v4().to_string();
174        // Parse links from content
175        let related_memories = parse_memory_links(&content);
176        Self {
177            id,
178            name: None,
179            created_at: Utc::now(),
180            last_referenced: Utc::now(),
181            category,
182            content,
183            source_session,
184            project_path,
185            reference_count: 0,
186            importance: category.default_importance(),
187            tags: Vec::new(),
188            is_manual: false,
189            related_memories,
190        }
191    }
192
193    /// Create a new memory entry with a name (for linking).
194    pub fn with_name(
195        category: MemoryCategory,
196        name: String,
197        content: String,
198        source_session: Option<String>,
199        project_path: Option<String>,
200    ) -> Self {
201        let mut entry = Self::new(category, content, source_session, project_path);
202        entry.name = Some(name);
203        entry
204    }
205
206    /// Create a manually added memory entry.
207    pub fn manual(category: MemoryCategory, content: String, project_path: Option<String>) -> Self {
208        let mut entry = Self::new(category, content, None, project_path);
209        entry.is_manual = true;
210        entry.importance = 95.0;
211        entry
212    }
213
214    /// Create a manually added memory entry with name.
215    pub fn manual_with_name(
216        category: MemoryCategory,
217        name: String,
218        content: String,
219        project_path: Option<String>,
220    ) -> Self {
221        let mut entry = Self::manual(category, content, project_path);
222        entry.name = Some(name);
223        entry
224    }
225
226    /// Create a manually added memory entry (global, no project path).
227    pub fn manual_global(category: MemoryCategory, content: String) -> Self {
228        Self::manual(category, content, None)
229    }
230
231    /// Get linked memory names from content.
232    pub fn get_links(&self) -> HashSet<String> {
233        parse_memory_links(&self.content)
234    }
235
236    /// Check if this memory has any links.
237    pub fn has_links(&self) -> bool {
238        has_memory_links(&self.content)
239    }
240
241    /// Update related_memories by re-parsing content.
242    pub fn refresh_links(&mut self) {
243        self.related_memories = parse_memory_links(&self.content);
244    }
245
246    /// Mark this memory as referenced (increases importance over time).
247    pub fn mark_referenced(&mut self) {
248        self.mark_referenced_with_increment(2.0);
249    }
250
251    /// Mark this memory as referenced with custom importance increment.
252    pub fn mark_referenced_with_increment(&mut self, increment: f64) {
253        self.reference_count += 1;
254        self.last_referenced = Utc::now();
255        self.importance = (self.importance + increment).min(MAX_IMPORTANCE_CEILING);
256    }
257
258    /// Format for display.
259    pub fn format_line(&self) -> String {
260        let time = self.created_at.format("%Y-%m-%d %H:%M");
261        let importance_marker = if self.importance >= IMPORTANCE_STAR_THRESHOLD {
262            "⭐"
263        } else {
264            ""
265        };
266        let manual_marker = if self.is_manual { "📝" } else { "" };
267        let link_marker = if self.has_links() { "🔗" } else { "" };
268        let name_display = self.name.as_deref().map(|n| format!("[{}]", n)).unwrap_or_default();
269        format!(
270            "{} {} {}{}{}{} {}",
271            self.category.icon(),
272            time,
273            importance_marker,
274            manual_marker,
275            link_marker,
276            name_display,
277            truncate_str(&self.content, MAX_DISPLAY_LENGTH)
278        )
279    }
280
281    /// Format for inclusion in system prompt.
282    /// Note: This is used inside category groups, so we don't repeat the category name.
283    pub fn format_for_prompt(&self) -> String {
284        if self.content.len() > MAX_MEMORY_CONTENT_LENGTH {
285            format!(
286                "{}...",
287                truncate(&self.content, MAX_MEMORY_CONTENT_LENGTH - 3)
288            )
289        } else {
290            self.content.clone()
291        }
292    }
293
294    /// Format for inclusion in system prompt with category name.
295    /// Use this when displaying entries outside of category groups.
296    pub fn format_for_prompt_with_category(&self) -> String {
297        let category_name = self.category.display_name();
298        if self.content.len() > MAX_MEMORY_CONTENT_LENGTH {
299            format!(
300                "{}: {}...",
301                category_name,
302                truncate(&self.content, MAX_MEMORY_CONTENT_LENGTH - 3)
303            )
304        } else {
305            format!("{}: {}", category_name, self.content)
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_parse_memory_links_single() {
316        let content = "使用 [[redis-config]] 进行缓存配置";
317        let links = parse_memory_links(content);
318        assert_eq!(links.len(), 1);
319        assert!(links.contains("redis-config"));
320    }
321
322    #[test]
323    fn test_parse_memory_links_multiple() {
324        let content = "参考 [[api-design]] 和 [[database-schema]] 进行开发";
325        let links = parse_memory_links(content);
326        assert_eq!(links.len(), 2);
327        assert!(links.contains("api-design"));
328        assert!(links.contains("database-schema"));
329    }
330
331    #[test]
332    fn test_parse_memory_links_no_links() {
333        let content = "这是一条普通记忆,没有链接";
334        let links = parse_memory_links(content);
335        assert!(links.is_empty());
336    }
337
338    #[test]
339    fn test_parse_memory_links_with_spaces() {
340        let content = "参考 [[  spaced-name  ]] 进行开发";
341        let links = parse_memory_links(content);
342        assert!(links.contains("spaced-name")); // trimmed
343    }
344
345    #[test]
346    fn test_has_memory_links() {
347        assert!(has_memory_links("参考 [[config]] 设置"));
348        assert!(!has_memory_links("没有链接的记忆"));
349    }
350
351    #[test]
352    fn test_memory_entry_extract_links_on_creation() {
353        let entry = MemoryEntry::new(
354            MemoryCategory::Decision,
355            "使用 [[redis]] 作为缓存,参考 [[config-pattern]]".to_string(),
356            None,
357            None,
358        );
359        assert_eq!(entry.related_memories.len(), 2);
360        assert!(entry.related_memories.contains("redis"));
361        assert!(entry.related_memories.contains("config-pattern"));
362    }
363
364    #[test]
365    fn test_memory_entry_with_name() {
366        let entry = MemoryEntry::with_name(
367            MemoryCategory::Technical,
368            "api-endpoints".to_string(),
369            "API 端点定义".to_string(),
370            None,
371            None,
372        );
373        assert_eq!(entry.name, Some("api-endpoints".to_string()));
374    }
375
376    #[test]
377    fn test_memory_entry_refresh_links() {
378        let mut entry = MemoryEntry::new(
379            MemoryCategory::Decision,
380            "初始内容".to_string(),
381            None,
382            None,
383        );
384        assert!(entry.related_memories.is_empty());
385
386        // Update content with links
387        entry.content = "更新内容,参考 [[new-link]]".to_string();
388        entry.refresh_links();
389        assert!(entry.related_memories.contains("new-link"));
390    }
391
392    #[test]
393    fn test_format_line_with_links() {
394        let entry = MemoryEntry::new(
395            MemoryCategory::Technical,
396            "参考 [[config]] 设置".to_string(),
397            None,
398            None,
399        );
400        let line = entry.format_line();
401        assert!(line.contains("🔗")); // Link marker
402    }
403
404    #[test]
405    fn test_format_line_with_name() {
406        let entry = MemoryEntry::with_name(
407            MemoryCategory::Decision,
408            "cache-decision".to_string(),
409            "决定使用 Redis".to_string(),
410            None,
411            None,
412        );
413        let line = entry.format_line();
414        assert!(line.contains("[cache-decision]"));
415    }
416}