1#[cfg(feature = "sqlite")]
17pub mod sqlite;
18
19#[cfg(feature = "sqlite")]
20pub use sqlite::SqliteMemoryStore;
21
22use anyhow::Context as _;
23use chrono::{DateTime, Utc};
24use serde::{Deserialize, Serialize};
25use std::collections::{HashMap, HashSet};
26use std::sync::OnceLock;
27use tokio::sync::RwLock;
28
29const MIN_DEDUPE_FINGERPRINT_CHARS: usize = 24;
30const MIN_NEAR_DEDUPE_TERMS: usize = 5;
31const NEAR_DEDUPE_JACCARD_THRESHOLD: f32 = 0.86;
32const PRUNE_PROTECTED_ACCESS_COUNT: u32 = 3;
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct RelevanceConfig {
42 #[serde(default = "RelevanceConfig::default_decay_days")]
44 pub decay_days: f32,
45 #[serde(default = "RelevanceConfig::default_importance_weight")]
47 pub importance_weight: f32,
48 #[serde(default = "RelevanceConfig::default_recency_weight")]
50 pub recency_weight: f32,
51}
52
53impl RelevanceConfig {
54 fn default_decay_days() -> f32 {
55 30.0
56 }
57 fn default_importance_weight() -> f32 {
58 0.7
59 }
60 fn default_recency_weight() -> f32 {
61 0.3
62 }
63}
64
65impl Default for RelevanceConfig {
66 fn default() -> Self {
67 Self {
68 decay_days: 30.0,
69 importance_weight: 0.7,
70 recency_weight: 0.3,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct PrunePolicy {
79 #[serde(default = "PrunePolicy::default_max_age_days")]
81 pub max_age_days: u32,
82 #[serde(default = "PrunePolicy::default_min_importance_to_keep")]
85 pub min_importance_to_keep: f32,
86 #[serde(default)]
89 pub max_items: usize,
90}
91
92impl PrunePolicy {
93 fn default_max_age_days() -> u32 {
94 90
95 }
96 fn default_min_importance_to_keep() -> f32 {
97 0.5
98 }
99}
100
101impl Default for PrunePolicy {
102 fn default() -> Self {
103 Self {
104 max_age_days: 90,
105 min_importance_to_keep: 0.5,
106 max_items: 0,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct MemoryItem {
118 pub id: String,
119 pub content: String,
120 pub timestamp: DateTime<Utc>,
121 pub importance: f32,
122 pub tags: Vec<String>,
123 pub memory_type: MemoryType,
124 pub metadata: HashMap<String, String>,
125 pub access_count: u32,
126 pub last_accessed: Option<DateTime<Utc>>,
127 #[serde(skip)]
128 pub content_lower: String,
129}
130
131impl MemoryItem {
132 pub fn new(content: impl Into<String>) -> Self {
133 let content = content.into();
134 let content_lower = content.to_lowercase();
135 Self {
136 id: uuid::Uuid::new_v4().to_string(),
137 content,
138 timestamp: Utc::now(),
139 importance: 0.5,
140 tags: Vec::new(),
141 memory_type: MemoryType::Episodic,
142 metadata: HashMap::new(),
143 access_count: 0,
144 last_accessed: None,
145 content_lower,
146 }
147 }
148
149 pub fn with_importance(mut self, importance: f32) -> Self {
150 self.importance = importance.clamp(0.0, 1.0);
151 self
152 }
153
154 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
155 self.tags = tags;
156 self
157 }
158
159 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
160 self.tags.push(tag.into());
161 self
162 }
163
164 pub fn with_type(mut self, memory_type: MemoryType) -> Self {
165 self.memory_type = memory_type;
166 self
167 }
168
169 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
170 self.metadata.insert(key.into(), value.into());
171 self
172 }
173
174 pub fn content_fingerprint(&self) -> Option<String> {
180 memory_content_fingerprint(&self.content)
181 }
182
183 pub fn merge_duplicate(self, incoming: MemoryItem) -> MemoryItem {
188 merge_duplicate_memory_item(self, incoming)
189 }
190
191 pub fn record_access(&mut self) {
192 self.access_count += 1;
193 self.last_accessed = Some(Utc::now());
194 }
195
196 pub fn relevance_score_at(&self, now: DateTime<Utc>, config: &RelevanceConfig) -> f32 {
198 let age_days = (now - self.timestamp).num_seconds() as f32 / 86400.0;
199 let decay = (-age_days / config.decay_days).exp();
200 self.importance * config.importance_weight + decay * config.recency_weight
201 }
202
203 pub fn relevance_score(&self) -> f32 {
205 self.relevance_score_at(Utc::now(), &RelevanceConfig::default())
206 }
207}
208
209fn normalize_item_for_store(mut item: MemoryItem) -> MemoryItem {
210 item.content_lower = item.content.to_lowercase();
211 item
212}
213
214fn memory_content_fingerprint(content: &str) -> Option<String> {
215 let mut tokens = Vec::new();
216 let mut current = String::new();
217 for ch in content.chars().flat_map(char::to_lowercase) {
218 if ch.is_alphanumeric() {
219 current.push(ch);
220 } else if !current.is_empty() {
221 tokens.push(std::mem::take(&mut current));
222 }
223 }
224 if !current.is_empty() {
225 tokens.push(current);
226 }
227
228 let fingerprint = tokens.join(" ");
229 if fingerprint.chars().count() < MIN_DEDUPE_FINGERPRINT_CHARS {
230 None
231 } else {
232 Some(fingerprint)
233 }
234}
235
236fn memories_are_store_duplicates(existing: &MemoryItem, incoming: &MemoryItem) -> bool {
237 if existing.id == incoming.id {
238 return true;
239 }
240 if existing.content_fingerprint().is_some()
241 && existing.content_fingerprint() == incoming.content_fingerprint()
242 {
243 return true;
244 }
245 memory_items_are_near_duplicates(existing, incoming)
246}
247
248fn memory_index_entry_is_duplicate(entry: &IndexEntry, incoming: &MemoryItem) -> bool {
249 if entry.id == incoming.id {
250 return true;
251 }
252 if incoming.content_fingerprint().is_some()
253 && memory_content_fingerprint(&entry.content_lower) == incoming.content_fingerprint()
254 {
255 return true;
256 }
257 memory_contents_are_near_duplicates(&entry.content_lower, entry.memory_type, incoming)
258}
259
260fn memory_items_are_near_duplicates(existing: &MemoryItem, incoming: &MemoryItem) -> bool {
261 memory_contents_are_near_duplicates(&existing.content, existing.memory_type, incoming)
262}
263
264fn memory_contents_are_near_duplicates(
265 existing_content: &str,
266 existing_type: MemoryType,
267 incoming: &MemoryItem,
268) -> bool {
269 if existing_type != incoming.memory_type {
270 return false;
271 }
272 if has_conflicting_dedupe_polarity(existing_content, &incoming.content) {
273 return false;
274 }
275
276 let existing_terms = dedupe_terms(existing_content);
277 let incoming_terms = dedupe_terms(&incoming.content);
278 if existing_terms.len() < MIN_NEAR_DEDUPE_TERMS || incoming_terms.len() < MIN_NEAR_DEDUPE_TERMS
279 {
280 return false;
281 }
282
283 let overlap = existing_terms.intersection(&incoming_terms).count();
284 let union = existing_terms.len() + incoming_terms.len() - overlap;
285 union > 0 && overlap as f32 / union as f32 >= NEAR_DEDUPE_JACCARD_THRESHOLD
286}
287
288fn dedupe_terms(content: &str) -> HashSet<String> {
289 content
290 .to_ascii_lowercase()
291 .split(|ch: char| !(ch.is_alphanumeric() || matches!(ch, '-' | '_' | '.' | '/')))
292 .map(str::trim)
293 .filter(|term| term.chars().count() >= 3)
294 .filter(|term| !is_dedupe_stopword(term))
295 .map(ToOwned::to_owned)
296 .collect()
297}
298
299fn has_conflicting_dedupe_polarity(left: &str, right: &str) -> bool {
300 has_negation_term(left) != has_negation_term(right)
301}
302
303fn has_negation_term(content: &str) -> bool {
304 content
305 .to_ascii_lowercase()
306 .split(|ch: char| !ch.is_alphanumeric())
307 .any(|term| {
308 matches!(
309 term,
310 "not" | "never" | "no" | "avoid" | "without" | "disable"
311 )
312 })
313}
314
315fn is_dedupe_stopword(term: &str) -> bool {
316 matches!(
317 term,
318 "the"
319 | "and"
320 | "for"
321 | "with"
322 | "after"
323 | "before"
324 | "from"
325 | "that"
326 | "this"
327 | "when"
328 | "then"
329 | "than"
330 | "into"
331 | "must"
332 | "should"
333 | "would"
334 | "could"
335 | "about"
336 )
337}
338
339fn merge_duplicate_memory_item(existing: MemoryItem, incoming: MemoryItem) -> MemoryItem {
340 let incoming = normalize_item_for_store(incoming);
341 let mut merged = existing.clone();
342
343 if should_replace_duplicate_content(&existing, &incoming) {
344 merged.content = incoming.content.clone();
345 merged.content_lower = incoming.content_lower.clone();
346 }
347 merged.importance = existing.importance.max(incoming.importance);
348 merged.timestamp = existing.timestamp.max(incoming.timestamp);
349 merged.memory_type = stronger_memory_type(existing.memory_type, incoming.memory_type);
350 merged.access_count = existing.access_count.max(incoming.access_count);
351 merged.last_accessed = max_optional_datetime(existing.last_accessed, incoming.last_accessed);
352
353 merge_tags(&mut merged.tags, &incoming.tags);
354 merge_metadata(&mut merged.metadata, &incoming.metadata);
355 record_duplicate_metadata(&mut merged.metadata, &incoming.id);
356
357 normalize_item_for_store(merged)
358}
359
360fn should_replace_duplicate_content(existing: &MemoryItem, incoming: &MemoryItem) -> bool {
361 incoming.importance > existing.importance
362 || (incoming.importance == existing.importance
363 && incoming.content.chars().count() > existing.content.chars().count())
364}
365
366fn stronger_memory_type(existing: MemoryType, incoming: MemoryType) -> MemoryType {
367 if memory_type_strength(incoming) > memory_type_strength(existing) {
368 incoming
369 } else {
370 existing
371 }
372}
373
374fn memory_type_strength(memory_type: MemoryType) -> u8 {
375 match memory_type {
376 MemoryType::Procedural | MemoryType::Semantic => 3,
377 MemoryType::Working => 2,
378 MemoryType::Episodic => 1,
379 }
380}
381
382fn max_optional_datetime(
383 left: Option<DateTime<Utc>>,
384 right: Option<DateTime<Utc>>,
385) -> Option<DateTime<Utc>> {
386 match (left, right) {
387 (Some(left), Some(right)) => Some(left.max(right)),
388 (Some(left), None) => Some(left),
389 (None, Some(right)) => Some(right),
390 (None, None) => None,
391 }
392}
393
394fn merge_tags(existing: &mut Vec<String>, incoming: &[String]) {
395 for tag in incoming {
396 if !existing.contains(tag) {
397 existing.push(tag.clone());
398 }
399 }
400}
401
402fn merge_metadata(existing: &mut HashMap<String, String>, incoming: &HashMap<String, String>) {
403 for (key, value) in incoming {
404 if value.trim().is_empty() {
405 continue;
406 }
407 match existing.get_mut(key) {
408 Some(current) if current == value => {}
409 Some(current) if is_list_metadata_key(key) => {
410 *current = merge_metadata_list(current, value);
411 }
412 Some(_) => {}
413 None => {
414 existing.insert(key.clone(), value.clone());
415 }
416 }
417 }
418}
419
420fn is_list_metadata_key(key: &str) -> bool {
421 matches!(
422 key,
423 "supersedes" | "conflicts_with" | "tools" | "aliases" | "entity_aliases"
424 )
425}
426
427fn merge_metadata_list(existing: &str, incoming: &str) -> String {
428 let mut values = Vec::new();
429 for raw in existing.split(',').chain(incoming.split(',')) {
430 let value = raw.trim();
431 if !value.is_empty() && !values.iter().any(|seen| seen == value) {
432 values.push(value.to_string());
433 }
434 }
435 values.join(",")
436}
437
438fn record_duplicate_metadata(metadata: &mut HashMap<String, String>, incoming_id: &str) {
439 let count = metadata
440 .get("duplicate_count")
441 .and_then(|value| value.parse::<u32>().ok())
442 .unwrap_or(0)
443 + 1;
444 metadata.insert("duplicate_count".to_string(), count.to_string());
445 metadata.insert("last_duplicate_at".to_string(), Utc::now().to_rfc3339());
446 if !incoming_id.trim().is_empty() {
447 let duplicate_ids = metadata
448 .get("duplicate_ids")
449 .map(|existing| merge_metadata_list(existing, incoming_id))
450 .unwrap_or_else(|| incoming_id.to_string());
451 metadata.insert("duplicate_ids".to_string(), duplicate_ids);
452 }
453}
454
455fn memory_is_prune_protected(item: &MemoryItem) -> bool {
456 item.access_count >= PRUNE_PROTECTED_ACCESS_COUNT
457 || item.tags.iter().any(|tag| {
458 matches!(
459 tag.as_str(),
460 "keep" | "pinned" | "protected" | "consolidated" | "conflict"
461 )
462 })
463 || metadata_truthy(&item.metadata, "keep")
464 || metadata_truthy(&item.metadata, "pinned")
465 || metadata_truthy(&item.metadata, "protected")
466 || metadata_nonempty(&item.metadata, "supersedes")
467 || metadata_nonempty(&item.metadata, "conflicts_with")
468}
469
470fn metadata_truthy(metadata: &HashMap<String, String>, key: &str) -> bool {
471 metadata
472 .get(key)
473 .map(|value| {
474 matches!(
475 value.trim().to_ascii_lowercase().as_str(),
476 "1" | "true" | "yes" | "keep" | "pinned" | "protected"
477 )
478 })
479 .unwrap_or(false)
480}
481
482fn metadata_nonempty(metadata: &HashMap<String, String>, key: &str) -> bool {
483 metadata
484 .get(key)
485 .is_some_and(|value| !value.trim().is_empty())
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
490#[serde(rename_all = "snake_case")]
491pub enum MemoryType {
492 Episodic,
493 Semantic,
494 Procedural,
495 Working,
496}
497
498#[async_trait::async_trait]
503pub trait MemoryStore: Send + Sync {
504 async fn store(&self, item: MemoryItem) -> anyhow::Result<()>;
505 async fn store_and_return(&self, item: MemoryItem) -> anyhow::Result<MemoryItem> {
511 self.store(item.clone()).await?;
512 Ok(item)
513 }
514 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>>;
515 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
516 async fn search_by_tags(
517 &self,
518 tags: &[String],
519 limit: usize,
520 ) -> anyhow::Result<Vec<MemoryItem>>;
521 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
522 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>>;
523 async fn delete(&self, id: &str) -> anyhow::Result<()>;
524 async fn clear(&self) -> anyhow::Result<()>;
525 async fn count(&self) -> anyhow::Result<usize>;
526
527 async fn prune(&self, policy: &PrunePolicy) -> anyhow::Result<usize> {
532 let _ = policy;
533 Ok(0)
534 }
535}
536
537fn index_score(entry: &IndexEntry, now: DateTime<Utc>, config: &RelevanceConfig) -> f32 {
543 let age_days = (now - entry.timestamp).num_seconds() as f32 / 86400.0;
544 let decay = (-age_days / config.decay_days).exp();
545 entry.importance * config.importance_weight + decay * config.recency_weight
546}
547
548fn sort_by_relevance(items: &mut [MemoryItem]) {
549 let now = Utc::now();
550 let config = RelevanceConfig::default();
551 items.sort_by(|a, b| {
552 b.relevance_score_at(now, &config)
553 .partial_cmp(&a.relevance_score_at(now, &config))
554 .unwrap_or(std::cmp::Ordering::Equal)
555 });
556}
557
558fn memory_type_to_query_key(memory_type: MemoryType) -> &'static str {
559 match memory_type {
560 MemoryType::Episodic => "episodic",
561 MemoryType::Semantic => "semantic",
562 MemoryType::Procedural => "procedural",
563 MemoryType::Working => "working",
564 }
565}
566
567fn query_terms(query: &str) -> Vec<String> {
568 let mut terms: Vec<String> = query
569 .to_lowercase()
570 .split(|ch: char| {
571 !(ch.is_alphanumeric() || matches!(ch, '/' | '\\' | '_' | '-' | '.' | ':' | '@'))
572 })
573 .map(str::trim)
574 .filter(|term| term.chars().count() >= 2)
575 .map(ToOwned::to_owned)
576 .collect();
577 terms.sort();
578 terms.dedup();
579 terms
580}
581
582fn lexical_match_score(
583 content_lower: &str,
584 tags: &[String],
585 memory_type: MemoryType,
586 query_lower: &str,
587 terms: &[String],
588) -> Option<f32> {
589 if query_lower.trim().is_empty() {
590 return Some(0.0);
591 }
592
593 let mut score = 0.0;
594 let mut matched_terms = 0usize;
595 if !query_lower.is_empty() && content_lower.contains(query_lower) {
596 score += 1.25;
597 }
598
599 let memory_type = memory_type_to_query_key(memory_type);
600 for term in terms {
601 let mut matched = false;
602 if content_lower.contains(term) {
603 score += 0.35;
604 matched = true;
605 }
606 if tags.iter().any(|tag| tag.to_lowercase().contains(term)) {
607 score += 0.55;
608 matched = true;
609 }
610 if memory_type.contains(term) {
611 score += 0.20;
612 matched = true;
613 }
614 if matched {
615 matched_terms += 1;
616 }
617 }
618
619 if score <= 0.0 {
620 return None;
621 }
622
623 if !terms.is_empty() {
624 score += matched_terms as f32 / terms.len() as f32;
625 }
626 Some(score)
627}
628
629fn index_search_score(
630 entry: &IndexEntry,
631 now: DateTime<Utc>,
632 config: &RelevanceConfig,
633 query_lower: &str,
634 terms: &[String],
635) -> Option<f32> {
636 let lexical = lexical_match_score(
637 &entry.content_lower,
638 &entry.tags,
639 entry.memory_type,
640 query_lower,
641 terms,
642 )?;
643 Some(index_score(entry, now, config) + lexical)
644}
645
646pub struct InMemoryStore {
654 items: RwLock<Vec<MemoryItem>>,
655}
656
657impl Default for InMemoryStore {
658 fn default() -> Self {
659 Self::new()
660 }
661}
662
663impl InMemoryStore {
664 pub fn new() -> Self {
665 Self {
666 items: RwLock::new(Vec::new()),
667 }
668 }
669}
670
671#[async_trait::async_trait]
672impl MemoryStore for InMemoryStore {
673 async fn store(&self, item: MemoryItem) -> anyhow::Result<()> {
674 self.store_and_return(item).await.map(|_| ())
675 }
676
677 async fn store_and_return(&self, item: MemoryItem) -> anyhow::Result<MemoryItem> {
678 let item = normalize_item_for_store(item);
679 let mut items = self.items.write().await;
680 if let Some(pos) = items.iter().position(|i| i.id == item.id) {
681 items[pos] = item.clone();
682 return Ok(item);
683 }
684
685 if let Some(pos) = items
686 .iter()
687 .position(|existing| memories_are_store_duplicates(existing, &item))
688 {
689 let merged = merge_duplicate_memory_item(items[pos].clone(), item);
690 items[pos] = merged.clone();
691 return Ok(merged);
692 }
693
694 items.push(item.clone());
695 Ok(item)
696 }
697
698 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
699 let mut items = self.items.write().await;
700 let Some(item) = items.iter_mut().find(|i| i.id == id) else {
701 return Ok(None);
702 };
703 item.record_access();
704 Ok(Some(item.clone()))
705 }
706
707 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
708 let query_lower = query.to_lowercase();
709 let config = RelevanceConfig::default();
710 let now = Utc::now();
711 let terms = query_terms(&query_lower);
712 let mut items = self.items.write().await;
713 let mut scored: Vec<(usize, f32)> = items
714 .iter()
715 .enumerate()
716 .filter_map(|(idx, item)| {
717 let lexical = lexical_match_score(
718 &item.content_lower,
719 &item.tags,
720 item.memory_type,
721 &query_lower,
722 &terms,
723 )?;
724 Some((idx, item.relevance_score_at(now, &config) + lexical))
725 })
726 .collect();
727 scored.sort_by(|a, b| {
728 b.1.partial_cmp(&a.1)
729 .unwrap_or(std::cmp::Ordering::Equal)
730 .then_with(|| items[a.0].timestamp.cmp(&items[b.0].timestamp))
731 });
732 let ids: Vec<usize> = scored.into_iter().take(limit).map(|(idx, _)| idx).collect();
733 let mut matches = Vec::with_capacity(ids.len());
734 for idx in ids {
735 items[idx].record_access();
736 matches.push(items[idx].clone());
737 }
738 Ok(matches)
739 }
740
741 async fn search_by_tags(
742 &self,
743 tags: &[String],
744 limit: usize,
745 ) -> anyhow::Result<Vec<MemoryItem>> {
746 let config = RelevanceConfig::default();
747 let now = Utc::now();
748 let mut items = self.items.write().await;
749 let mut scored: Vec<(usize, f32)> = items
750 .iter()
751 .enumerate()
752 .filter(|(_, item)| tags.iter().any(|t| item.tags.contains(t)))
753 .map(|(idx, item)| (idx, item.relevance_score_at(now, &config)))
754 .collect();
755 scored.sort_by(|a, b| {
756 b.1.partial_cmp(&a.1)
757 .unwrap_or(std::cmp::Ordering::Equal)
758 .then_with(|| items[a.0].timestamp.cmp(&items[b.0].timestamp))
759 });
760 let ids: Vec<usize> = scored.into_iter().take(limit).map(|(idx, _)| idx).collect();
761 let mut matches = Vec::with_capacity(ids.len());
762 for idx in ids {
763 items[idx].record_access();
764 matches.push(items[idx].clone());
765 }
766 Ok(matches)
767 }
768
769 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
770 let items = self.items.read().await;
771 let mut sorted: Vec<MemoryItem> = items.iter().cloned().collect();
772 sorted.sort_by_key(|item| std::cmp::Reverse(item.timestamp));
773 sorted.truncate(limit);
774 Ok(sorted)
775 }
776
777 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
778 let items = self.items.read().await;
779 let mut matches: Vec<MemoryItem> = items
780 .iter()
781 .filter(|i| i.importance >= threshold)
782 .cloned()
783 .collect();
784 matches.sort_by(|a, b| {
785 b.importance
786 .partial_cmp(&a.importance)
787 .unwrap_or(std::cmp::Ordering::Equal)
788 });
789 matches.truncate(limit);
790 Ok(matches)
791 }
792
793 async fn delete(&self, id: &str) -> anyhow::Result<()> {
794 self.items.write().await.retain(|i| i.id != id);
795 Ok(())
796 }
797
798 async fn clear(&self) -> anyhow::Result<()> {
799 self.items.write().await.clear();
800 Ok(())
801 }
802
803 async fn count(&self) -> anyhow::Result<usize> {
804 Ok(self.items.read().await.len())
805 }
806
807 async fn prune(&self, policy: &PrunePolicy) -> anyhow::Result<usize> {
808 let now = Utc::now();
809 let cutoff = now - chrono::Duration::days(policy.max_age_days as i64);
810 let min_importance = policy.min_importance_to_keep;
811
812 let mut items = self.items.write().await;
813 let before = items.len();
814
815 items.retain(|item| {
818 memory_is_prune_protected(item)
819 || item.importance >= min_importance
820 || item.timestamp >= cutoff
821 });
822
823 if policy.max_items > 0 && items.len() > policy.max_items {
826 let config = RelevanceConfig::default();
827 let protected_count = items
828 .iter()
829 .filter(|item| memory_is_prune_protected(item))
830 .count();
831 let unprotected_to_keep = policy.max_items.saturating_sub(protected_count);
832 let mut unprotected_seen = 0usize;
833 items.sort_by(|a, b| {
834 memory_is_prune_protected(b)
835 .cmp(&memory_is_prune_protected(a))
836 .then_with(|| {
837 b.relevance_score_at(now, &config)
838 .partial_cmp(&a.relevance_score_at(now, &config))
839 .unwrap_or(std::cmp::Ordering::Equal)
840 })
841 });
842 items.retain(|item| {
843 if memory_is_prune_protected(item) {
844 true
845 } else if unprotected_seen < unprotected_to_keep {
846 unprotected_seen += 1;
847 true
848 } else {
849 false
850 }
851 });
852 }
853
854 Ok(before - items.len())
855 }
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
863struct IndexEntry {
864 id: String,
865 content_lower: String,
866 tags: Vec<String>,
867 importance: f32,
868 timestamp: DateTime<Utc>,
869 memory_type: MemoryType,
870}
871
872impl From<&MemoryItem> for IndexEntry {
873 fn from(item: &MemoryItem) -> Self {
874 Self {
875 id: item.id.clone(),
876 content_lower: item.content.to_lowercase(),
877 tags: item.tags.clone(),
878 importance: item.importance,
879 timestamp: item.timestamp,
880 memory_type: item.memory_type,
881 }
882 }
883}
884
885pub struct FileMemoryStore {
893 items_dir: std::path::PathBuf,
894 index_path: std::path::PathBuf,
895 index: RwLock<Vec<IndexEntry>>,
896}
897
898static FILE_MEMORY_INDEX_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
899
900fn file_memory_index_lock() -> &'static tokio::sync::Mutex<()> {
901 FILE_MEMORY_INDEX_LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
902}
903
904impl FileMemoryStore {
905 pub async fn new(dir: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
906 let dir = dir.as_ref().to_path_buf();
907 let items_dir = dir.join("items");
908 let index_path = dir.join("index.json");
909
910 tokio::fs::create_dir_all(&items_dir)
911 .await
912 .with_context(|| {
913 format!("Failed to create memory directory: {}", items_dir.display())
914 })?;
915
916 let index = if index_path.exists() {
917 let data = tokio::fs::read_to_string(&index_path)
918 .await
919 .with_context(|| {
920 format!("Failed to read memory index: {}", index_path.display())
921 })?;
922 serde_json::from_str(&data).unwrap_or_default()
923 } else {
924 Vec::new()
925 };
926
927 Ok(Self {
928 items_dir,
929 index_path,
930 index: RwLock::new(index),
931 })
932 }
933
934 fn safe_id(id: &str) -> String {
935 id.replace(['/', '\\'], "_").replace("..", "_")
936 }
937
938 fn item_path(&self, id: &str) -> std::path::PathBuf {
939 self.items_dir.join(format!("{}.json", Self::safe_id(id)))
940 }
941
942 async fn read_index_from_disk(&self) -> anyhow::Result<Vec<IndexEntry>> {
943 if !self.index_path.exists() {
944 return Ok(Vec::new());
945 }
946 let data = tokio::fs::read_to_string(&self.index_path)
947 .await
948 .with_context(|| {
949 format!("Failed to read memory index: {}", self.index_path.display())
950 })?;
951 Ok(serde_json::from_str(&data).unwrap_or_default())
952 }
953
954 async fn current_index(&self) -> Vec<IndexEntry> {
955 match self.read_index_from_disk().await {
956 Ok(index) => {
957 *self.index.write().await = index.clone();
958 index
959 }
960 Err(_) => self.index.read().await.clone(),
961 }
962 }
963
964 async fn write_index_entries(&self, index: &[IndexEntry]) -> anyhow::Result<()> {
965 let json = serde_json::to_string(index).context("Failed to serialize memory index")?;
966 let tmp = self
967 .index_path
968 .with_extension(format!("json.{}.tmp", uuid::Uuid::new_v4()));
969 tokio::fs::write(&tmp, json.as_bytes())
970 .await
971 .context("Failed to write memory index temp file")?;
972 tokio::fs::rename(&tmp, &self.index_path)
973 .await
974 .context("Failed to rename memory index")?;
975 Ok(())
976 }
977
978 async fn save_index(&self) -> anyhow::Result<()> {
979 let index = self.index.read().await.clone();
980 self.write_index_entries(&index).await
981 }
982
983 async fn save_item(&self, item: &MemoryItem) -> anyhow::Result<()> {
984 let path = self.item_path(&item.id);
985 let json = serde_json::to_string_pretty(item)
986 .with_context(|| format!("Failed to serialize memory item: {}", item.id))?;
987 let tmp = path.with_extension("json.tmp");
988 tokio::fs::write(&tmp, json.as_bytes())
989 .await
990 .with_context(|| format!("Failed to write memory item: {}", item.id))?;
991 tokio::fs::rename(&tmp, &path)
992 .await
993 .with_context(|| format!("Failed to rename memory item: {}", item.id))?;
994 Ok(())
995 }
996
997 async fn load_item_without_access(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
998 let path = self.item_path(id);
999 if !path.exists() {
1000 return Ok(None);
1001 }
1002 let data = tokio::fs::read_to_string(&path).await?;
1003 let item: MemoryItem = serde_json::from_str(&data)?;
1004 Ok(Some(normalize_item_for_store(item)))
1005 }
1006
1007 pub async fn rebuild_index(&self) -> anyhow::Result<usize> {
1009 let _guard = file_memory_index_lock().lock().await;
1010 let mut entries = tokio::fs::read_dir(&self.items_dir).await?;
1011 let mut new_index = Vec::new();
1012 while let Some(entry) = entries.next_entry().await? {
1013 let path = entry.path();
1014 if path.extension().is_some_and(|ext| ext == "json") {
1015 if let Ok(data) = tokio::fs::read_to_string(&path).await {
1016 if let Ok(item) = serde_json::from_str::<MemoryItem>(&data) {
1017 new_index.push(IndexEntry::from(&item));
1018 }
1019 }
1020 }
1021 }
1022 let count = new_index.len();
1023 self.write_index_entries(&new_index).await?;
1024 *self.index.write().await = new_index;
1025 Ok(count)
1026 }
1027}
1028
1029#[async_trait::async_trait]
1030impl MemoryStore for FileMemoryStore {
1031 async fn store(&self, item: MemoryItem) -> anyhow::Result<()> {
1032 self.store_and_return(item).await.map(|_| ())
1033 }
1034
1035 async fn store_and_return(&self, item: MemoryItem) -> anyhow::Result<MemoryItem> {
1036 let _guard = file_memory_index_lock().lock().await;
1037 let mut item = normalize_item_for_store(item);
1038 item.id = Self::safe_id(&item.id);
1039 let mut index = self.read_index_from_disk().await.unwrap_or_default();
1040
1041 let duplicate_id = index
1042 .iter()
1043 .find(|entry| memory_index_entry_is_duplicate(entry, &item))
1044 .map(|entry| entry.id.clone());
1045 if let Some(duplicate_id) = duplicate_id {
1046 if duplicate_id != item.id {
1047 if let Some(existing) = self.load_item_without_access(&duplicate_id).await? {
1048 if memories_are_store_duplicates(&existing, &item) {
1049 let merged = merge_duplicate_memory_item(existing, item.clone());
1050 self.save_item(&merged).await?;
1051 if item.id != merged.id {
1052 let stale_path = self.item_path(&item.id);
1053 if stale_path.exists() {
1054 let _ = tokio::fs::remove_file(stale_path).await;
1055 }
1056 }
1057 index.retain(|entry| entry.id != item.id && entry.id != merged.id);
1058 index.push(IndexEntry::from(&merged));
1059 self.write_index_entries(&index).await?;
1060 *self.index.write().await = index;
1061 return Ok(merged);
1062 }
1063 }
1064 }
1065 }
1066
1067 self.save_item(&item).await?;
1068 let entry = IndexEntry::from(&item);
1069 if let Some(pos) = index.iter().position(|e| e.id == item.id) {
1070 index[pos] = entry;
1071 } else {
1072 index.push(entry);
1073 }
1074 self.write_index_entries(&index).await?;
1075 *self.index.write().await = index;
1076 Ok(item)
1077 }
1078
1079 async fn retrieve(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
1080 let path = self.item_path(id);
1081 if !path.exists() {
1082 return Ok(None);
1083 }
1084 let data = tokio::fs::read_to_string(&path).await?;
1085 let mut item: MemoryItem = serde_json::from_str(&data)?;
1086 item.content_lower = item.content.to_lowercase();
1087 item.record_access();
1088 self.save_item(&item).await?;
1089 Ok(Some(item))
1090 }
1091
1092 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
1093 let query_lower = query.to_lowercase();
1094 let index = self.current_index().await;
1095 let now = Utc::now();
1096 let config = RelevanceConfig::default();
1097 let terms = query_terms(&query_lower);
1098 let mut matches: Vec<(&IndexEntry, f32)> = index
1099 .iter()
1100 .filter_map(|e| {
1101 Some((
1102 e,
1103 index_search_score(e, now, &config, &query_lower, &terms)?,
1104 ))
1105 })
1106 .collect();
1107 matches.sort_by(|a, b| {
1108 b.1.partial_cmp(&a.1)
1109 .unwrap_or(std::cmp::Ordering::Equal)
1110 .then_with(|| b.0.timestamp.cmp(&a.0.timestamp))
1111 });
1112 let ids: Vec<String> = matches
1113 .iter()
1114 .take(limit)
1115 .map(|(e, _)| e.id.clone())
1116 .collect();
1117 let mut items = Vec::with_capacity(ids.len());
1118 for id in ids {
1119 if let Some(item) = self.retrieve(&id).await? {
1120 items.push(item);
1121 }
1122 }
1123 Ok(items)
1124 }
1125
1126 async fn search_by_tags(
1127 &self,
1128 tags: &[String],
1129 limit: usize,
1130 ) -> anyhow::Result<Vec<MemoryItem>> {
1131 let index = self.current_index().await;
1132 let now = Utc::now();
1133 let config = RelevanceConfig::default();
1134 let mut matches: Vec<&IndexEntry> = index
1135 .iter()
1136 .filter(|e| tags.iter().any(|t| e.tags.contains(t)))
1137 .collect();
1138 matches.sort_by(|a, b| {
1139 index_score(a, now, &config)
1140 .partial_cmp(&index_score(b, now, &config))
1141 .unwrap_or(std::cmp::Ordering::Equal)
1142 .reverse()
1143 });
1144 let ids: Vec<String> = matches.iter().take(limit).map(|e| e.id.clone()).collect();
1145 let mut items = Vec::with_capacity(ids.len());
1146 for id in ids {
1147 if let Some(item) = self.retrieve(&id).await? {
1148 items.push(item);
1149 }
1150 }
1151 sort_by_relevance(&mut items);
1152 Ok(items)
1153 }
1154
1155 async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
1156 let index = self.current_index().await;
1157 let mut sorted: Vec<&IndexEntry> = index.iter().collect();
1158 sorted.sort_by_key(|entry| std::cmp::Reverse(entry.timestamp));
1159 let ids: Vec<String> = sorted.iter().take(limit).map(|e| e.id.clone()).collect();
1160 let mut items = Vec::with_capacity(ids.len());
1161 for id in ids {
1162 if let Some(item) = self.retrieve(&id).await? {
1163 items.push(item);
1164 }
1165 }
1166 items.sort_by_key(|item| std::cmp::Reverse(item.timestamp));
1167 Ok(items)
1168 }
1169
1170 async fn get_important(&self, threshold: f32, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
1171 let index = self.current_index().await;
1172 let mut matches: Vec<&IndexEntry> =
1173 index.iter().filter(|e| e.importance >= threshold).collect();
1174 matches.sort_by(|a, b| {
1175 b.importance
1176 .partial_cmp(&a.importance)
1177 .unwrap_or(std::cmp::Ordering::Equal)
1178 });
1179 let ids: Vec<String> = matches.iter().take(limit).map(|e| e.id.clone()).collect();
1180 let mut items = Vec::with_capacity(ids.len());
1181 for id in ids {
1182 if let Some(item) = self.retrieve(&id).await? {
1183 items.push(item);
1184 }
1185 }
1186 items.sort_by(|a, b| {
1187 b.importance
1188 .partial_cmp(&a.importance)
1189 .unwrap_or(std::cmp::Ordering::Equal)
1190 });
1191 Ok(items)
1192 }
1193
1194 async fn delete(&self, id: &str) -> anyhow::Result<()> {
1195 let _guard = file_memory_index_lock().lock().await;
1196 let path = self.item_path(id);
1197 if path.exists() {
1198 tokio::fs::remove_file(&path).await?;
1199 }
1200 let mut index = self.read_index_from_disk().await.unwrap_or_default();
1201 index.retain(|e| e.id != id);
1202 self.write_index_entries(&index).await?;
1203 *self.index.write().await = index;
1204 Ok(())
1205 }
1206
1207 async fn clear(&self) -> anyhow::Result<()> {
1208 let _guard = file_memory_index_lock().lock().await;
1209 let mut entries = tokio::fs::read_dir(&self.items_dir).await?;
1210 while let Some(entry) = entries.next_entry().await? {
1211 let path = entry.path();
1212 if path.extension().is_some_and(|ext| ext == "json") {
1213 let _ = tokio::fs::remove_file(&path).await;
1214 }
1215 }
1216 self.index.write().await.clear();
1217 self.save_index().await
1218 }
1219
1220 async fn count(&self) -> anyhow::Result<usize> {
1221 Ok(self.current_index().await.len())
1222 }
1223
1224 async fn prune(&self, policy: &PrunePolicy) -> anyhow::Result<usize> {
1225 let now = Utc::now();
1226 let cutoff = now - chrono::Duration::days(policy.max_age_days as i64);
1227 let min_importance = policy.min_importance_to_keep;
1228
1229 let mut items = Vec::new();
1232 for entry in self.current_index().await {
1233 if let Some(item) = self.load_item_without_access(&entry.id).await? {
1234 items.push(item);
1235 }
1236 }
1237 let phase1_ids: Vec<String> = items
1238 .iter()
1239 .filter(|item| {
1240 !memory_is_prune_protected(item)
1241 && item.importance < min_importance
1242 && item.timestamp < cutoff
1243 })
1244 .map(|item| item.id.clone())
1245 .collect();
1246 let mut deleted = phase1_ids.len();
1247 for id in &phase1_ids {
1248 self.delete(id).await?;
1249 }
1250
1251 if policy.max_items > 0 {
1255 let config = RelevanceConfig::default();
1256 let phase2_ids: Vec<String> = {
1257 let mut remaining = Vec::new();
1258 for entry in self.current_index().await {
1259 if let Some(item) = self.load_item_without_access(&entry.id).await? {
1260 remaining.push(item);
1261 }
1262 }
1263 if remaining.len() <= policy.max_items {
1264 Vec::new()
1265 } else {
1266 let protected_count = remaining
1267 .iter()
1268 .filter(|item| memory_is_prune_protected(item))
1269 .count();
1270 let unprotected_to_keep = policy.max_items.saturating_sub(protected_count);
1271 let mut unprotected: Vec<MemoryItem> = remaining
1272 .into_iter()
1273 .filter(|item| !memory_is_prune_protected(item))
1274 .collect();
1275 unprotected.sort_by(|a, b| {
1276 b.relevance_score_at(now, &config)
1277 .partial_cmp(&a.relevance_score_at(now, &config))
1278 .unwrap_or(std::cmp::Ordering::Equal)
1279 });
1280 unprotected
1281 .into_iter()
1282 .skip(unprotected_to_keep)
1283 .map(|item| item.id)
1284 .collect()
1285 }
1286 };
1287 deleted += phase2_ids.len();
1288 for id in &phase2_ids {
1289 self.delete(id).await?;
1290 }
1291 }
1292
1293 Ok(deleted)
1294 }
1295}
1296
1297#[cfg(test)]
1302mod tests {
1303 use super::*;
1304
1305 #[test]
1308 fn test_memory_item_creation() {
1309 let item = MemoryItem::new("Test memory")
1310 .with_importance(0.8)
1311 .with_tag("test")
1312 .with_type(MemoryType::Semantic);
1313 assert_eq!(item.content, "Test memory");
1314 assert_eq!(item.importance, 0.8);
1315 assert_eq!(item.tags, vec!["test"]);
1316 assert_eq!(item.memory_type, MemoryType::Semantic);
1317 }
1318
1319 #[test]
1320 fn test_memory_item_importance_clamped() {
1321 assert_eq!(MemoryItem::new("x").with_importance(1.5).importance, 1.0);
1322 assert_eq!(MemoryItem::new("x").with_importance(-0.5).importance, 0.0);
1323 }
1324
1325 #[test]
1326 fn test_memory_item_record_access() {
1327 let mut item = MemoryItem::new("test");
1328 assert_eq!(item.access_count, 0);
1329 item.record_access();
1330 assert_eq!(item.access_count, 1);
1331 assert!(item.last_accessed.is_some());
1332 }
1333
1334 #[test]
1335 fn test_memory_item_merge_duplicate_preserves_canonical_id() {
1336 let existing = MemoryItem::new("Run focused memory store tests after parser changes.")
1337 .with_importance(0.4)
1338 .with_tag("memory")
1339 .with_metadata("source", "workflow");
1340 let existing_id = existing.id.clone();
1341 let incoming =
1342 MemoryItem::new("Run focused memory store regression tests after parser changes.")
1343 .with_importance(0.9)
1344 .with_tag("tests")
1345 .with_metadata("supersedes", "old-memory");
1346
1347 let merged = existing.merge_duplicate(incoming);
1348
1349 assert_eq!(merged.id, existing_id);
1350 assert!(merged.content.contains("regression tests"));
1351 assert_eq!(merged.importance, 0.9);
1352 assert!(merged.tags.contains(&"memory".to_string()));
1353 assert!(merged.tags.contains(&"tests".to_string()));
1354 assert_eq!(
1355 merged.metadata.get("duplicate_count").map(String::as_str),
1356 Some("1")
1357 );
1358 assert_eq!(
1359 merged.metadata.get("supersedes").map(String::as_str),
1360 Some("old-memory")
1361 );
1362 }
1363
1364 #[test]
1365 fn test_memory_item_default_type_is_episodic() {
1366 assert_eq!(MemoryItem::new("test").memory_type, MemoryType::Episodic);
1367 }
1368
1369 #[test]
1370 fn test_memory_item_all_types() {
1371 assert_eq!(
1372 MemoryItem::new("e")
1373 .with_type(MemoryType::Episodic)
1374 .memory_type,
1375 MemoryType::Episodic
1376 );
1377 assert_eq!(
1378 MemoryItem::new("s")
1379 .with_type(MemoryType::Semantic)
1380 .memory_type,
1381 MemoryType::Semantic
1382 );
1383 assert_eq!(
1384 MemoryItem::new("p")
1385 .with_type(MemoryType::Procedural)
1386 .memory_type,
1387 MemoryType::Procedural
1388 );
1389 assert_eq!(
1390 MemoryItem::new("w")
1391 .with_type(MemoryType::Working)
1392 .memory_type,
1393 MemoryType::Working
1394 );
1395 }
1396
1397 #[test]
1400 fn test_relevance_score_uses_config() {
1401 let item = MemoryItem::new("test").with_importance(1.0);
1402 let now = Utc::now();
1403
1404 let config_importance = RelevanceConfig {
1406 decay_days: 30.0,
1407 importance_weight: 0.9,
1408 recency_weight: 0.1,
1409 };
1410 let score = item.relevance_score_at(now, &config_importance);
1411 assert!(score > 0.95, "score was {score}");
1412
1413 let config_fast_decay = RelevanceConfig {
1415 decay_days: 1.0,
1416 importance_weight: 0.7,
1417 recency_weight: 0.3,
1418 };
1419 let score2 = item.relevance_score_at(now, &config_fast_decay);
1420 assert!(score2 > 0.9, "score was {score2}");
1421 }
1422
1423 #[test]
1424 fn test_relevance_score_decays_with_age() {
1425 let mut old_item = MemoryItem::new("old").with_importance(0.5);
1426 old_item.timestamp = Utc::now() - chrono::Duration::days(60);
1427 let config = RelevanceConfig::default(); let score = old_item.relevance_score_at(Utc::now(), &config);
1429 assert!(score < 0.45, "score was {score}");
1432 }
1433
1434 #[test]
1435 fn test_relevance_score_default_uses_default_config() {
1436 let item = MemoryItem::new("test").with_importance(0.9);
1437 let score = item.relevance_score();
1438 assert!(score > 0.6);
1439 }
1440
1441 #[test]
1444 fn test_relevance_config_defaults() {
1445 let c = RelevanceConfig::default();
1446 assert_eq!(c.decay_days, 30.0);
1447 assert_eq!(c.importance_weight, 0.7);
1448 assert_eq!(c.recency_weight, 0.3);
1449 }
1450
1451 #[tokio::test]
1454 async fn test_in_memory_store_retrieve() {
1455 let store = InMemoryStore::new();
1456 let item = MemoryItem::new("hello").with_tag("test");
1457 store.store(item.clone()).await.unwrap();
1458 let r = store.retrieve(&item.id).await.unwrap();
1459 assert!(r.is_some());
1460 assert_eq!(r.unwrap().content, "hello");
1461 let r = store.retrieve(&item.id).await.unwrap().unwrap();
1462 assert_eq!(r.access_count, 2);
1463 }
1464
1465 #[tokio::test]
1466 async fn test_in_memory_store_retrieve_nonexistent() {
1467 let store = InMemoryStore::new();
1468 assert!(store.retrieve("nope").await.unwrap().is_none());
1469 }
1470
1471 #[tokio::test]
1472 async fn test_in_memory_store_upsert() {
1473 let store = InMemoryStore::new();
1474 let mut item = MemoryItem::new("original");
1475 let id = item.id.clone();
1476 store.store(item.clone()).await.unwrap();
1477 item.content = "updated".to_string();
1478 item.content_lower = "updated".to_string();
1479 store.store(item).await.unwrap();
1480 assert_eq!(store.count().await.unwrap(), 1);
1481 assert_eq!(
1482 store.retrieve(&id).await.unwrap().unwrap().content,
1483 "updated"
1484 );
1485 }
1486
1487 #[tokio::test]
1488 async fn test_in_memory_store_search_and_tags() {
1489 let store = InMemoryStore::new();
1490 store
1491 .store(MemoryItem::new("create file").with_tag("file"))
1492 .await
1493 .unwrap();
1494 store
1495 .store(MemoryItem::new("delete file").with_tag("file"))
1496 .await
1497 .unwrap();
1498 store
1499 .store(MemoryItem::new("create dir").with_tag("dir"))
1500 .await
1501 .unwrap();
1502 assert_eq!(store.search("create", 10).await.unwrap().len(), 2);
1503 assert_eq!(
1504 store
1505 .search_by_tags(&["file".to_string()], 10)
1506 .await
1507 .unwrap()
1508 .len(),
1509 2
1510 );
1511 }
1512
1513 #[tokio::test]
1514 async fn test_in_memory_store_search_relevance_order() {
1515 let store = InMemoryStore::new();
1516 store
1517 .store(MemoryItem::new("rust tip").with_importance(0.3))
1518 .await
1519 .unwrap();
1520 store
1521 .store(MemoryItem::new("rust trick").with_importance(0.9))
1522 .await
1523 .unwrap();
1524 let results = store.search("rust", 10).await.unwrap();
1525 assert_eq!(results.len(), 2);
1526 assert!(results[0].importance >= results[1].importance);
1527 }
1528
1529 #[tokio::test]
1530 async fn test_in_memory_store_delete_and_clear() {
1531 let store = InMemoryStore::new();
1532 let item = MemoryItem::new("to delete");
1533 let id = item.id.clone();
1534 store.store(item).await.unwrap();
1535 store.delete(&id).await.unwrap();
1536 assert_eq!(store.count().await.unwrap(), 0);
1537
1538 for i in 0..3 {
1539 store
1540 .store(MemoryItem::new(format!("item {i}")))
1541 .await
1542 .unwrap();
1543 }
1544 store.clear().await.unwrap();
1545 assert_eq!(store.count().await.unwrap(), 0);
1546 }
1547
1548 #[tokio::test]
1549 async fn test_in_memory_store_get_recent() {
1550 let store = InMemoryStore::new();
1551 for i in 0..5 {
1552 let mut item = MemoryItem::new(format!("item {i}"));
1553 item.timestamp = Utc::now() + chrono::Duration::seconds(i as i64);
1554 store.store(item).await.unwrap();
1555 }
1556 let recent = store.get_recent(3).await.unwrap();
1557 assert_eq!(recent.len(), 3);
1558 assert!(recent[0].timestamp >= recent[1].timestamp);
1559 }
1560
1561 #[tokio::test]
1562 async fn test_in_memory_store_get_important() {
1563 let store = InMemoryStore::new();
1564 store
1565 .store(MemoryItem::new("low").with_importance(0.2))
1566 .await
1567 .unwrap();
1568 store
1569 .store(MemoryItem::new("high").with_importance(0.9))
1570 .await
1571 .unwrap();
1572 store
1573 .store(MemoryItem::new("medium").with_importance(0.5))
1574 .await
1575 .unwrap();
1576 let results = store.get_important(0.7, 10).await.unwrap();
1577 assert_eq!(results.len(), 1);
1578 assert_eq!(results[0].content, "high");
1579 }
1580
1581 #[test]
1582 fn test_in_memory_store_default() {
1583 let _store: InMemoryStore = InMemoryStore::default();
1584 }
1585
1586 #[test]
1589 fn test_prune_policy_defaults() {
1590 let p = PrunePolicy::default();
1591 assert_eq!(p.max_age_days, 90);
1592 assert_eq!(p.min_importance_to_keep, 0.5);
1593 assert_eq!(p.max_items, 0);
1594 }
1595
1596 #[tokio::test]
1597 async fn test_prune_removes_old_low_importance() {
1598 let store = InMemoryStore::new();
1599 let mut old_item = MemoryItem::new("stale memory").with_importance(0.2);
1600 old_item.timestamp = Utc::now() - chrono::Duration::days(100);
1601 store.store(old_item).await.unwrap();
1602
1603 let policy = PrunePolicy {
1604 max_age_days: 90,
1605 min_importance_to_keep: 0.5,
1606 max_items: 0,
1607 };
1608 let deleted = store.prune(&policy).await.unwrap();
1609 assert_eq!(deleted, 1);
1610 assert_eq!(store.count().await.unwrap(), 0);
1611 }
1612
1613 #[tokio::test]
1614 async fn test_prune_keeps_high_importance() {
1615 let store = InMemoryStore::new();
1616 let mut old_item = MemoryItem::new("important memory").with_importance(0.9);
1617 old_item.timestamp = Utc::now() - chrono::Duration::days(100);
1618 store.store(old_item).await.unwrap();
1619
1620 let policy = PrunePolicy {
1621 max_age_days: 90,
1622 min_importance_to_keep: 0.5,
1623 max_items: 0,
1624 };
1625 let deleted = store.prune(&policy).await.unwrap();
1626 assert_eq!(deleted, 0);
1627 assert_eq!(store.count().await.unwrap(), 1);
1628 }
1629
1630 #[tokio::test]
1631 async fn test_prune_max_items() {
1632 let store = InMemoryStore::new();
1633 for i in 0..10 {
1634 store
1635 .store(MemoryItem::new(format!("item {i}")).with_importance(i as f32 * 0.1))
1636 .await
1637 .unwrap();
1638 }
1639 let policy = PrunePolicy {
1640 max_age_days: 9999,
1641 min_importance_to_keep: 0.0,
1642 max_items: 5,
1643 };
1644 let deleted = store.prune(&policy).await.unwrap();
1645 assert_eq!(deleted, 5);
1646 assert_eq!(store.count().await.unwrap(), 5);
1647 }
1648
1649 #[tokio::test]
1650 async fn test_prune_keeps_recent_low_importance() {
1651 let store = InMemoryStore::new();
1653 store
1654 .store(MemoryItem::new("fresh").with_importance(0.1))
1655 .await
1656 .unwrap();
1657
1658 let policy = PrunePolicy {
1659 max_age_days: 90,
1660 min_importance_to_keep: 0.5,
1661 max_items: 0,
1662 };
1663 let deleted = store.prune(&policy).await.unwrap();
1664 assert_eq!(deleted, 0);
1665 assert_eq!(store.count().await.unwrap(), 1);
1666 }
1667}
1668
1669#[cfg(test)]
1670mod file_memory_store_tests {
1671 use super::*;
1672 use tempfile::TempDir;
1673
1674 async fn setup() -> (TempDir, FileMemoryStore) {
1675 let dir = TempDir::new().unwrap();
1676 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1677 (dir, store)
1678 }
1679
1680 #[tokio::test]
1681 async fn test_store_and_retrieve() {
1682 let (_dir, store) = setup().await;
1683 let item = MemoryItem::new("hello world");
1684 let id = item.id.clone();
1685 store.store(item).await.unwrap();
1686 let r = store.retrieve(&id).await.unwrap().unwrap();
1687 assert_eq!(r.content, "hello world");
1688 }
1689
1690 #[tokio::test]
1691 async fn test_retrieve_nonexistent() {
1692 let (_dir, store) = setup().await;
1693 assert!(store.retrieve("nonexistent").await.unwrap().is_none());
1694 }
1695
1696 #[tokio::test]
1697 async fn test_search_by_content() {
1698 let (_dir, store) = setup().await;
1699 store
1700 .store(MemoryItem::new("rust programming"))
1701 .await
1702 .unwrap();
1703 store
1704 .store(MemoryItem::new("python scripting"))
1705 .await
1706 .unwrap();
1707 store
1708 .store(MemoryItem::new("rust async patterns"))
1709 .await
1710 .unwrap();
1711 let results = store.search("rust", 10).await.unwrap();
1712 assert_eq!(results.len(), 2);
1713 }
1714
1715 #[tokio::test]
1716 async fn test_search_matches_non_contiguous_terms() {
1717 let (_dir, store) = setup().await;
1718 store
1719 .store(MemoryItem::new(
1720 "Success: release preflight\nTools: bash\nResult: provider verification passed",
1721 ))
1722 .await
1723 .unwrap();
1724 let results = store.search("release provider check", 10).await.unwrap();
1725 assert_eq!(results.len(), 1);
1726 assert!(results[0].content.contains("release preflight"));
1727 }
1728
1729 #[tokio::test]
1730 async fn test_retrieve_records_access_on_disk() {
1731 let (dir, store) = setup().await;
1732 let item = MemoryItem::new("access me");
1733 let id = item.id.clone();
1734 store.store(item).await.unwrap();
1735
1736 let item = store.retrieve(&id).await.unwrap().unwrap();
1737 assert_eq!(item.access_count, 1);
1738
1739 let reopened = FileMemoryStore::new(dir.path()).await.unwrap();
1740 let item = reopened.retrieve(&id).await.unwrap().unwrap();
1741 assert_eq!(item.access_count, 2);
1742 assert!(item.last_accessed.is_some());
1743 }
1744
1745 #[tokio::test]
1746 async fn test_search_limit() {
1747 let (_dir, store) = setup().await;
1748 for i in 0..10 {
1749 store
1750 .store(MemoryItem::new(format!("item {i}")))
1751 .await
1752 .unwrap();
1753 }
1754 assert_eq!(store.search("item", 3).await.unwrap().len(), 3);
1755 }
1756
1757 #[tokio::test]
1758 async fn test_search_by_tags() {
1759 let (_dir, store) = setup().await;
1760 store
1761 .store(MemoryItem::new("one").with_tags(vec!["rust".into(), "async".into()]))
1762 .await
1763 .unwrap();
1764 store
1765 .store(MemoryItem::new("two").with_tags(vec!["python".into()]))
1766 .await
1767 .unwrap();
1768 store
1769 .store(MemoryItem::new("three").with_tags(vec!["rust".into()]))
1770 .await
1771 .unwrap();
1772 assert_eq!(
1773 store
1774 .search_by_tags(&["rust".to_string()], 10)
1775 .await
1776 .unwrap()
1777 .len(),
1778 2
1779 );
1780 }
1781
1782 #[tokio::test]
1783 async fn test_get_recent_ordered() {
1784 let (_dir, store) = setup().await;
1785 for i in 0..5 {
1786 let mut item = MemoryItem::new(format!("item {i}"));
1787 item.timestamp = Utc::now() + chrono::Duration::seconds(i as i64);
1788 store.store(item).await.unwrap();
1789 }
1790 let results = store.get_recent(3).await.unwrap();
1791 assert_eq!(results.len(), 3);
1792 assert!(results[0].timestamp >= results[1].timestamp);
1793 }
1794
1795 #[tokio::test]
1796 async fn test_get_important() {
1797 let (_dir, store) = setup().await;
1798 store
1799 .store(MemoryItem::new("low").with_importance(0.1))
1800 .await
1801 .unwrap();
1802 store
1803 .store(MemoryItem::new("high").with_importance(0.9))
1804 .await
1805 .unwrap();
1806 store
1807 .store(MemoryItem::new("medium").with_importance(0.5))
1808 .await
1809 .unwrap();
1810 let results = store.get_important(0.0, 2).await.unwrap();
1811 assert_eq!(results.len(), 2);
1812 assert!(results[0].importance >= results[1].importance);
1813 }
1814
1815 #[tokio::test]
1816 async fn test_delete() {
1817 let (_dir, store) = setup().await;
1818 let item = MemoryItem::new("to delete");
1819 let id = item.id.clone();
1820 store.store(item).await.unwrap();
1821 store.delete(&id).await.unwrap();
1822 assert_eq!(store.count().await.unwrap(), 0);
1823 assert!(store.retrieve(&id).await.unwrap().is_none());
1824 }
1825
1826 #[tokio::test]
1827 async fn test_delete_nonexistent() {
1828 let (_dir, store) = setup().await;
1829 store.delete("nonexistent").await.unwrap();
1830 }
1831
1832 #[tokio::test]
1833 async fn test_clear() {
1834 let (_dir, store) = setup().await;
1835 for i in 0..5 {
1836 store
1837 .store(MemoryItem::new(format!("item {i}")))
1838 .await
1839 .unwrap();
1840 }
1841 store.clear().await.unwrap();
1842 assert_eq!(store.count().await.unwrap(), 0);
1843 }
1844
1845 #[tokio::test]
1846 async fn test_persistence_across_instances() {
1847 let dir = TempDir::new().unwrap();
1848 {
1849 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1850 store
1851 .store(MemoryItem::new("persistent data").with_tags(vec!["test".into()]))
1852 .await
1853 .unwrap();
1854 }
1855 {
1856 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1857 assert_eq!(store.count().await.unwrap(), 1);
1858 assert_eq!(store.search("persistent", 10).await.unwrap().len(), 1);
1859 }
1860 }
1861
1862 #[tokio::test]
1863 async fn test_stale_instances_merge_index_on_store() {
1864 let dir = TempDir::new().unwrap();
1865 let store_a = FileMemoryStore::new(dir.path()).await.unwrap();
1866 let store_b = FileMemoryStore::new(dir.path()).await.unwrap();
1867
1868 store_a
1869 .store(MemoryItem::new("alpha stale merge"))
1870 .await
1871 .unwrap();
1872 store_b
1873 .store(MemoryItem::new("beta stale merge"))
1874 .await
1875 .unwrap();
1876
1877 let reopened = FileMemoryStore::new(dir.path()).await.unwrap();
1878 assert_eq!(reopened.count().await.unwrap(), 2);
1879 assert_eq!(reopened.search("stale merge", 10).await.unwrap().len(), 2);
1880 }
1881
1882 #[tokio::test]
1883 async fn test_rebuild_index() {
1884 let dir = TempDir::new().unwrap();
1885 {
1886 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1887 store.store(MemoryItem::new("alpha")).await.unwrap();
1888 store.store(MemoryItem::new("beta")).await.unwrap();
1889 }
1890 tokio::fs::remove_file(dir.path().join("index.json"))
1891 .await
1892 .unwrap();
1893 {
1894 let store = FileMemoryStore::new(dir.path()).await.unwrap();
1895 assert_eq!(store.count().await.unwrap(), 0);
1896 store.rebuild_index().await.unwrap();
1897 assert_eq!(store.count().await.unwrap(), 2);
1898 }
1899 }
1900
1901 #[tokio::test]
1902 async fn test_path_traversal_prevention() {
1903 let (_dir, store) = setup().await;
1904 let mut item = MemoryItem::new("sneaky");
1905 item.id = "../../../etc/passwd".to_string();
1906 store.store(item).await.unwrap();
1907 let results = store.search("sneaky", 10).await.unwrap();
1908 assert_eq!(results.len(), 1);
1909 assert!(!results[0].id.contains('/'));
1910 assert!(!results[0].id.contains(".."));
1911 }
1912
1913 #[tokio::test]
1914 async fn test_importance_threshold() {
1915 let (_dir, store) = setup().await;
1916 store
1917 .store(MemoryItem::new("low").with_importance(0.2))
1918 .await
1919 .unwrap();
1920 store
1921 .store(MemoryItem::new("high").with_importance(0.8))
1922 .await
1923 .unwrap();
1924 let results = store.get_important(0.5, 10).await.unwrap();
1925 assert_eq!(results.len(), 1);
1926 assert_eq!(results[0].content, "high");
1927 }
1928
1929 #[tokio::test]
1930 async fn test_file_prune_removes_old_low_importance() {
1931 let (_dir, store) = setup().await;
1932 let mut old_item = MemoryItem::new("stale").with_importance(0.2);
1933 old_item.timestamp = Utc::now() - chrono::Duration::days(100);
1934 store.store(old_item).await.unwrap();
1935
1936 let policy = PrunePolicy {
1937 max_age_days: 90,
1938 min_importance_to_keep: 0.5,
1939 max_items: 0,
1940 };
1941 let deleted = store.prune(&policy).await.unwrap();
1942 assert_eq!(deleted, 1);
1943 assert_eq!(store.count().await.unwrap(), 0);
1944 }
1945
1946 #[tokio::test]
1947 async fn test_file_prune_keeps_high_importance() {
1948 let (_dir, store) = setup().await;
1949 let mut old_item = MemoryItem::new("important").with_importance(0.9);
1950 old_item.timestamp = Utc::now() - chrono::Duration::days(100);
1951 store.store(old_item).await.unwrap();
1952
1953 let policy = PrunePolicy {
1954 max_age_days: 90,
1955 min_importance_to_keep: 0.5,
1956 max_items: 0,
1957 };
1958 let deleted = store.prune(&policy).await.unwrap();
1959 assert_eq!(deleted, 0);
1960 assert_eq!(store.count().await.unwrap(), 1);
1961 }
1962
1963 #[tokio::test]
1964 async fn test_file_prune_max_items() {
1965 let (_dir, store) = setup().await;
1966 for i in 0..10 {
1967 store
1968 .store(MemoryItem::new(format!("item {i}")).with_importance(i as f32 * 0.1))
1969 .await
1970 .unwrap();
1971 }
1972 let policy = PrunePolicy {
1973 max_age_days: 9999,
1974 min_importance_to_keep: 0.0,
1975 max_items: 5,
1976 };
1977 let deleted = store.prune(&policy).await.unwrap();
1978 assert_eq!(deleted, 5);
1979 assert_eq!(store.count().await.unwrap(), 5);
1980 }
1981}