1use mentedb_core::MemoryNode;
2use mentedb_core::edge::EdgeType;
3use mentedb_core::memory::MemoryType;
4use mentedb_core::types::MemoryId;
5
6use crate::llm::{
7 CognitiveLlmService, ContradictionVerdict, InvalidationVerdict, LlmJudge, MemorySummary,
8};
9
10#[derive(Debug, Clone)]
11pub enum InferredAction {
12 FlagContradiction {
13 existing: MemoryId,
14 new: MemoryId,
15 reason: String,
16 },
17 MarkObsolete {
18 memory: MemoryId,
19 superseded_by: MemoryId,
20 },
21 InvalidateMemory {
23 memory: MemoryId,
24 superseded_by: MemoryId,
25 valid_until: u64,
26 },
27 DeduplicateExact {
32 duplicate: MemoryId,
33 keeper: MemoryId,
34 },
35 UpdateContent {
37 memory: MemoryId,
38 new_content: String,
39 reason: String,
40 },
41 CreateEdge {
42 source: MemoryId,
43 target: MemoryId,
44 edge_type: EdgeType,
45 weight: f32,
46 },
47 UpdateConfidence {
48 memory: MemoryId,
49 new_confidence: f32,
50 },
51 PropagateBeliefChange {
52 root: MemoryId,
53 delta: f32,
54 },
55}
56
57fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
58 if a.len() != b.len() || a.is_empty() {
59 return 0.0;
60 }
61 let mut dot = 0.0f32;
62 let mut norm_a = 0.0f32;
63 let mut norm_b = 0.0f32;
64 for i in 0..a.len() {
65 dot += a[i] * b[i];
66 norm_a += a[i] * a[i];
67 norm_b += b[i] * b[i];
68 }
69 let denom = norm_a.sqrt() * norm_b.sqrt();
70 if denom == 0.0 { 0.0 } else { dot / denom }
71}
72
73#[derive(Debug, Clone)]
75pub struct WriteInferenceConfig {
76 pub contradiction_threshold: f32,
78 pub obsolete_threshold: f32,
80 pub related_min: f32,
82 pub related_max: f32,
84 pub correction_threshold: f32,
86 pub confidence_decay_factor: f32,
88 pub confidence_floor: f32,
90 pub value_update_enabled: bool,
98 pub value_update_min_similarity: f32,
111 pub value_update_prefix_share: f32,
115 pub value_update_max_tail: usize,
118}
119
120impl Default for WriteInferenceConfig {
121 fn default() -> Self {
122 Self {
123 contradiction_threshold: 0.95,
124 obsolete_threshold: 0.85,
125 related_min: 0.6,
126 related_max: 0.85,
127 correction_threshold: 0.5,
128 confidence_decay_factor: 0.5,
129 confidence_floor: 0.1,
130 value_update_enabled: true,
131 value_update_min_similarity: 0.5,
132 value_update_prefix_share: 0.6,
133 value_update_max_tail: 4,
134 }
135 }
136}
137
138fn frame_tokens(s: &str) -> Vec<String> {
141 s.split(|c: char| !c.is_alphanumeric())
142 .filter(|t| !t.is_empty())
143 .map(|t| t.to_lowercase())
144 .collect()
145}
146
147fn is_value_update(new: &str, old: &str, frame_share: f32, max_value: usize) -> bool {
169 let nt = frame_tokens(new);
170 let ot = frame_tokens(old);
171 if nt.is_empty() || ot.is_empty() || nt == ot {
172 return false;
173 }
174 if nt.len().min(ot.len()) < 4 {
179 return false;
180 }
181 let prefix = nt.iter().zip(ot.iter()).take_while(|(a, b)| a == b).count();
182 if prefix < 2 {
183 return false;
184 }
185 let suffix = nt
186 .iter()
187 .rev()
188 .zip(ot.iter().rev())
189 .take_while(|(a, b)| a == b)
190 .count()
191 .min(nt.len().min(ot.len()) - prefix);
193 let longest = nt.len().max(ot.len());
194 let value_new = nt.len() - prefix - suffix;
195 let value_old = ot.len() - prefix - suffix;
196 if suffix > 0 {
197 let all_numeric =
198 |tokens: &[String]| tokens.iter().all(|t| t.chars().all(|c| c.is_ascii_digit()));
199 if all_numeric(&nt[prefix..nt.len() - suffix])
200 && all_numeric(&ot[prefix..ot.len() - suffix])
201 {
202 return false;
203 }
204 }
205 (prefix + suffix) as f32 / longest as f32 >= frame_share
206 && value_new.max(value_old) > 0
207 && value_new <= max_value
208 && value_old <= max_value
209}
210
211fn value_update_resolution(
219 new: &MemoryNode,
220 existing: &MemoryNode,
221) -> Option<(MemoryId, MemoryId, f32)> {
222 let new_correction = new.memory_type == MemoryType::Correction;
223 let old_correction = existing.memory_type == MemoryType::Correction;
224 if new_correction && !old_correction {
225 return Some((new.id, existing.id, existing.confidence));
226 }
227 if old_correction && !new_correction {
228 return Some((existing.id, new.id, new.confidence));
229 }
230 if new.created_at > existing.created_at {
231 return Some((new.id, existing.id, existing.confidence));
232 }
233 if existing.created_at > new.created_at {
234 return Some((existing.id, new.id, new.confidence));
235 }
236 None
237}
238
239pub struct WriteInferenceEngine {
240 config: WriteInferenceConfig,
241}
242
243impl WriteInferenceEngine {
244 pub fn new() -> Self {
245 Self {
246 config: WriteInferenceConfig::default(),
247 }
248 }
249
250 pub fn with_config(config: WriteInferenceConfig) -> Self {
251 Self { config }
252 }
253
254 pub fn looks_like_value_update(&self, new: &str, old: &str) -> bool {
261 is_value_update(
262 new,
263 old,
264 self.config.value_update_prefix_share,
265 self.config.value_update_max_tail,
266 )
267 }
268
269 pub fn infer_on_write(
270 &self,
271 new_memory: &MemoryNode,
272 existing_memories: &[MemoryNode],
273 existing_edges: &[(MemoryId, MemoryId, EdgeType)],
274 ) -> Vec<InferredAction> {
275 let _ = existing_edges; let mut actions = Vec::new();
277 let mut value_update_handled: Vec<MemoryId> = Vec::new();
280
281 for existing in existing_memories {
282 if existing.id == new_memory.id {
283 continue;
284 }
285
286 let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
287
288 if existing.content == new_memory.content
302 && sim > self.config.obsolete_threshold
303 && new_memory.created_at > existing.created_at
304 {
305 actions.push(InferredAction::DeduplicateExact {
306 duplicate: existing.id,
307 keeper: new_memory.id,
308 });
309 } else if self.config.value_update_enabled
310 && sim >= self.config.value_update_min_similarity
311 && existing.agent_id == new_memory.agent_id
312 && existing.user_id == new_memory.user_id
313 && is_value_update(
314 &new_memory.content,
315 &existing.content,
316 self.config.value_update_prefix_share,
317 self.config.value_update_max_tail,
318 )
319 {
320 value_update_handled.push(existing.id);
329 if let Some((winner, loser, loser_confidence)) =
330 value_update_resolution(new_memory, existing)
331 {
332 let now = std::time::SystemTime::now()
338 .duration_since(std::time::UNIX_EPOCH)
339 .unwrap_or_default()
340 .as_micros() as u64;
341 actions.push(InferredAction::InvalidateMemory {
342 memory: loser,
343 superseded_by: winner,
344 valid_until: now,
345 });
346 actions.push(InferredAction::UpdateConfidence {
347 memory: loser,
348 new_confidence: (loser_confidence * self.config.confidence_decay_factor)
349 .max(self.config.confidence_floor),
350 });
351 }
352 } else if sim > self.config.related_min && sim <= self.config.related_max {
356 actions.push(InferredAction::CreateEdge {
357 source: new_memory.id,
358 target: existing.id,
359 edge_type: EdgeType::Related,
360 weight: sim,
361 });
362 }
363 }
364
365 if new_memory.memory_type == MemoryType::Correction
368 && let Some(original) = existing_memories
369 .iter()
370 .filter(|m| m.id != new_memory.id && !value_update_handled.contains(&m.id))
371 .max_by(|a, b| {
372 cosine_similarity(&new_memory.embedding, &a.embedding)
373 .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
374 .unwrap_or(std::cmp::Ordering::Equal)
375 })
376 {
377 let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
378 if sim > self.config.correction_threshold {
379 actions.push(InferredAction::CreateEdge {
380 source: new_memory.id,
381 target: original.id,
382 edge_type: EdgeType::Supersedes,
383 weight: 1.0,
384 });
385 actions.push(InferredAction::UpdateConfidence {
386 memory: original.id,
387 new_confidence: (original.confidence * self.config.confidence_decay_factor)
388 .max(self.config.confidence_floor),
389 });
390 }
391 }
392
393 actions
394 }
395
396 pub async fn infer_on_write_with_llm<J: LlmJudge>(
401 &self,
402 new_memory: &MemoryNode,
403 existing_memories: &[MemoryNode],
404 existing_edges: &[(MemoryId, MemoryId, EdgeType)],
405 llm: &CognitiveLlmService<J>,
406 ) -> Vec<InferredAction> {
407 let _ = existing_edges;
408 let mut actions = Vec::new();
409
410 let new_summary = memory_to_summary(new_memory);
411
412 for existing in existing_memories {
413 if existing.id == new_memory.id {
414 continue;
415 }
416
417 let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
418
419 if sim > 0.5 && existing.agent_id == new_memory.agent_id {
422 let old_summary = memory_to_summary(existing);
423
424 if let Ok(verdict) = llm.judge_invalidation(&old_summary, &new_summary).await {
426 match verdict {
427 InvalidationVerdict::Invalidate { reason: _ } => {
428 actions.push(InferredAction::InvalidateMemory {
431 memory: existing.id,
432 superseded_by: new_memory.id,
433 valid_until: new_memory.created_at,
434 });
435 actions.push(InferredAction::UpdateConfidence {
436 memory: existing.id,
437 new_confidence: (existing.confidence
438 * self.config.confidence_decay_factor)
439 .max(self.config.confidence_floor),
440 });
441 continue;
443 }
444 InvalidationVerdict::Update {
445 merged_content,
446 reason,
447 } => {
448 actions.push(InferredAction::UpdateContent {
449 memory: existing.id,
450 new_content: merged_content,
451 reason,
452 });
453 continue;
454 }
455 InvalidationVerdict::Keep { .. } => {
456 }
458 }
459 }
460
461 if sim > 0.7
463 && existing.content != new_memory.content
464 && let Ok(verdict) = llm.detect_contradiction(&old_summary, &new_summary).await
465 {
466 match verdict {
467 ContradictionVerdict::Contradicts { reason } => {
468 actions.push(InferredAction::FlagContradiction {
469 existing: existing.id,
470 new: new_memory.id,
471 reason,
472 });
473 }
474 ContradictionVerdict::Supersedes { winner, reason: _ } => {
475 let winner_is_new = winner == new_memory.id.to_string();
476 let (obsolete, superseder) = if winner_is_new {
477 (existing.id, new_memory.id)
478 } else {
479 (new_memory.id, existing.id)
480 };
481 actions.push(InferredAction::InvalidateMemory {
482 memory: obsolete,
483 superseded_by: superseder,
484 valid_until: new_memory.created_at,
485 });
486 }
487 ContradictionVerdict::Compatible { .. } => {}
488 }
489 }
490 }
491
492 if sim > self.config.related_min && sim <= self.config.related_max {
494 actions.push(InferredAction::CreateEdge {
495 source: new_memory.id,
496 target: existing.id,
497 edge_type: EdgeType::Related,
498 weight: sim,
499 });
500 }
501 }
502
503 if new_memory.memory_type == MemoryType::Correction
505 && let Some(original) = existing_memories
506 .iter()
507 .filter(|m| m.id != new_memory.id)
508 .max_by(|a, b| {
509 cosine_similarity(&new_memory.embedding, &a.embedding)
510 .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
511 .unwrap_or(std::cmp::Ordering::Equal)
512 })
513 {
514 let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
515 if sim > self.config.correction_threshold {
516 actions.push(InferredAction::InvalidateMemory {
517 memory: original.id,
518 superseded_by: new_memory.id,
519 valid_until: new_memory.created_at,
520 });
521 actions.push(InferredAction::UpdateConfidence {
522 memory: original.id,
523 new_confidence: (original.confidence * self.config.confidence_decay_factor)
524 .max(self.config.confidence_floor),
525 });
526 }
527 }
528
529 actions
530 }
531}
532
533impl Default for WriteInferenceEngine {
534 fn default() -> Self {
535 Self::new()
536 }
537}
538
539fn memory_to_summary(m: &MemoryNode) -> MemorySummary {
540 MemorySummary {
541 id: m.id,
542 content: m.content.clone(),
543 memory_type: m.memory_type,
544 confidence: m.confidence,
545 created_at: m.created_at,
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use mentedb_core::memory::MemoryType;
553 use mentedb_core::types::AgentId;
554
555 use crate::llm::MockLlmJudge;
556
557 fn make_memory(content: &str, embedding: Vec<f32>, mem_type: MemoryType) -> MemoryNode {
558 let mut m = MemoryNode::new(AgentId::new(), mem_type, content.to_string(), embedding);
559 m.created_at = 1000;
560 m
561 }
562
563 #[test]
564 fn test_heuristic_does_not_flag_contradiction_from_similarity() {
565 let agent = AgentId::new();
572 let mut existing =
573 make_memory("uses PostgreSQL", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
574 existing.agent_id = agent;
575
576 let mut new_mem = make_memory("uses MySQL", vec![0.99, 0.01, 0.0], MemoryType::Semantic);
577 new_mem.agent_id = agent;
578 new_mem.created_at = 2000;
579
580 let engine = WriteInferenceEngine::new();
581 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
582 assert!(
583 !actions
584 .iter()
585 .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
586 "Heuristic must not flag contradictions from bare similarity, got: {:?}",
587 actions
588 );
589 }
590
591 #[test]
592 fn value_update_supersedes_the_old_fact() {
593 let agent = AgentId::new();
597 let mut existing = make_memory(
598 "The user's favorite coffee order is a cortado",
599 vec![1.0, 0.0, 0.0],
600 MemoryType::Semantic,
601 );
602 existing.agent_id = agent;
603 let mut new_mem = make_memory(
604 "The user's favorite coffee order is a flat white",
605 vec![0.98, 0.02, 0.0],
606 MemoryType::Semantic,
607 );
608 new_mem.agent_id = agent;
609 new_mem.created_at = 2000;
610
611 let engine = WriteInferenceEngine::new();
612 let actions = engine.infer_on_write(&new_mem, &[existing.clone()], &[]);
613 assert!(
614 actions.iter().any(|a| matches!(
615 a,
616 InferredAction::InvalidateMemory { memory, superseded_by, .. }
617 if *superseded_by == new_mem.id && *memory == existing.id
618 )),
619 "value update must invalidate the stale fact, got: {actions:?}"
620 );
621 assert!(
622 actions
623 .iter()
624 .any(|a| matches!(a, InferredAction::UpdateConfidence { memory, .. } if *memory == existing.id)),
625 "superseded fact's confidence must decay"
626 );
627 }
628
629 #[test]
630 fn value_update_fires_at_realistic_embedder_cosine() {
631 let agent = AgentId::new();
639 let mut existing = make_memory(
640 "The user's favorite coffee order is a cortado",
641 vec![1.0, 0.0, 0.0],
642 MemoryType::Semantic,
643 );
644 existing.agent_id = agent;
645 let mut new_mem = make_memory(
647 "The user's favorite coffee order is a flat white",
648 vec![0.6, 0.8, 0.0],
649 MemoryType::Semantic,
650 );
651 new_mem.agent_id = agent;
652 new_mem.created_at = 2000;
653
654 let actions =
655 WriteInferenceEngine::new().infer_on_write(&new_mem, &[existing.clone()], &[]);
656 assert!(
657 actions.iter().any(|a| matches!(
658 a,
659 InferredAction::InvalidateMemory { memory, superseded_by, .. }
660 if *superseded_by == new_mem.id && *memory == existing.id
661 )),
662 "a frame-matched correction at realistic cosine 0.6 must supersede, got: {actions:?}"
663 );
664 }
665
666 #[test]
667 fn value_update_below_cosine_floor_does_not_supersede() {
668 let agent = AgentId::new();
673 let mut existing = make_memory(
674 "The user's favorite coffee order is a cortado",
675 vec![1.0, 0.0, 0.0],
676 MemoryType::Semantic,
677 );
678 existing.agent_id = agent;
679 let mut new_mem = make_memory(
681 "The user's favorite coffee order is a flat white",
682 vec![0.4, 0.917, 0.0],
683 MemoryType::Semantic,
684 );
685 new_mem.agent_id = agent;
686 new_mem.created_at = 2000;
687
688 let actions = WriteInferenceEngine::new().infer_on_write(&new_mem, &[existing], &[]);
689 assert!(
690 !actions.iter().any(|a| matches!(
691 a,
692 InferredAction::CreateEdge {
693 edge_type: EdgeType::Supersedes,
694 ..
695 }
696 )),
697 "below the cosine floor a frame match must not supersede, got: {actions:?}"
698 );
699 }
700
701 #[test]
702 fn correction_supersedes_regardless_of_store_order() {
703 let agent = AgentId::new();
708 let mut correction = make_memory(
709 "The user's favorite coffee is a flat white",
710 vec![1.0, 0.0, 0.0],
711 MemoryType::Correction,
712 );
713 correction.agent_id = agent;
714 correction.created_at = 5000;
715 let mut original = make_memory(
716 "The user's favorite coffee is a cortado",
717 vec![0.98, 0.02, 0.0],
718 MemoryType::Semantic,
719 );
720 original.agent_id = agent;
721 original.created_at = 5000; let actions =
725 WriteInferenceEngine::new().infer_on_write(&original, &[correction.clone()], &[]);
726 assert!(
727 actions.iter().any(|a| matches!(
728 a,
729 InferredAction::InvalidateMemory { memory, superseded_by, .. }
730 if *superseded_by == correction.id && *memory == original.id
731 )),
732 "the correction must invalidate the later-arriving original, got: {actions:?}"
733 );
734 }
735
736 #[test]
737 fn correction_supersedes_original_at_equal_timestamps() {
738 let agent = AgentId::new();
741 let mut original = make_memory(
742 "The user's favorite coffee is a cortado",
743 vec![1.0, 0.0, 0.0],
744 MemoryType::Semantic,
745 );
746 original.agent_id = agent;
747 original.created_at = 5000;
748 let mut correction = make_memory(
749 "The user's favorite coffee is a flat white",
750 vec![0.98, 0.02, 0.0],
751 MemoryType::Correction,
752 );
753 correction.agent_id = agent;
754 correction.created_at = 5000;
755
756 let actions =
757 WriteInferenceEngine::new().infer_on_write(&correction, &[original.clone()], &[]);
758 assert!(
759 actions.iter().any(|a| matches!(
760 a,
761 InferredAction::InvalidateMemory { memory, superseded_by, .. }
762 if *superseded_by == correction.id && *memory == original.id
763 )),
764 "the correction must invalidate the original at equal timestamps, got: {actions:?}"
765 );
766 }
767
768 #[test]
769 fn multivalued_facts_at_equal_timestamps_are_both_kept() {
770 let agent = AgentId::new();
775 let mut a = make_memory(
776 "The user knows Rust",
777 vec![1.0, 0.0, 0.0],
778 MemoryType::Semantic,
779 );
780 a.agent_id = agent;
781 a.created_at = 5000;
782 let mut b = make_memory(
783 "The user knows Python",
784 vec![0.97, 0.03, 0.0],
785 MemoryType::Semantic,
786 );
787 b.agent_id = agent;
788 b.created_at = 5000;
789
790 let actions = WriteInferenceEngine::new().infer_on_write(&b, &[a], &[]);
791 assert!(
792 !actions.iter().any(|x| matches!(
793 x,
794 InferredAction::CreateEdge {
795 edge_type: EdgeType::Supersedes,
796 ..
797 }
798 )),
799 "coexisting facts at equal timestamps must not supersede, got: {actions:?}"
800 );
801 }
802
803 #[test]
804 fn subject_change_is_not_a_value_update() {
805 let agent = AgentId::new();
808 let mut existing = make_memory(
809 "My dog is allergic to chicken",
810 vec![1.0, 0.0, 0.0],
811 MemoryType::Semantic,
812 );
813 existing.agent_id = agent;
814 let mut new_mem = make_memory(
815 "My cat is allergic to chicken",
816 vec![0.99, 0.01, 0.0],
817 MemoryType::Semantic,
818 );
819 new_mem.agent_id = agent;
820 new_mem.created_at = 2000;
821
822 let engine = WriteInferenceEngine::new();
823 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
824 assert!(
825 !actions.iter().any(|a| matches!(
826 a,
827 InferredAction::CreateEdge {
828 edge_type: EdgeType::Supersedes,
829 ..
830 }
831 )),
832 "a subject change must not supersede, got: {actions:?}"
833 );
834 }
835
836 #[test]
837 fn value_update_respects_owner_and_config() {
838 let mut existing = make_memory(
840 "The user's favorite coffee order is a cortado",
841 vec![1.0, 0.0, 0.0],
842 MemoryType::Semantic,
843 );
844 existing.agent_id = AgentId::new();
845 let mut new_mem = make_memory(
846 "The user's favorite coffee order is a flat white",
847 vec![0.98, 0.02, 0.0],
848 MemoryType::Semantic,
849 );
850 new_mem.agent_id = AgentId::new(); new_mem.created_at = 2000;
852
853 let engine = WriteInferenceEngine::new();
854 let actions = engine.infer_on_write(&new_mem, &[existing.clone()], &[]);
855 assert!(
856 !actions.iter().any(|a| matches!(
857 a,
858 InferredAction::CreateEdge {
859 edge_type: EdgeType::Supersedes,
860 ..
861 }
862 )),
863 "cross-owner memories must never supersede each other"
864 );
865
866 let agent = AgentId::new();
868 existing.agent_id = agent;
869 new_mem.agent_id = agent;
870 let engine_off = WriteInferenceEngine::with_config(WriteInferenceConfig {
871 value_update_enabled: false,
872 ..WriteInferenceConfig::default()
873 });
874 let actions = engine_off.infer_on_write(&new_mem, &[existing], &[]);
875 assert!(
876 !actions.iter().any(|a| matches!(
877 a,
878 InferredAction::CreateEdge {
879 edge_type: EdgeType::Supersedes,
880 ..
881 }
882 )),
883 "disabled rule must not supersede"
884 );
885 }
886
887 #[test]
888 fn is_value_update_frame_analysis() {
889 assert!(is_value_update(
891 "my phone number is 4321",
892 "my phone number is 1234",
893 0.6,
894 4
895 ));
896 assert!(!is_value_update(
898 "my cat is allergic to chicken",
899 "my dog is allergic to chicken",
900 0.6,
901 4
902 ));
903 assert!(!is_value_update("same fact", "same fact", 0.6, 4));
905 assert!(is_value_update(
910 "The user uses React for the front end",
911 "The user uses Next.js for the front end",
912 0.6,
913 4
914 ));
915 assert!(is_value_update(
916 "i use react for the front end",
917 "i use next.js for the front end",
918 0.6,
919 4
920 ));
921 assert!(!is_value_update(
924 "the cat is allergic to chicken and needs the special food",
925 "the dog is allergic to chicken and needs the special food",
926 0.6,
927 4
928 ));
929 assert!(!is_value_update(
933 "API sibling rule 1 about payload naming",
934 "API sibling rule 0 about payload naming",
935 0.6,
936 4
937 ));
938 assert!(!is_value_update(
942 "Directive number 1",
943 "Directive number 0",
944 0.6,
945 4
946 ));
947 assert!(!is_value_update(
949 "the deploy runs every friday and sarah reviews it after standup",
950 "the deploy runs every friday unless the release train is frozen for the quarter end audit",
951 0.6,
952 4
953 ));
954 }
955
956 #[test]
957 fn test_byte_identical_duplicate_is_deduplicated() {
958 let agent = AgentId::new();
961 let mut existing = make_memory(
962 "Ran command: ls -la",
963 vec![1.0, 0.0, 0.0],
964 MemoryType::Episodic,
965 );
966 existing.agent_id = agent;
967
968 let mut new_mem = make_memory(
969 "Ran command: ls -la",
970 vec![1.0, 0.0, 0.0],
971 MemoryType::Episodic,
972 );
973 new_mem.agent_id = agent;
974 new_mem.created_at = 2000;
975
976 let engine = WriteInferenceEngine::new();
977 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
978 assert!(
979 actions
980 .iter()
981 .any(|a| matches!(a, InferredAction::DeduplicateExact { .. })),
982 "Expected identical duplicate to be deduplicated, got: {:?}",
983 actions
984 );
985 }
986
987 #[test]
988 fn test_moderate_similarity_creates_edge() {
989 let existing = make_memory("topic A", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
990 let new_mem = make_memory("topic B", vec![0.7, 0.714, 0.0], MemoryType::Semantic);
992
993 let engine = WriteInferenceEngine::new();
994 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
995 assert!(
996 actions.iter().any(|a| matches!(
997 a,
998 InferredAction::CreateEdge {
999 edge_type: EdgeType::Related,
1000 ..
1001 }
1002 )),
1003 "Expected CreateEdge Related, got: {:?}",
1004 actions
1005 );
1006 }
1007
1008 #[tokio::test]
1009 async fn test_llm_invalidation_emits_temporal_actions() {
1010 let agent = AgentId::new();
1011 let mut existing = make_memory(
1012 "Alice works at Acme",
1013 vec![0.8, 0.6, 0.0],
1014 MemoryType::Semantic,
1015 );
1016 existing.agent_id = agent;
1017
1018 let mut new_mem = make_memory(
1019 "Alice joined Google last week",
1020 vec![0.75, 0.65, 0.1],
1021 MemoryType::Semantic,
1022 );
1023 new_mem.agent_id = agent;
1024 new_mem.created_at = 2000;
1025
1026 let judge =
1027 MockLlmJudge::new(r#"{"verdict": "invalidate", "reason": "Alice changed jobs"}"#);
1028 let llm = CognitiveLlmService::new(judge);
1029 let engine = WriteInferenceEngine::new();
1030 let actions = engine
1031 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
1032 .await;
1033
1034 assert!(
1035 actions
1036 .iter()
1037 .any(|a| matches!(a, InferredAction::InvalidateMemory { .. })),
1038 "Expected InvalidateMemory from LLM verdict, got: {:?}",
1039 actions
1040 );
1041 let invalidation_count = actions
1045 .iter()
1046 .filter(|a| {
1047 matches!(
1048 a,
1049 InferredAction::InvalidateMemory { .. } | InferredAction::MarkObsolete { .. }
1050 )
1051 })
1052 .count();
1053 assert_eq!(
1054 invalidation_count, 1,
1055 "Expected a single invalidation action, got: {:?}",
1056 actions
1057 );
1058 }
1059
1060 #[tokio::test]
1061 async fn test_llm_update_emits_update_content() {
1062 let agent = AgentId::new();
1063 let mut existing = make_memory(
1064 "Project uses React",
1065 vec![0.8, 0.6, 0.0],
1066 MemoryType::Semantic,
1067 );
1068 existing.agent_id = agent;
1069
1070 let mut new_mem = make_memory(
1071 "Project migrated from React to Vue",
1072 vec![0.75, 0.65, 0.1],
1073 MemoryType::Semantic,
1074 );
1075 new_mem.agent_id = agent;
1076 new_mem.created_at = 2000;
1077
1078 let judge = MockLlmJudge::new(
1079 r#"{"verdict": "update", "merged_content": "Project migrated from React to Vue in Q2", "reason": "adds temporal context"}"#,
1080 );
1081 let llm = CognitiveLlmService::new(judge);
1082 let engine = WriteInferenceEngine::new();
1083 let actions = engine
1084 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
1085 .await;
1086
1087 assert!(
1088 actions
1089 .iter()
1090 .any(|a| matches!(a, InferredAction::UpdateContent { .. })),
1091 "Expected UpdateContent from LLM update verdict, got: {:?}",
1092 actions
1093 );
1094 }
1095
1096 #[tokio::test]
1097 async fn test_llm_keep_falls_through_to_contradiction_check() {
1098 let agent = AgentId::new();
1099 let mut existing = make_memory("Prefers tabs", vec![0.9, 0.44, 0.0], MemoryType::Semantic);
1101 existing.agent_id = agent;
1102
1103 let mut new_mem = make_memory(
1104 "Prefers spaces",
1105 vec![0.88, 0.47, 0.0],
1106 MemoryType::Semantic,
1107 );
1108 new_mem.agent_id = agent;
1109 new_mem.created_at = 2000;
1110
1111 let judge = MockLlmJudge::new(
1112 r#"{"verdict": "compatible", "reason": "different formatting preferences"}"#,
1113 );
1114 let llm = CognitiveLlmService::new(judge);
1115 let engine = WriteInferenceEngine::new();
1116 let actions = engine
1117 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
1118 .await;
1119
1120 assert!(
1123 !actions
1124 .iter()
1125 .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
1126 "Should not flag contradiction when LLM says compatible",
1127 );
1128 }
1129}