1#[cfg(feature = "sqlite")]
12pub mod sqlite;
13
14#[cfg(feature = "sqlite")]
15pub use sqlite::SqliteMemoryStore;
16
17use anyhow::Context as _;
18use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use tokio::sync::RwLock;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct RelevanceConfig {
31 #[serde(default = "RelevanceConfig::default_decay_days")]
33 pub decay_days: f32,
34 #[serde(default = "RelevanceConfig::default_importance_weight")]
36 pub importance_weight: f32,
37 #[serde(default = "RelevanceConfig::default_recency_weight")]
39 pub recency_weight: f32,
40}
41
42impl RelevanceConfig {
43 fn default_decay_days() -> f32 {
44 30.0
45 }
46 fn default_importance_weight() -> f32 {
47 0.7
48 }
49 fn default_recency_weight() -> f32 {
50 0.3
51 }
52}
53
54impl Default for RelevanceConfig {
55 fn default() -> Self {
56 Self {
57 decay_days: 30.0,
58 importance_weight: 0.7,
59 recency_weight: 0.3,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct PrunePolicy {
68 #[serde(default = "PrunePolicy::default_max_age_days")]
70 pub max_age_days: u32,
71 #[serde(default = "PrunePolicy::default_min_importance_to_keep")]
74 pub min_importance_to_keep: f32,
75 #[serde(default)]
78 pub max_items: usize,
79}
80
81impl PrunePolicy {
82 fn default_max_age_days() -> u32 {
83 90
84 }
85 fn default_min_importance_to_keep() -> f32 {
86 0.5
87 }
88}
89
90impl Default for PrunePolicy {
91 fn default() -> Self {
92 Self {
93 max_age_days: 90,
94 min_importance_to_keep: 0.5,
95 max_items: 0,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct MemoryItem {
107 pub id: String,
108 pub content: String,
109 pub timestamp: DateTime<Utc>,
110 pub importance: f32,
111 pub tags: Vec<String>,
112 pub memory_type: MemoryType,
113 pub metadata: HashMap<String, String>,
114 pub access_count: u32,
115 pub last_accessed: Option<DateTime<Utc>>,
116 #[serde(skip)]
117 pub content_lower: String,
118}
119
120impl MemoryItem {
121 pub fn new(content: impl Into<String>) -> Self {
122 let content = content.into();
123 let content_lower = content.to_lowercase();
124 Self {
125 id: uuid::Uuid::new_v4().to_string(),
126 content,
127 timestamp: Utc::now(),
128 importance: 0.5,
129 tags: Vec::new(),
130 memory_type: MemoryType::Episodic,
131 metadata: HashMap::new(),
132 access_count: 0,
133 last_accessed: None,
134 content_lower,
135 }
136 }
137
138 pub fn with_importance(mut self, importance: f32) -> Self {
139 self.importance = importance.clamp(0.0, 1.0);
140 self
141 }
142
143 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
144 self.tags = tags;
145 self
146 }
147
148 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
149 self.tags.push(tag.into());
150 self
151 }
152
153 pub fn with_type(mut self, memory_type: MemoryType) -> Self {
154 self.memory_type = memory_type;
155 self
156 }
157
158 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
159 self.metadata.insert(key.into(), value.into());
160 self
161 }
162
163 pub fn record_access(&mut self) {
164 self.access_count += 1;
165 self.last_accessed = Some(Utc::now());
166 }
167
168 pub fn relevance_score_at(&self, now: DateTime<Utc>, config: &RelevanceConfig) -> f32 {
170 let age_days = (now - self.timestamp).num_seconds() as f32 / 86400.0;
171 let decay = (-age_days / config.decay_days).exp();
172 self.importance * config.importance_weight + decay * config.recency_weight
173 }
174
175 pub fn relevance_score(&self) -> f32 {
177 self.relevance_score_at(Utc::now(), &RelevanceConfig::default())
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum MemoryType {
185 Episodic,
186 Semantic,
187 Procedural,
188 Working,
189}
190
191#[async_trait::async_trait]
196pub trait MemoryStore: Send + Sync {
197 async fn store(&self, item: MemoryItem) -> anyhow::Result<()>;
198 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>>;
199 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
200 async fn search_by_tags(
201 &self,
202 tags: &[String],
203 limit: usize,
204 ) -> anyhow::Result<Vec<MemoryItem>>;
205 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
206 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
207 async fn delete(&self, id: &str) -> anyhow::Result<()>;
208 async fn clear(&self) -> anyhow::Result<()>;
209 async fn count(&self) -> anyhow::Result<usize>;
210
211 async fn prune(&self, policy: &PrunePolicy) -> anyhow::Result<usize> {
216 let _ = policy;
217 Ok(0)
218 }
219}
220
221fn index_score(entry: &IndexEntry, now: DateTime<Utc>, config: &RelevanceConfig) -> f32 {
227 let age_days = (now - entry.timestamp).num_seconds() as f32 / 86400.0;
228 let decay = (-age_days / config.decay_days).exp();
229 entry.importance * config.importance_weight + decay * config.recency_weight
230}
231
232fn sort_by_relevance(items: &mut [MemoryItem]) {
233 let now = Utc::now();
234 let config = RelevanceConfig::default();
235 items.sort_by(|a, b| {
236 b.relevance_score_at(now, &config)
237 .partial_cmp(&a.relevance_score_at(now, &config))
238 .unwrap_or(std::cmp::Ordering::Equal)
239 });
240}
241
242pub struct InMemoryStore {
250 items: RwLock<Vec<MemoryItem>>,
251}
252
253impl Default for InMemoryStore {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259impl InMemoryStore {
260 pub fn new() -> Self {
261 Self {
262 items: RwLock::new(Vec::new()),
263 }
264 }
265}
266
267#[async_trait::async_trait]
268impl MemoryStore for InMemoryStore {
269 async fn store(&self, item: MemoryItem) -> anyhow::Result<()> {
270 let mut items = self.items.write().await;
271 if let Some(pos) = items.iter().position(|i| i.id == item.id) {
272 items[pos] = item;
273 } else {
274 items.push(item);
275 }
276 Ok(())
277 }
278
279 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
280 Ok(self.items.read().await.iter().find(|i| i.id == id).cloned())
281 }
282
283 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
284 let query_lower = query.to_lowercase();
285 let config = RelevanceConfig::default();
286 let now = Utc::now();
287 let items = self.items.read().await;
288 let mut matches: Vec<MemoryItem> = items
289 .iter()
290 .filter(|i| i.content_lower.contains(&query_lower))
291 .cloned()
292 .collect();
293 matches.sort_by(|a, b| {
294 b.relevance_score_at(now, &config)
295 .partial_cmp(&a.relevance_score_at(now, &config))
296 .unwrap_or(std::cmp::Ordering::Equal)
297 });
298 matches.truncate(limit);
299 Ok(matches)
300 }
301
302 async fn search_by_tags(
303 &self,
304 tags: &[String],
305 limit: usize,
306 ) -> anyhow::Result<Vec<MemoryItem>> {
307 let config = RelevanceConfig::default();
308 let now = Utc::now();
309 let items = self.items.read().await;
310 let mut matches: Vec<MemoryItem> = items
311 .iter()
312 .filter(|i| tags.iter().any(|t| i.tags.contains(t)))
313 .cloned()
314 .collect();
315 matches.sort_by(|a, b| {
316 b.relevance_score_at(now, &config)
317 .partial_cmp(&a.relevance_score_at(now, &config))
318 .unwrap_or(std::cmp::Ordering::Equal)
319 });
320 matches.truncate(limit);
321 Ok(matches)
322 }
323
324 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
325 let items = self.items.read().await;
326 let mut sorted: Vec<MemoryItem> = items.iter().cloned().collect();
327 sorted.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
328 sorted.truncate(limit);
329 Ok(sorted)
330 }
331
332 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
333 let items = self.items.read().await;
334 let mut matches: Vec<MemoryItem> = items
335 .iter()
336 .filter(|i| i.importance >= threshold)
337 .cloned()
338 .collect();
339 matches.sort_by(|a, b| {
340 b.importance
341 .partial_cmp(&a.importance)
342 .unwrap_or(std::cmp::Ordering::Equal)
343 });
344 matches.truncate(limit);
345 Ok(matches)
346 }
347
348 async fn delete(&self, id: &str) -> anyhow::Result<()> {
349 self.items.write().await.retain(|i| i.id != id);
350 Ok(())
351 }
352
353 async fn clear(&self) -> anyhow::Result<()> {
354 self.items.write().await.clear();
355 Ok(())
356 }
357
358 async fn count(&self) -> anyhow::Result<usize> {
359 Ok(self.items.read().await.len())
360 }
361
362 async fn prune(&self, policy: &PrunePolicy) -> anyhow::Result<usize> {
363 let now = Utc::now();
364 let cutoff = now - chrono::Duration::days(policy.max_age_days as i64);
365 let min_importance = policy.min_importance_to_keep;
366
367 let mut items = self.items.write().await;
368 let before = items.len();
369
370 items.retain(|item| item.importance >= min_importance || item.timestamp >= cutoff);
372
373 if policy.max_items > 0 && items.len() > policy.max_items {
375 let config = RelevanceConfig::default();
376 items.sort_by(|a, b| {
377 b.relevance_score_at(now, &config)
378 .partial_cmp(&a.relevance_score_at(now, &config))
379 .unwrap_or(std::cmp::Ordering::Equal)
380 });
381 items.truncate(policy.max_items);
382 }
383
384 Ok(before - items.len())
385 }
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
393struct IndexEntry {
394 id: String,
395 content_lower: String,
396 tags: Vec<String>,
397 importance: f32,
398 timestamp: DateTime<Utc>,
399 memory_type: MemoryType,
400}
401
402impl From<&MemoryItem> for IndexEntry {
403 fn from(item: &MemoryItem) -> Self {
404 Self {
405 id: item.id.clone(),
406 content_lower: item.content.to_lowercase(),
407 tags: item.tags.clone(),
408 importance: item.importance,
409 timestamp: item.timestamp,
410 memory_type: item.memory_type,
411 }
412 }
413}
414
415pub struct FileMemoryStore {
423 items_dir: std::path::PathBuf,
424 index_path: std::path::PathBuf,
425 index: RwLock<Vec<IndexEntry>>,
426}
427
428impl FileMemoryStore {
429 pub async fn new(dir: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
430 let dir = dir.as_ref().to_path_buf();
431 let items_dir = dir.join("items");
432 let index_path = dir.join("index.json");
433
434 tokio::fs::create_dir_all(&items_dir)
435 .await
436 .with_context(|| {
437 format!("Failed to create memory directory: {}", items_dir.display())
438 })?;
439
440 let index = if index_path.exists() {
441 let data = tokio::fs::read_to_string(&index_path)
442 .await
443 .with_context(|| {
444 format!("Failed to read memory index: {}", index_path.display())
445 })?;
446 serde_json::from_str(&data).unwrap_or_default()
447 } else {
448 Vec::new()
449 };
450
451 Ok(Self {
452 items_dir,
453 index_path,
454 index: RwLock::new(index),
455 })
456 }
457
458 fn safe_id(id: &str) -> String {
459 id.replace(['/', '\\'], "_").replace("..", "_")
460 }
461
462 fn item_path(&self, id: &str) -> std::path::PathBuf {
463 self.items_dir.join(format!("{}.json", Self::safe_id(id)))
464 }
465
466 async fn save_index(&self) -> anyhow::Result<()> {
467 let index = self.index.read().await;
468 let json = serde_json::to_string(&*index).context("Failed to serialize memory index")?;
469 drop(index);
470 let tmp = self.index_path.with_extension("json.tmp");
471 tokio::fs::write(&tmp, json.as_bytes())
472 .await
473 .context("Failed to write memory index temp file")?;
474 tokio::fs::rename(&tmp, &self.index_path)
475 .await
476 .context("Failed to rename memory index")?;
477 Ok(())
478 }
479
480 async fn save_item(&self, item: &MemoryItem) -> anyhow::Result<()> {
481 let path = self.item_path(&item.id);
482 let json = serde_json::to_string_pretty(item)
483 .with_context(|| format!("Failed to serialize memory item: {}", item.id))?;
484 let tmp = path.with_extension("json.tmp");
485 tokio::fs::write(&tmp, json.as_bytes())
486 .await
487 .with_context(|| format!("Failed to write memory item: {}", item.id))?;
488 tokio::fs::rename(&tmp, &path)
489 .await
490 .with_context(|| format!("Failed to rename memory item: {}", item.id))?;
491 Ok(())
492 }
493
494 pub async fn rebuild_index(&self) -> anyhow::Result<usize> {
496 let mut entries = tokio::fs::read_dir(&self.items_dir).await?;
497 let mut new_index = Vec::new();
498 while let Some(entry) = entries.next_entry().await? {
499 let path = entry.path();
500 if path.extension().is_some_and(|ext| ext == "json") {
501 if let Ok(data) = tokio::fs::read_to_string(&path).await {
502 if let Ok(item) = serde_json::from_str::<MemoryItem>(&data) {
503 new_index.push(IndexEntry::from(&item));
504 }
505 }
506 }
507 }
508 let count = new_index.len();
509 *self.index.write().await = new_index;
510 self.save_index().await?;
511 Ok(count)
512 }
513}
514
515#[async_trait::async_trait]
516impl MemoryStore for FileMemoryStore {
517 async fn store(&self, item: MemoryItem) -> anyhow::Result<()> {
518 let mut item = item;
519 item.id = Self::safe_id(&item.id);
520 self.save_item(&item).await?;
521 let entry = IndexEntry::from(&item);
522 let mut index = self.index.write().await;
523 if let Some(pos) = index.iter().position(|e| e.id == item.id) {
524 index[pos] = entry;
525 } else {
526 index.push(entry);
527 }
528 drop(index);
529 self.save_index().await
530 }
531
532 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
533 let path = self.item_path(id);
534 if !path.exists() {
535 return Ok(None);
536 }
537 let data = tokio::fs::read_to_string(&path).await?;
538 let mut item: MemoryItem = serde_json::from_str(&data)?;
539 item.content_lower = item.content.to_lowercase();
540 Ok(Some(item))
541 }
542
543 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
544 let query_lower = query.to_lowercase();
545 let index = self.index.read().await;
546 let now = Utc::now();
547 let config = RelevanceConfig::default();
548 let mut matches: Vec<&IndexEntry> = index
549 .iter()
550 .filter(|e| e.content_lower.contains(&query_lower))
551 .collect();
552 matches.sort_by(|a, b| {
553 index_score(a, now, &config)
554 .partial_cmp(&index_score(b, now, &config))
555 .unwrap_or(std::cmp::Ordering::Equal)
556 .reverse()
557 });
558 let ids: Vec<String> = matches.iter().take(limit).map(|e| e.id.clone()).collect();
559 drop(index);
560 let mut items = Vec::with_capacity(ids.len());
561 for id in ids {
562 if let Some(item) = self.retrieve(&id).await? {
563 items.push(item);
564 }
565 }
566 sort_by_relevance(&mut items);
567 Ok(items)
568 }
569
570 async fn search_by_tags(
571 &self,
572 tags: &[String],
573 limit: usize,
574 ) -> anyhow::Result<Vec<MemoryItem>> {
575 let index = self.index.read().await;
576 let now = Utc::now();
577 let config = RelevanceConfig::default();
578 let mut matches: Vec<&IndexEntry> = index
579 .iter()
580 .filter(|e| tags.iter().any(|t| e.tags.contains(t)))
581 .collect();
582 matches.sort_by(|a, b| {
583 index_score(a, now, &config)
584 .partial_cmp(&index_score(b, now, &config))
585 .unwrap_or(std::cmp::Ordering::Equal)
586 .reverse()
587 });
588 let ids: Vec<String> = matches.iter().take(limit).map(|e| e.id.clone()).collect();
589 drop(index);
590 let mut items = Vec::with_capacity(ids.len());
591 for id in ids {
592 if let Some(item) = self.retrieve(&id).await? {
593 items.push(item);
594 }
595 }
596 sort_by_relevance(&mut items);
597 Ok(items)
598 }
599
600 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
601 let index = self.index.read().await;
602 let mut sorted: Vec<&IndexEntry> = index.iter().collect();
603 sorted.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
604 let ids: Vec<String> = sorted.iter().take(limit).map(|e| e.id.clone()).collect();
605 drop(index);
606 let mut items = Vec::with_capacity(ids.len());
607 for id in ids {
608 if let Some(item) = self.retrieve(&id).await? {
609 items.push(item);
610 }
611 }
612 items.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
613 Ok(items)
614 }
615
616 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
617 let index = self.index.read().await;
618 let mut matches: Vec<&IndexEntry> =
619 index.iter().filter(|e| e.importance >= threshold).collect();
620 matches.sort_by(|a, b| {
621 b.importance
622 .partial_cmp(&a.importance)
623 .unwrap_or(std::cmp::Ordering::Equal)
624 });
625 let ids: Vec<String> = matches.iter().take(limit).map(|e| e.id.clone()).collect();
626 drop(index);
627 let mut items = Vec::with_capacity(ids.len());
628 for id in ids {
629 if let Some(item) = self.retrieve(&id).await? {
630 items.push(item);
631 }
632 }
633 items.sort_by(|a, b| {
634 b.importance
635 .partial_cmp(&a.importance)
636 .unwrap_or(std::cmp::Ordering::Equal)
637 });
638 Ok(items)
639 }
640
641 async fn delete(&self, id: &str) -> anyhow::Result<()> {
642 let path = self.item_path(id);
643 if path.exists() {
644 tokio::fs::remove_file(&path).await?;
645 }
646 let mut index = self.index.write().await;
647 index.retain(|e| e.id != id);
648 drop(index);
649 self.save_index().await
650 }
651
652 async fn clear(&self) -> anyhow::Result<()> {
653 let mut entries = tokio::fs::read_dir(&self.items_dir).await?;
654 while let Some(entry) = entries.next_entry().await? {
655 let path = entry.path();
656 if path.extension().is_some_and(|ext| ext == "json") {
657 let _ = tokio::fs::remove_file(&path).await;
658 }
659 }
660 self.index.write().await.clear();
661 self.save_index().await
662 }
663
664 async fn count(&self) -> anyhow::Result<usize> {
665 Ok(self.index.read().await.len())
666 }
667
668 async fn prune(&self, policy: &PrunePolicy) -> anyhow::Result<usize> {
669 let now = Utc::now();
670 let cutoff = now - chrono::Duration::days(policy.max_age_days as i64);
671 let min_importance = policy.min_importance_to_keep;
672
673 let phase1_ids: Vec<String> = {
675 let index = self.index.read().await;
676 index
677 .iter()
678 .filter(|e| e.importance < min_importance && e.timestamp < cutoff)
679 .map(|e| e.id.clone())
680 .collect()
681 };
682 let mut deleted = phase1_ids.len();
683 for id in &phase1_ids {
684 self.delete(id).await?;
685 }
686
687 if policy.max_items > 0 {
689 let config = RelevanceConfig::default();
690 let phase2_ids: Vec<String> = {
691 let index = self.index.read().await;
692 if index.len() <= policy.max_items {
693 Vec::new()
694 } else {
695 let mut entries: Vec<&IndexEntry> = index.iter().collect();
696 entries.sort_by(|a, b| {
697 index_score(b, now, &config)
698 .partial_cmp(&index_score(a, now, &config))
699 .unwrap_or(std::cmp::Ordering::Equal)
700 });
701 entries[policy.max_items..]
702 .iter()
703 .map(|e| e.id.clone())
704 .collect()
705 }
706 };
707 deleted += phase2_ids.len();
708 for id in &phase2_ids {
709 self.delete(id).await?;
710 }
711 }
712
713 Ok(deleted)
714 }
715}
716
717#[cfg(test)]
722mod tests {
723 use super::*;
724
725 #[test]
728 fn test_memory_item_creation() {
729 let item = MemoryItem::new("Test memory")
730 .with_importance(0.8)
731 .with_tag("test")
732 .with_type(MemoryType::Semantic);
733 assert_eq!(item.content, "Test memory");
734 assert_eq!(item.importance, 0.8);
735 assert_eq!(item.tags, vec!["test"]);
736 assert_eq!(item.memory_type, MemoryType::Semantic);
737 }
738
739 #[test]
740 fn test_memory_item_importance_clamped() {
741 assert_eq!(MemoryItem::new("x").with_importance(1.5).importance, 1.0);
742 assert_eq!(MemoryItem::new("x").with_importance(-0.5).importance, 0.0);
743 }
744
745 #[test]
746 fn test_memory_item_record_access() {
747 let mut item = MemoryItem::new("test");
748 assert_eq!(item.access_count, 0);
749 item.record_access();
750 assert_eq!(item.access_count, 1);
751 assert!(item.last_accessed.is_some());
752 }
753
754 #[test]
755 fn test_memory_item_default_type_is_episodic() {
756 assert_eq!(MemoryItem::new("test").memory_type, MemoryType::Episodic);
757 }
758
759 #[test]
760 fn test_memory_item_all_types() {
761 assert_eq!(
762 MemoryItem::new("e")
763 .with_type(MemoryType::Episodic)
764 .memory_type,
765 MemoryType::Episodic
766 );
767 assert_eq!(
768 MemoryItem::new("s")
769 .with_type(MemoryType::Semantic)
770 .memory_type,
771 MemoryType::Semantic
772 );
773 assert_eq!(
774 MemoryItem::new("p")
775 .with_type(MemoryType::Procedural)
776 .memory_type,
777 MemoryType::Procedural
778 );
779 assert_eq!(
780 MemoryItem::new("w")
781 .with_type(MemoryType::Working)
782 .memory_type,
783 MemoryType::Working
784 );
785 }
786
787 #[test]
790 fn test_relevance_score_uses_config() {
791 let item = MemoryItem::new("test").with_importance(1.0);
792 let now = Utc::now();
793
794 let config_importance = RelevanceConfig {
796 decay_days: 30.0,
797 importance_weight: 0.9,
798 recency_weight: 0.1,
799 };
800 let score = item.relevance_score_at(now, &config_importance);
801 assert!(score > 0.95, "score was {score}");
802
803 let config_fast_decay = RelevanceConfig {
805 decay_days: 1.0,
806 importance_weight: 0.7,
807 recency_weight: 0.3,
808 };
809 let score2 = item.relevance_score_at(now, &config_fast_decay);
810 assert!(score2 > 0.9, "score was {score2}");
811 }
812
813 #[test]
814 fn test_relevance_score_decays_with_age() {
815 let mut old_item = MemoryItem::new("old").with_importance(0.5);
816 old_item.timestamp = Utc::now() - chrono::Duration::days(60);
817 let config = RelevanceConfig::default(); let score = old_item.relevance_score_at(Utc::now(), &config);
819 assert!(score < 0.45, "score was {score}");
822 }
823
824 #[test]
825 fn test_relevance_score_default_uses_default_config() {
826 let item = MemoryItem::new("test").with_importance(0.9);
827 let score = item.relevance_score();
828 assert!(score > 0.6);
829 }
830
831 #[test]
834 fn test_relevance_config_defaults() {
835 let c = RelevanceConfig::default();
836 assert_eq!(c.decay_days, 30.0);
837 assert_eq!(c.importance_weight, 0.7);
838 assert_eq!(c.recency_weight, 0.3);
839 }
840
841 #[tokio::test]
844 async fn test_in_memory_store_retrieve() {
845 let store = InMemoryStore::new();
846 let item = MemoryItem::new("hello").with_tag("test");
847 store.store(item.clone()).await.unwrap();
848 let r = store.retrieve(&item.id).await.unwrap();
849 assert!(r.is_some());
850 assert_eq!(r.unwrap().content, "hello");
851 }
852
853 #[tokio::test]
854 async fn test_in_memory_store_retrieve_nonexistent() {
855 let store = InMemoryStore::new();
856 assert!(store.retrieve("nope").await.unwrap().is_none());
857 }
858
859 #[tokio::test]
860 async fn test_in_memory_store_upsert() {
861 let store = InMemoryStore::new();
862 let mut item = MemoryItem::new("original");
863 let id = item.id.clone();
864 store.store(item.clone()).await.unwrap();
865 item.content = "updated".to_string();
866 item.content_lower = "updated".to_string();
867 store.store(item).await.unwrap();
868 assert_eq!(store.count().await.unwrap(), 1);
869 assert_eq!(
870 store.retrieve(&id).await.unwrap().unwrap().content,
871 "updated"
872 );
873 }
874
875 #[tokio::test]
876 async fn test_in_memory_store_search_and_tags() {
877 let store = InMemoryStore::new();
878 store
879 .store(MemoryItem::new("create file").with_tag("file"))
880 .await
881 .unwrap();
882 store
883 .store(MemoryItem::new("delete file").with_tag("file"))
884 .await
885 .unwrap();
886 store
887 .store(MemoryItem::new("create dir").with_tag("dir"))
888 .await
889 .unwrap();
890 assert_eq!(store.search("create", 10).await.unwrap().len(), 2);
891 assert_eq!(
892 store
893 .search_by_tags(&["file".to_string()], 10)
894 .await
895 .unwrap()
896 .len(),
897 2
898 );
899 }
900
901 #[tokio::test]
902 async fn test_in_memory_store_search_relevance_order() {
903 let store = InMemoryStore::new();
904 store
905 .store(MemoryItem::new("rust tip").with_importance(0.3))
906 .await
907 .unwrap();
908 store
909 .store(MemoryItem::new("rust trick").with_importance(0.9))
910 .await
911 .unwrap();
912 let results = store.search("rust", 10).await.unwrap();
913 assert_eq!(results.len(), 2);
914 assert!(results[0].importance >= results[1].importance);
915 }
916
917 #[tokio::test]
918 async fn test_in_memory_store_delete_and_clear() {
919 let store = InMemoryStore::new();
920 let item = MemoryItem::new("to delete");
921 let id = item.id.clone();
922 store.store(item).await.unwrap();
923 store.delete(&id).await.unwrap();
924 assert_eq!(store.count().await.unwrap(), 0);
925
926 for i in 0..3 {
927 store
928 .store(MemoryItem::new(format!("item {i}")))
929 .await
930 .unwrap();
931 }
932 store.clear().await.unwrap();
933 assert_eq!(store.count().await.unwrap(), 0);
934 }
935
936 #[tokio::test]
937 async fn test_in_memory_store_get_recent() {
938 let store = InMemoryStore::new();
939 for i in 0..5 {
940 let mut item = MemoryItem::new(format!("item {i}"));
941 item.timestamp = Utc::now() + chrono::Duration::seconds(i as i64);
942 store.store(item).await.unwrap();
943 }
944 let recent = store.get_recent(3).await.unwrap();
945 assert_eq!(recent.len(), 3);
946 assert!(recent[0].timestamp >= recent[1].timestamp);
947 }
948
949 #[tokio::test]
950 async fn test_in_memory_store_get_important() {
951 let store = InMemoryStore::new();
952 store
953 .store(MemoryItem::new("low").with_importance(0.2))
954 .await
955 .unwrap();
956 store
957 .store(MemoryItem::new("high").with_importance(0.9))
958 .await
959 .unwrap();
960 store
961 .store(MemoryItem::new("medium").with_importance(0.5))
962 .await
963 .unwrap();
964 let results = store.get_important(0.7, 10).await.unwrap();
965 assert_eq!(results.len(), 1);
966 assert_eq!(results[0].content, "high");
967 }
968
969 #[test]
970 fn test_in_memory_store_default() {
971 let _store: InMemoryStore = InMemoryStore::default();
972 }
973
974 #[test]
977 fn test_prune_policy_defaults() {
978 let p = PrunePolicy::default();
979 assert_eq!(p.max_age_days, 90);
980 assert_eq!(p.min_importance_to_keep, 0.5);
981 assert_eq!(p.max_items, 0);
982 }
983
984 #[tokio::test]
985 async fn test_prune_removes_old_low_importance() {
986 let store = InMemoryStore::new();
987 let mut old_item = MemoryItem::new("stale memory").with_importance(0.2);
988 old_item.timestamp = Utc::now() - chrono::Duration::days(100);
989 store.store(old_item).await.unwrap();
990
991 let policy = PrunePolicy {
992 max_age_days: 90,
993 min_importance_to_keep: 0.5,
994 max_items: 0,
995 };
996 let deleted = store.prune(&policy).await.unwrap();
997 assert_eq!(deleted, 1);
998 assert_eq!(store.count().await.unwrap(), 0);
999 }
1000
1001 #[tokio::test]
1002 async fn test_prune_keeps_high_importance() {
1003 let store = InMemoryStore::new();
1004 let mut old_item = MemoryItem::new("important memory").with_importance(0.9);
1005 old_item.timestamp = Utc::now() - chrono::Duration::days(100);
1006 store.store(old_item).await.unwrap();
1007
1008 let policy = PrunePolicy {
1009 max_age_days: 90,
1010 min_importance_to_keep: 0.5,
1011 max_items: 0,
1012 };
1013 let deleted = store.prune(&policy).await.unwrap();
1014 assert_eq!(deleted, 0);
1015 assert_eq!(store.count().await.unwrap(), 1);
1016 }
1017
1018 #[tokio::test]
1019 async fn test_prune_max_items() {
1020 let store = InMemoryStore::new();
1021 for i in 0..10 {
1022 store
1023 .store(MemoryItem::new(format!("item {i}")).with_importance(i as f32 * 0.1))
1024 .await
1025 .unwrap();
1026 }
1027 let policy = PrunePolicy {
1028 max_age_days: 9999,
1029 min_importance_to_keep: 0.0,
1030 max_items: 5,
1031 };
1032 let deleted = store.prune(&policy).await.unwrap();
1033 assert_eq!(deleted, 5);
1034 assert_eq!(store.count().await.unwrap(), 5);
1035 }
1036
1037 #[tokio::test]
1038 async fn test_prune_keeps_recent_low_importance() {
1039 let store = InMemoryStore::new();
1041 store
1042 .store(MemoryItem::new("fresh").with_importance(0.1))
1043 .await
1044 .unwrap();
1045
1046 let policy = PrunePolicy {
1047 max_age_days: 90,
1048 min_importance_to_keep: 0.5,
1049 max_items: 0,
1050 };
1051 let deleted = store.prune(&policy).await.unwrap();
1052 assert_eq!(deleted, 0);
1053 assert_eq!(store.count().await.unwrap(), 1);
1054 }
1055}
1056
1057#[cfg(test)]
1058mod file_memory_store_tests {
1059 use super::*;
1060 use tempfile::TempDir;
1061
1062 async fn setup() -> (TempDir, FileMemoryStore) {
1063 let dir = TempDir::new().unwrap();
1064 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1065 (dir, store)
1066 }
1067
1068 #[tokio::test]
1069 async fn test_store_and_retrieve() {
1070 let (_dir, store) = setup().await;
1071 let item = MemoryItem::new("hello world");
1072 let id = item.id.clone();
1073 store.store(item).await.unwrap();
1074 let r = store.retrieve(&id).await.unwrap().unwrap();
1075 assert_eq!(r.content, "hello world");
1076 }
1077
1078 #[tokio::test]
1079 async fn test_retrieve_nonexistent() {
1080 let (_dir, store) = setup().await;
1081 assert!(store.retrieve("nonexistent").await.unwrap().is_none());
1082 }
1083
1084 #[tokio::test]
1085 async fn test_search_by_content() {
1086 let (_dir, store) = setup().await;
1087 store
1088 .store(MemoryItem::new("rust programming"))
1089 .await
1090 .unwrap();
1091 store
1092 .store(MemoryItem::new("python scripting"))
1093 .await
1094 .unwrap();
1095 store
1096 .store(MemoryItem::new("rust async patterns"))
1097 .await
1098 .unwrap();
1099 let results = store.search("rust", 10).await.unwrap();
1100 assert_eq!(results.len(), 2);
1101 }
1102
1103 #[tokio::test]
1104 async fn test_search_limit() {
1105 let (_dir, store) = setup().await;
1106 for i in 0..10 {
1107 store
1108 .store(MemoryItem::new(format!("item {i}")))
1109 .await
1110 .unwrap();
1111 }
1112 assert_eq!(store.search("item", 3).await.unwrap().len(), 3);
1113 }
1114
1115 #[tokio::test]
1116 async fn test_search_by_tags() {
1117 let (_dir, store) = setup().await;
1118 store
1119 .store(MemoryItem::new("one").with_tags(vec!["rust".into(), "async".into()]))
1120 .await
1121 .unwrap();
1122 store
1123 .store(MemoryItem::new("two").with_tags(vec!["python".into()]))
1124 .await
1125 .unwrap();
1126 store
1127 .store(MemoryItem::new("three").with_tags(vec!["rust".into()]))
1128 .await
1129 .unwrap();
1130 assert_eq!(
1131 store
1132 .search_by_tags(&["rust".to_string()], 10)
1133 .await
1134 .unwrap()
1135 .len(),
1136 2
1137 );
1138 }
1139
1140 #[tokio::test]
1141 async fn test_get_recent_ordered() {
1142 let (_dir, store) = setup().await;
1143 for i in 0..5 {
1144 let mut item = MemoryItem::new(format!("item {i}"));
1145 item.timestamp = Utc::now() + chrono::Duration::seconds(i as i64);
1146 store.store(item).await.unwrap();
1147 }
1148 let results = store.get_recent(3).await.unwrap();
1149 assert_eq!(results.len(), 3);
1150 assert!(results[0].timestamp >= results[1].timestamp);
1151 }
1152
1153 #[tokio::test]
1154 async fn test_get_important() {
1155 let (_dir, store) = setup().await;
1156 store
1157 .store(MemoryItem::new("low").with_importance(0.1))
1158 .await
1159 .unwrap();
1160 store
1161 .store(MemoryItem::new("high").with_importance(0.9))
1162 .await
1163 .unwrap();
1164 store
1165 .store(MemoryItem::new("medium").with_importance(0.5))
1166 .await
1167 .unwrap();
1168 let results = store.get_important(0.0, 2).await.unwrap();
1169 assert_eq!(results.len(), 2);
1170 assert!(results[0].importance >= results[1].importance);
1171 }
1172
1173 #[tokio::test]
1174 async fn test_delete() {
1175 let (_dir, store) = setup().await;
1176 let item = MemoryItem::new("to delete");
1177 let id = item.id.clone();
1178 store.store(item).await.unwrap();
1179 store.delete(&id).await.unwrap();
1180 assert_eq!(store.count().await.unwrap(), 0);
1181 assert!(store.retrieve(&id).await.unwrap().is_none());
1182 }
1183
1184 #[tokio::test]
1185 async fn test_delete_nonexistent() {
1186 let (_dir, store) = setup().await;
1187 store.delete("nonexistent").await.unwrap();
1188 }
1189
1190 #[tokio::test]
1191 async fn test_clear() {
1192 let (_dir, store) = setup().await;
1193 for i in 0..5 {
1194 store
1195 .store(MemoryItem::new(format!("item {i}")))
1196 .await
1197 .unwrap();
1198 }
1199 store.clear().await.unwrap();
1200 assert_eq!(store.count().await.unwrap(), 0);
1201 }
1202
1203 #[tokio::test]
1204 async fn test_persistence_across_instances() {
1205 let dir = TempDir::new().unwrap();
1206 {
1207 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1208 store
1209 .store(MemoryItem::new("persistent data").with_tags(vec!["test".into()]))
1210 .await
1211 .unwrap();
1212 }
1213 {
1214 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1215 assert_eq!(store.count().await.unwrap(), 1);
1216 assert_eq!(store.search("persistent", 10).await.unwrap().len(), 1);
1217 }
1218 }
1219
1220 #[tokio::test]
1221 async fn test_rebuild_index() {
1222 let dir = TempDir::new().unwrap();
1223 {
1224 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1225 store.store(MemoryItem::new("alpha")).await.unwrap();
1226 store.store(MemoryItem::new("beta")).await.unwrap();
1227 }
1228 tokio::fs::remove_file(dir.path().join("index.json"))
1229 .await
1230 .unwrap();
1231 {
1232 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1233 assert_eq!(store.count().await.unwrap(), 0);
1234 store.rebuild_index().await.unwrap();
1235 assert_eq!(store.count().await.unwrap(), 2);
1236 }
1237 }
1238
1239 #[tokio::test]
1240 async fn test_path_traversal_prevention() {
1241 let (_dir, store) = setup().await;
1242 let mut item = MemoryItem::new("sneaky");
1243 item.id = "../../../etc/passwd".to_string();
1244 store.store(item).await.unwrap();
1245 let results = store.search("sneaky", 10).await.unwrap();
1246 assert_eq!(results.len(), 1);
1247 assert!(!results[0].id.contains('/'));
1248 assert!(!results[0].id.contains(".."));
1249 }
1250
1251 #[tokio::test]
1252 async fn test_importance_threshold() {
1253 let (_dir, store) = setup().await;
1254 store
1255 .store(MemoryItem::new("low").with_importance(0.2))
1256 .await
1257 .unwrap();
1258 store
1259 .store(MemoryItem::new("high").with_importance(0.8))
1260 .await
1261 .unwrap();
1262 let results = store.get_important(0.5, 10).await.unwrap();
1263 assert_eq!(results.len(), 1);
1264 assert_eq!(results[0].content, "high");
1265 }
1266
1267 #[tokio::test]
1268 async fn test_file_prune_removes_old_low_importance() {
1269 let (_dir, store) = setup().await;
1270 let mut old_item = MemoryItem::new("stale").with_importance(0.2);
1271 old_item.timestamp = Utc::now() - chrono::Duration::days(100);
1272 store.store(old_item).await.unwrap();
1273
1274 let policy = PrunePolicy {
1275 max_age_days: 90,
1276 min_importance_to_keep: 0.5,
1277 max_items: 0,
1278 };
1279 let deleted = store.prune(&policy).await.unwrap();
1280 assert_eq!(deleted, 1);
1281 assert_eq!(store.count().await.unwrap(), 0);
1282 }
1283
1284 #[tokio::test]
1285 async fn test_file_prune_keeps_high_importance() {
1286 let (_dir, store) = setup().await;
1287 let mut old_item = MemoryItem::new("important").with_importance(0.9);
1288 old_item.timestamp = Utc::now() - chrono::Duration::days(100);
1289 store.store(old_item).await.unwrap();
1290
1291 let policy = PrunePolicy {
1292 max_age_days: 90,
1293 min_importance_to_keep: 0.5,
1294 max_items: 0,
1295 };
1296 let deleted = store.prune(&policy).await.unwrap();
1297 assert_eq!(deleted, 0);
1298 assert_eq!(store.count().await.unwrap(), 1);
1299 }
1300
1301 #[tokio::test]
1302 async fn test_file_prune_max_items() {
1303 let (_dir, store) = setup().await;
1304 for i in 0..10 {
1305 store
1306 .store(MemoryItem::new(format!("item {i}")).with_importance(i as f32 * 0.1))
1307 .await
1308 .unwrap();
1309 }
1310 let policy = PrunePolicy {
1311 max_age_days: 9999,
1312 min_importance_to_keep: 0.0,
1313 max_items: 5,
1314 };
1315 let deleted = store.prune(&policy).await.unwrap();
1316 assert_eq!(deleted, 5);
1317 assert_eq!(store.count().await.unwrap(), 5);
1318 }
1319}