Skip to main content

matrixcode_core/memory/
entry.rs

1//! Memory entry types and categories.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use super::config::*;
7use crate::truncate::{find_boundary, truncate_with_suffix};
8
9// ============================================================================
10// Helper Functions
11// ============================================================================
12
13/// Truncate string with "..." suffix, respecting UTF-8 boundaries.
14pub(crate) fn truncate_str(s: &str, max_len: usize) -> String {
15    truncate_with_suffix(s, max_len)
16}
17
18/// Truncate string without suffix, respecting UTF-8 boundaries.
19pub(crate) fn truncate(s: &str, max_len: usize) -> String {
20    if s.len() <= max_len {
21        s.to_string()
22    } else {
23        let end = find_boundary(s, max_len);
24        s[..end].to_string()
25    }
26}
27
28// ============================================================================
29// Memory Categories
30// ============================================================================
31
32/// Categories for memory entries.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
34#[serde(rename_all = "snake_case")]
35pub enum MemoryCategory {
36    /// User preferences (e.g., "I prefer vim over nano")
37    Preference,
38    /// Project decisions (e.g., "Decided to use PostgreSQL")
39    Decision,
40    /// Key findings (e.g., "API endpoint is at /api/v2")
41    Finding,
42    /// Problem solutions (e.g., "Fixed auth bug by adding token refresh")
43    Solution,
44    /// Technical notes (e.g., "React Query is used for data fetching")
45    Technical,
46    /// Project structure (e.g., "src/index.ts is entry point")
47    Structure,
48    /// Key decisions made during task execution
49    KeyDecision,
50    /// Failed approaches to avoid repeating
51    FailedApproach,
52    /// User intent patterns learned from interactions
53    UserIntentPattern,
54    /// Task completion patterns
55    TaskPattern,
56}
57
58impl MemoryCategory {
59    /// Get display name for the category.
60    pub fn display_name(&self) -> &'static str {
61        match self {
62            MemoryCategory::Preference => "偏好",
63            MemoryCategory::Decision => "决策",
64            MemoryCategory::Finding => "发现",
65            MemoryCategory::Solution => "解决方案",
66            MemoryCategory::Technical => "技术",
67            MemoryCategory::Structure => "结构",
68            MemoryCategory::KeyDecision => "关键决策",
69            MemoryCategory::FailedApproach => "失败方案",
70            MemoryCategory::UserIntentPattern => "意图模式",
71            MemoryCategory::TaskPattern => "任务模式",
72        }
73    }
74
75    /// Get icon for the category.
76    pub fn icon(&self) -> &'static str {
77        match self {
78            MemoryCategory::Preference => "👤",
79            MemoryCategory::Decision => "🎯",
80            MemoryCategory::Finding => "💡",
81            MemoryCategory::Solution => "🔧",
82            MemoryCategory::Technical => "📚",
83            MemoryCategory::Structure => "🏗️",
84            MemoryCategory::KeyDecision => "⚡",
85            MemoryCategory::FailedApproach => "❌",
86            MemoryCategory::UserIntentPattern => "🧠",
87            MemoryCategory::TaskPattern => "📋",
88        }
89    }
90
91    /// Get default importance score for the category.
92    pub fn default_importance(&self) -> f64 {
93        match self {
94            MemoryCategory::Decision => DEFAULT_IMPORTANCE_DECISION,
95            MemoryCategory::Solution => DEFAULT_IMPORTANCE_SOLUTION,
96            MemoryCategory::Preference => DEFAULT_IMPORTANCE_PREF,
97            MemoryCategory::Finding => DEFAULT_IMPORTANCE_FINDING,
98            MemoryCategory::Technical => DEFAULT_IMPORTANCE_TECH,
99            MemoryCategory::Structure => DEFAULT_IMPORTANCE_STRUCTURE,
100            MemoryCategory::KeyDecision => 85.0,
101            MemoryCategory::FailedApproach => 70.0,
102            MemoryCategory::UserIntentPattern => 80.0,
103            MemoryCategory::TaskPattern => 75.0,
104        }
105    }
106}
107
108// ============================================================================
109// Memory Entry
110// ============================================================================
111
112/// A single memory entry.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct MemoryEntry {
115    /// Unique identifier.
116    pub id: String,
117    /// When the memory was created.
118    pub created_at: DateTime<Utc>,
119    /// When the memory was last accessed/referenced.
120    pub last_referenced: DateTime<Utc>,
121    /// Category of the memory.
122    pub category: MemoryCategory,
123    /// The memory content.
124    pub content: String,
125    /// Source session ID (where this memory was created).
126    pub source_session: Option<String>,
127    /// Project path where this memory was created.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub project_path: Option<String>,
130    /// Number of times this memory has been referenced.
131    pub reference_count: u32,
132    /// Importance score (0-100, higher = more important).
133    pub importance: f64,
134    /// Tags for searching/filtering.
135    pub tags: Vec<String>,
136    /// Whether this memory was manually added by user.
137    pub is_manual: bool,
138}
139
140impl MemoryEntry {
141    /// Create a new memory entry.
142    pub fn new(
143        category: MemoryCategory,
144        content: String,
145        source_session: Option<String>,
146        project_path: Option<String>,
147    ) -> Self {
148        let id = uuid::Uuid::new_v4().to_string();
149        Self {
150            id,
151            created_at: Utc::now(),
152            last_referenced: Utc::now(),
153            category,
154            content,
155            source_session,
156            project_path,
157            reference_count: 0,
158            importance: category.default_importance(),
159            tags: Vec::new(),
160            is_manual: false,
161        }
162    }
163
164    /// Create a manually added memory entry.
165    pub fn manual(category: MemoryCategory, content: String, project_path: Option<String>) -> Self {
166        let mut entry = Self::new(category, content, None, project_path);
167        entry.is_manual = true;
168        entry.importance = 95.0;
169        entry
170    }
171
172    /// Create a manually added memory entry (global, no project path).
173    pub fn manual_global(category: MemoryCategory, content: String) -> Self {
174        Self::manual(category, content, None)
175    }
176
177    /// Mark this memory as referenced (increases importance over time).
178    pub fn mark_referenced(&mut self) {
179        self.mark_referenced_with_increment(2.0);
180    }
181
182    /// Mark this memory as referenced with custom importance increment.
183    pub fn mark_referenced_with_increment(&mut self, increment: f64) {
184        self.reference_count += 1;
185        self.last_referenced = Utc::now();
186        self.importance = (self.importance + increment).min(MAX_IMPORTANCE_CEILING);
187    }
188
189    /// Format for display.
190    pub fn format_line(&self) -> String {
191        let time = self.created_at.format("%Y-%m-%d %H:%M");
192        let importance_marker = if self.importance >= IMPORTANCE_STAR_THRESHOLD {
193            "⭐"
194        } else {
195            ""
196        };
197        let manual_marker = if self.is_manual { "📝" } else { "" };
198        format!(
199            "{} {} {}{}{} {}",
200            self.category.icon(),
201            time,
202            importance_marker,
203            manual_marker,
204            self.category.display_name(),
205            truncate_str(&self.content, MAX_DISPLAY_LENGTH)
206        )
207    }
208
209    /// Format for inclusion in system prompt.
210    /// Note: This is used inside category groups, so we don't repeat the category name.
211    pub fn format_for_prompt(&self) -> String {
212        if self.content.len() > MAX_MEMORY_CONTENT_LENGTH {
213            format!(
214                "{}...",
215                truncate(&self.content, MAX_MEMORY_CONTENT_LENGTH - 3)
216            )
217        } else {
218            self.content.clone()
219        }
220    }
221
222    /// Format for inclusion in system prompt with category name.
223    /// Use this when displaying entries outside of category groups.
224    pub fn format_for_prompt_with_category(&self) -> String {
225        let category_name = self.category.display_name();
226        if self.content.len() > MAX_MEMORY_CONTENT_LENGTH {
227            format!(
228                "{}: {}...",
229                category_name,
230                truncate(&self.content, MAX_MEMORY_CONTENT_LENGTH - 3)
231            )
232        } else {
233            format!("{}: {}", category_name, self.content)
234        }
235    }
236}