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,
114 pub value_update_prefix_share: f32,
118 pub value_update_max_tail: usize,
121}
122
123impl Default for WriteInferenceConfig {
124 fn default() -> Self {
125 Self {
126 contradiction_threshold: 0.95,
127 obsolete_threshold: 0.85,
128 related_min: 0.6,
129 related_max: 0.85,
130 correction_threshold: 0.5,
131 confidence_decay_factor: 0.5,
132 confidence_floor: 0.1,
133 value_update_enabled: true,
134 value_update_min_similarity: 0.3,
135 value_update_prefix_share: 0.6,
136 value_update_max_tail: 4,
137 }
138 }
139}
140
141fn frame_tokens(s: &str) -> Vec<String> {
144 s.split(|c: char| !c.is_alphanumeric())
145 .filter(|t| !t.is_empty())
146 .map(|t| t.to_lowercase())
147 .collect()
148}
149
150fn is_value_update(new: &str, old: &str, frame_share: f32, max_value: usize) -> bool {
172 let nt = frame_tokens(new);
173 let ot = frame_tokens(old);
174 if nt.is_empty() || ot.is_empty() || nt == ot {
175 return false;
176 }
177 if nt.len().min(ot.len()) < 4 {
182 return false;
183 }
184 let prefix = nt.iter().zip(ot.iter()).take_while(|(a, b)| a == b).count();
185 if prefix < 2 {
186 return false;
187 }
188 let suffix = nt
189 .iter()
190 .rev()
191 .zip(ot.iter().rev())
192 .take_while(|(a, b)| a == b)
193 .count()
194 .min(nt.len().min(ot.len()) - prefix);
196 let longest = nt.len().max(ot.len());
197 let value_new = nt.len() - prefix - suffix;
198 let value_old = ot.len() - prefix - suffix;
199 if suffix > 0 {
200 let all_numeric =
201 |tokens: &[String]| tokens.iter().all(|t| t.chars().all(|c| c.is_ascii_digit()));
202 if all_numeric(&nt[prefix..nt.len() - suffix])
203 && all_numeric(&ot[prefix..ot.len() - suffix])
204 {
205 return false;
206 }
207 }
208 (prefix + suffix) as f32 / longest as f32 >= frame_share
209 && value_new.max(value_old) > 0
210 && value_new <= max_value
211 && value_old <= max_value
212}
213
214fn value_update_resolution(
222 new: &MemoryNode,
223 existing: &MemoryNode,
224) -> Option<(MemoryId, MemoryId, f32)> {
225 let new_correction = new.memory_type == MemoryType::Correction;
226 let old_correction = existing.memory_type == MemoryType::Correction;
227 if new_correction && !old_correction {
228 return Some((new.id, existing.id, existing.confidence));
229 }
230 if old_correction && !new_correction {
231 return Some((existing.id, new.id, new.confidence));
232 }
233 if new.created_at > existing.created_at {
234 return Some((new.id, existing.id, existing.confidence));
235 }
236 if existing.created_at > new.created_at {
237 return Some((existing.id, new.id, new.confidence));
238 }
239 None
240}
241
242pub struct WriteInferenceEngine {
243 config: WriteInferenceConfig,
244}
245
246impl WriteInferenceEngine {
247 pub fn new() -> Self {
248 Self {
249 config: WriteInferenceConfig::default(),
250 }
251 }
252
253 pub fn with_config(config: WriteInferenceConfig) -> Self {
254 Self { config }
255 }
256
257 pub fn looks_like_value_update(&self, new: &str, old: &str) -> bool {
264 is_value_update(
265 new,
266 old,
267 self.config.value_update_prefix_share,
268 self.config.value_update_max_tail,
269 )
270 }
271
272 pub fn infer_on_write(
273 &self,
274 new_memory: &MemoryNode,
275 existing_memories: &[MemoryNode],
276 existing_edges: &[(MemoryId, MemoryId, EdgeType)],
277 ) -> Vec<InferredAction> {
278 let _ = existing_edges; let mut actions = Vec::new();
280 let mut value_update_handled: Vec<MemoryId> = Vec::new();
283
284 for existing in existing_memories {
285 if existing.id == new_memory.id {
286 continue;
287 }
288
289 let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
290
291 if existing.content == new_memory.content
305 && sim > self.config.obsolete_threshold
306 && new_memory.created_at > existing.created_at
307 {
308 actions.push(InferredAction::DeduplicateExact {
309 duplicate: existing.id,
310 keeper: new_memory.id,
311 });
312 } else if self.config.value_update_enabled
313 && existing.agent_id == new_memory.agent_id
314 && existing.user_id == new_memory.user_id
315 && is_value_update(
316 &new_memory.content,
317 &existing.content,
318 self.config.value_update_prefix_share,
319 self.config.value_update_max_tail,
320 )
321 {
322 if sim < self.config.value_update_min_similarity {
323 tracing::info!(
328 similarity = sim,
329 "value update frame matched below the cosine floor, skipped"
330 );
331 continue;
332 }
333 value_update_handled.push(existing.id);
342 if let Some((winner, loser, loser_confidence)) =
343 value_update_resolution(new_memory, existing)
344 {
345 let now = std::time::SystemTime::now()
351 .duration_since(std::time::UNIX_EPOCH)
352 .unwrap_or_default()
353 .as_micros() as u64;
354 actions.push(InferredAction::InvalidateMemory {
355 memory: loser,
356 superseded_by: winner,
357 valid_until: now,
358 });
359 actions.push(InferredAction::UpdateConfidence {
360 memory: loser,
361 new_confidence: (loser_confidence * self.config.confidence_decay_factor)
362 .max(self.config.confidence_floor),
363 });
364 }
365 } else if sim > self.config.related_min && sim <= self.config.related_max {
369 actions.push(InferredAction::CreateEdge {
370 source: new_memory.id,
371 target: existing.id,
372 edge_type: EdgeType::Related,
373 weight: sim,
374 });
375 }
376 }
377
378 if new_memory.memory_type == MemoryType::Correction
381 && let Some(original) = existing_memories
382 .iter()
383 .filter(|m| m.id != new_memory.id && !value_update_handled.contains(&m.id))
384 .max_by(|a, b| {
385 cosine_similarity(&new_memory.embedding, &a.embedding)
386 .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
387 .unwrap_or(std::cmp::Ordering::Equal)
388 })
389 {
390 let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
391 if sim > self.config.correction_threshold {
392 actions.push(InferredAction::CreateEdge {
393 source: new_memory.id,
394 target: original.id,
395 edge_type: EdgeType::Supersedes,
396 weight: 1.0,
397 });
398 actions.push(InferredAction::UpdateConfidence {
399 memory: original.id,
400 new_confidence: (original.confidence * self.config.confidence_decay_factor)
401 .max(self.config.confidence_floor),
402 });
403 }
404 }
405
406 actions
407 }
408
409 pub async fn infer_on_write_with_llm<J: LlmJudge>(
414 &self,
415 new_memory: &MemoryNode,
416 existing_memories: &[MemoryNode],
417 existing_edges: &[(MemoryId, MemoryId, EdgeType)],
418 llm: &CognitiveLlmService<J>,
419 ) -> Vec<InferredAction> {
420 let _ = existing_edges;
421 let mut actions = Vec::new();
422
423 let new_summary = memory_to_summary(new_memory);
424
425 for existing in existing_memories {
426 if existing.id == new_memory.id {
427 continue;
428 }
429
430 let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
431
432 if sim > 0.5 && existing.agent_id == new_memory.agent_id {
435 let old_summary = memory_to_summary(existing);
436
437 if let Ok(verdict) = llm.judge_invalidation(&old_summary, &new_summary).await {
439 match verdict {
440 InvalidationVerdict::Invalidate { reason: _ } => {
441 actions.push(InferredAction::InvalidateMemory {
444 memory: existing.id,
445 superseded_by: new_memory.id,
446 valid_until: new_memory.created_at,
447 });
448 actions.push(InferredAction::UpdateConfidence {
449 memory: existing.id,
450 new_confidence: (existing.confidence
451 * self.config.confidence_decay_factor)
452 .max(self.config.confidence_floor),
453 });
454 continue;
456 }
457 InvalidationVerdict::Update {
458 merged_content,
459 reason,
460 } => {
461 actions.push(InferredAction::UpdateContent {
462 memory: existing.id,
463 new_content: merged_content,
464 reason,
465 });
466 continue;
467 }
468 InvalidationVerdict::Keep { .. } => {
469 }
471 }
472 }
473
474 if sim > 0.7
476 && existing.content != new_memory.content
477 && let Ok(verdict) = llm.detect_contradiction(&old_summary, &new_summary).await
478 {
479 match verdict {
480 ContradictionVerdict::Contradicts { reason } => {
481 actions.push(InferredAction::FlagContradiction {
482 existing: existing.id,
483 new: new_memory.id,
484 reason,
485 });
486 }
487 ContradictionVerdict::Supersedes { winner, reason: _ } => {
488 let winner_is_new = winner == new_memory.id.to_string();
489 let (obsolete, superseder) = if winner_is_new {
490 (existing.id, new_memory.id)
491 } else {
492 (new_memory.id, existing.id)
493 };
494 actions.push(InferredAction::InvalidateMemory {
495 memory: obsolete,
496 superseded_by: superseder,
497 valid_until: new_memory.created_at,
498 });
499 }
500 ContradictionVerdict::Compatible { .. } => {}
501 }
502 }
503 }
504
505 if sim > self.config.related_min && sim <= self.config.related_max {
507 actions.push(InferredAction::CreateEdge {
508 source: new_memory.id,
509 target: existing.id,
510 edge_type: EdgeType::Related,
511 weight: sim,
512 });
513 }
514 }
515
516 if new_memory.memory_type == MemoryType::Correction
518 && let Some(original) = existing_memories
519 .iter()
520 .filter(|m| m.id != new_memory.id)
521 .max_by(|a, b| {
522 cosine_similarity(&new_memory.embedding, &a.embedding)
523 .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
524 .unwrap_or(std::cmp::Ordering::Equal)
525 })
526 {
527 let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
528 if sim > self.config.correction_threshold {
529 actions.push(InferredAction::InvalidateMemory {
530 memory: original.id,
531 superseded_by: new_memory.id,
532 valid_until: new_memory.created_at,
533 });
534 actions.push(InferredAction::UpdateConfidence {
535 memory: original.id,
536 new_confidence: (original.confidence * self.config.confidence_decay_factor)
537 .max(self.config.confidence_floor),
538 });
539 }
540 }
541
542 actions
543 }
544}
545
546impl Default for WriteInferenceEngine {
547 fn default() -> Self {
548 Self::new()
549 }
550}
551
552fn memory_to_summary(m: &MemoryNode) -> MemorySummary {
553 MemorySummary {
554 id: m.id,
555 content: m.content.clone(),
556 memory_type: m.memory_type,
557 confidence: m.confidence,
558 created_at: m.created_at,
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use mentedb_core::memory::MemoryType;
566 use mentedb_core::types::AgentId;
567
568 use crate::llm::MockLlmJudge;
569
570 fn make_memory(content: &str, embedding: Vec<f32>, mem_type: MemoryType) -> MemoryNode {
571 let mut m = MemoryNode::new(AgentId::new(), mem_type, content.to_string(), embedding);
572 m.created_at = 1000;
573 m
574 }
575
576 #[test]
577 fn test_heuristic_does_not_flag_contradiction_from_similarity() {
578 let agent = AgentId::new();
585 let mut existing =
586 make_memory("uses PostgreSQL", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
587 existing.agent_id = agent;
588
589 let mut new_mem = make_memory("uses MySQL", vec![0.99, 0.01, 0.0], MemoryType::Semantic);
590 new_mem.agent_id = agent;
591 new_mem.created_at = 2000;
592
593 let engine = WriteInferenceEngine::new();
594 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
595 assert!(
596 !actions
597 .iter()
598 .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
599 "Heuristic must not flag contradictions from bare similarity, got: {:?}",
600 actions
601 );
602 }
603
604 #[test]
605 fn value_update_supersedes_the_old_fact() {
606 let agent = AgentId::new();
610 let mut existing = make_memory(
611 "The user's favorite coffee order is a cortado",
612 vec![1.0, 0.0, 0.0],
613 MemoryType::Semantic,
614 );
615 existing.agent_id = agent;
616 let mut new_mem = make_memory(
617 "The user's favorite coffee order is a flat white",
618 vec![0.98, 0.02, 0.0],
619 MemoryType::Semantic,
620 );
621 new_mem.agent_id = agent;
622 new_mem.created_at = 2000;
623
624 let engine = WriteInferenceEngine::new();
625 let actions = engine.infer_on_write(&new_mem, &[existing.clone()], &[]);
626 assert!(
627 actions.iter().any(|a| matches!(
628 a,
629 InferredAction::InvalidateMemory { memory, superseded_by, .. }
630 if *superseded_by == new_mem.id && *memory == existing.id
631 )),
632 "value update must invalidate the stale fact, got: {actions:?}"
633 );
634 assert!(
635 actions
636 .iter()
637 .any(|a| matches!(a, InferredAction::UpdateConfidence { memory, .. } if *memory == existing.id)),
638 "superseded fact's confidence must decay"
639 );
640 }
641
642 #[test]
643 fn value_update_fires_at_realistic_embedder_cosine() {
644 let agent = AgentId::new();
652 let mut existing = make_memory(
653 "The user's favorite coffee order is a cortado",
654 vec![1.0, 0.0, 0.0],
655 MemoryType::Semantic,
656 );
657 existing.agent_id = agent;
658 let mut new_mem = make_memory(
660 "The user's favorite coffee order is a flat white",
661 vec![0.6, 0.8, 0.0],
662 MemoryType::Semantic,
663 );
664 new_mem.agent_id = agent;
665 new_mem.created_at = 2000;
666
667 let actions =
668 WriteInferenceEngine::new().infer_on_write(&new_mem, &[existing.clone()], &[]);
669 assert!(
670 actions.iter().any(|a| matches!(
671 a,
672 InferredAction::InvalidateMemory { memory, superseded_by, .. }
673 if *superseded_by == new_mem.id && *memory == existing.id
674 )),
675 "a frame-matched correction at realistic cosine 0.6 must supersede, got: {actions:?}"
676 );
677 }
678
679 #[test]
680 fn value_update_fires_below_the_old_floor() {
681 let agent = AgentId::new();
685 let mut existing = make_memory(
686 "The user uses Next.js for the front end",
687 vec![1.0, 0.0, 0.0],
688 MemoryType::Semantic,
689 );
690 existing.agent_id = agent;
691 let mut new_mem = make_memory(
692 "The user uses React for the front end",
693 vec![0.4, 0.9165, 0.0],
694 MemoryType::Semantic,
695 );
696 new_mem.agent_id = agent;
697 new_mem.created_at = 2000;
698
699 let actions =
700 WriteInferenceEngine::new().infer_on_write(&new_mem, &[existing.clone()], &[]);
701 assert!(
702 actions.iter().any(|a| matches!(
703 a,
704 InferredAction::InvalidateMemory { memory, superseded_by, .. }
705 if *superseded_by == new_mem.id && *memory == existing.id
706 )),
707 "an identical frame pair at cosine 0.4 must supersede, got: {actions:?}"
708 );
709 }
710
711 #[test]
712 fn value_update_below_cosine_floor_does_not_supersede() {
713 let agent = AgentId::new();
718 let mut existing = make_memory(
719 "The user's favorite coffee order is a cortado",
720 vec![1.0, 0.0, 0.0],
721 MemoryType::Semantic,
722 );
723 existing.agent_id = agent;
724 let mut new_mem = make_memory(
726 "The user's favorite coffee order is a flat white",
727 vec![0.2, 0.9798, 0.0],
728 MemoryType::Semantic,
729 );
730 new_mem.agent_id = agent;
731 new_mem.created_at = 2000;
732
733 let actions = WriteInferenceEngine::new().infer_on_write(&new_mem, &[existing], &[]);
734 assert!(
735 !actions
736 .iter()
737 .any(|a| matches!(a, InferredAction::InvalidateMemory { .. })),
738 "below the cosine floor a frame match must not supersede, got: {actions:?}"
739 );
740 }
741
742 #[test]
743 fn correction_supersedes_regardless_of_store_order() {
744 let agent = AgentId::new();
749 let mut correction = make_memory(
750 "The user's favorite coffee is a flat white",
751 vec![1.0, 0.0, 0.0],
752 MemoryType::Correction,
753 );
754 correction.agent_id = agent;
755 correction.created_at = 5000;
756 let mut original = make_memory(
757 "The user's favorite coffee is a cortado",
758 vec![0.98, 0.02, 0.0],
759 MemoryType::Semantic,
760 );
761 original.agent_id = agent;
762 original.created_at = 5000; let actions =
766 WriteInferenceEngine::new().infer_on_write(&original, &[correction.clone()], &[]);
767 assert!(
768 actions.iter().any(|a| matches!(
769 a,
770 InferredAction::InvalidateMemory { memory, superseded_by, .. }
771 if *superseded_by == correction.id && *memory == original.id
772 )),
773 "the correction must invalidate the later-arriving original, got: {actions:?}"
774 );
775 }
776
777 #[test]
778 fn correction_supersedes_original_at_equal_timestamps() {
779 let agent = AgentId::new();
782 let mut original = make_memory(
783 "The user's favorite coffee is a cortado",
784 vec![1.0, 0.0, 0.0],
785 MemoryType::Semantic,
786 );
787 original.agent_id = agent;
788 original.created_at = 5000;
789 let mut correction = make_memory(
790 "The user's favorite coffee is a flat white",
791 vec![0.98, 0.02, 0.0],
792 MemoryType::Correction,
793 );
794 correction.agent_id = agent;
795 correction.created_at = 5000;
796
797 let actions =
798 WriteInferenceEngine::new().infer_on_write(&correction, &[original.clone()], &[]);
799 assert!(
800 actions.iter().any(|a| matches!(
801 a,
802 InferredAction::InvalidateMemory { memory, superseded_by, .. }
803 if *superseded_by == correction.id && *memory == original.id
804 )),
805 "the correction must invalidate the original at equal timestamps, got: {actions:?}"
806 );
807 }
808
809 #[test]
810 fn multivalued_facts_at_equal_timestamps_are_both_kept() {
811 let agent = AgentId::new();
816 let mut a = make_memory(
817 "The user knows Rust",
818 vec![1.0, 0.0, 0.0],
819 MemoryType::Semantic,
820 );
821 a.agent_id = agent;
822 a.created_at = 5000;
823 let mut b = make_memory(
824 "The user knows Python",
825 vec![0.97, 0.03, 0.0],
826 MemoryType::Semantic,
827 );
828 b.agent_id = agent;
829 b.created_at = 5000;
830
831 let actions = WriteInferenceEngine::new().infer_on_write(&b, &[a], &[]);
832 assert!(
833 !actions.iter().any(|x| matches!(
834 x,
835 InferredAction::CreateEdge {
836 edge_type: EdgeType::Supersedes,
837 ..
838 }
839 )),
840 "coexisting facts at equal timestamps must not supersede, got: {actions:?}"
841 );
842 }
843
844 #[test]
845 fn subject_change_is_not_a_value_update() {
846 let agent = AgentId::new();
849 let mut existing = make_memory(
850 "My dog is allergic to chicken",
851 vec![1.0, 0.0, 0.0],
852 MemoryType::Semantic,
853 );
854 existing.agent_id = agent;
855 let mut new_mem = make_memory(
856 "My cat is allergic to chicken",
857 vec![0.99, 0.01, 0.0],
858 MemoryType::Semantic,
859 );
860 new_mem.agent_id = agent;
861 new_mem.created_at = 2000;
862
863 let engine = WriteInferenceEngine::new();
864 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
865 assert!(
866 !actions.iter().any(|a| matches!(
867 a,
868 InferredAction::CreateEdge {
869 edge_type: EdgeType::Supersedes,
870 ..
871 }
872 )),
873 "a subject change must not supersede, got: {actions:?}"
874 );
875 }
876
877 #[test]
878 fn value_update_respects_owner_and_config() {
879 let mut existing = make_memory(
881 "The user's favorite coffee order is a cortado",
882 vec![1.0, 0.0, 0.0],
883 MemoryType::Semantic,
884 );
885 existing.agent_id = AgentId::new();
886 let mut new_mem = make_memory(
887 "The user's favorite coffee order is a flat white",
888 vec![0.98, 0.02, 0.0],
889 MemoryType::Semantic,
890 );
891 new_mem.agent_id = AgentId::new(); new_mem.created_at = 2000;
893
894 let engine = WriteInferenceEngine::new();
895 let actions = engine.infer_on_write(&new_mem, &[existing.clone()], &[]);
896 assert!(
897 !actions.iter().any(|a| matches!(
898 a,
899 InferredAction::CreateEdge {
900 edge_type: EdgeType::Supersedes,
901 ..
902 }
903 )),
904 "cross-owner memories must never supersede each other"
905 );
906
907 let agent = AgentId::new();
909 existing.agent_id = agent;
910 new_mem.agent_id = agent;
911 let engine_off = WriteInferenceEngine::with_config(WriteInferenceConfig {
912 value_update_enabled: false,
913 ..WriteInferenceConfig::default()
914 });
915 let actions = engine_off.infer_on_write(&new_mem, &[existing], &[]);
916 assert!(
917 !actions.iter().any(|a| matches!(
918 a,
919 InferredAction::CreateEdge {
920 edge_type: EdgeType::Supersedes,
921 ..
922 }
923 )),
924 "disabled rule must not supersede"
925 );
926 }
927
928 #[test]
929 fn is_value_update_frame_analysis() {
930 assert!(is_value_update(
932 "my phone number is 4321",
933 "my phone number is 1234",
934 0.6,
935 4
936 ));
937 assert!(!is_value_update(
939 "my cat is allergic to chicken",
940 "my dog is allergic to chicken",
941 0.6,
942 4
943 ));
944 assert!(!is_value_update("same fact", "same fact", 0.6, 4));
946 assert!(is_value_update(
951 "The user uses React for the front end",
952 "The user uses Next.js for the front end",
953 0.6,
954 4
955 ));
956 assert!(is_value_update(
957 "i use react for the front end",
958 "i use next.js for the front end",
959 0.6,
960 4
961 ));
962 assert!(!is_value_update(
965 "the cat is allergic to chicken and needs the special food",
966 "the dog is allergic to chicken and needs the special food",
967 0.6,
968 4
969 ));
970 assert!(!is_value_update(
974 "API sibling rule 1 about payload naming",
975 "API sibling rule 0 about payload naming",
976 0.6,
977 4
978 ));
979 assert!(!is_value_update(
983 "Directive number 1",
984 "Directive number 0",
985 0.6,
986 4
987 ));
988 assert!(!is_value_update(
990 "the deploy runs every friday and sarah reviews it after standup",
991 "the deploy runs every friday unless the release train is frozen for the quarter end audit",
992 0.6,
993 4
994 ));
995 }
996
997 #[test]
998 fn test_byte_identical_duplicate_is_deduplicated() {
999 let agent = AgentId::new();
1002 let mut existing = make_memory(
1003 "Ran command: ls -la",
1004 vec![1.0, 0.0, 0.0],
1005 MemoryType::Episodic,
1006 );
1007 existing.agent_id = agent;
1008
1009 let mut new_mem = make_memory(
1010 "Ran command: ls -la",
1011 vec![1.0, 0.0, 0.0],
1012 MemoryType::Episodic,
1013 );
1014 new_mem.agent_id = agent;
1015 new_mem.created_at = 2000;
1016
1017 let engine = WriteInferenceEngine::new();
1018 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
1019 assert!(
1020 actions
1021 .iter()
1022 .any(|a| matches!(a, InferredAction::DeduplicateExact { .. })),
1023 "Expected identical duplicate to be deduplicated, got: {:?}",
1024 actions
1025 );
1026 }
1027
1028 #[test]
1029 fn test_moderate_similarity_creates_edge() {
1030 let existing = make_memory("topic A", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
1031 let new_mem = make_memory("topic B", vec![0.7, 0.714, 0.0], MemoryType::Semantic);
1033
1034 let engine = WriteInferenceEngine::new();
1035 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
1036 assert!(
1037 actions.iter().any(|a| matches!(
1038 a,
1039 InferredAction::CreateEdge {
1040 edge_type: EdgeType::Related,
1041 ..
1042 }
1043 )),
1044 "Expected CreateEdge Related, got: {:?}",
1045 actions
1046 );
1047 }
1048
1049 #[tokio::test]
1050 async fn test_llm_invalidation_emits_temporal_actions() {
1051 let agent = AgentId::new();
1052 let mut existing = make_memory(
1053 "Alice works at Acme",
1054 vec![0.8, 0.6, 0.0],
1055 MemoryType::Semantic,
1056 );
1057 existing.agent_id = agent;
1058
1059 let mut new_mem = make_memory(
1060 "Alice joined Google last week",
1061 vec![0.75, 0.65, 0.1],
1062 MemoryType::Semantic,
1063 );
1064 new_mem.agent_id = agent;
1065 new_mem.created_at = 2000;
1066
1067 let judge =
1068 MockLlmJudge::new(r#"{"verdict": "invalidate", "reason": "Alice changed jobs"}"#);
1069 let llm = CognitiveLlmService::new(judge);
1070 let engine = WriteInferenceEngine::new();
1071 let actions = engine
1072 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
1073 .await;
1074
1075 assert!(
1076 actions
1077 .iter()
1078 .any(|a| matches!(a, InferredAction::InvalidateMemory { .. })),
1079 "Expected InvalidateMemory from LLM verdict, got: {:?}",
1080 actions
1081 );
1082 let invalidation_count = actions
1086 .iter()
1087 .filter(|a| {
1088 matches!(
1089 a,
1090 InferredAction::InvalidateMemory { .. } | InferredAction::MarkObsolete { .. }
1091 )
1092 })
1093 .count();
1094 assert_eq!(
1095 invalidation_count, 1,
1096 "Expected a single invalidation action, got: {:?}",
1097 actions
1098 );
1099 }
1100
1101 #[tokio::test]
1102 async fn test_llm_update_emits_update_content() {
1103 let agent = AgentId::new();
1104 let mut existing = make_memory(
1105 "Project uses React",
1106 vec![0.8, 0.6, 0.0],
1107 MemoryType::Semantic,
1108 );
1109 existing.agent_id = agent;
1110
1111 let mut new_mem = make_memory(
1112 "Project migrated from React to Vue",
1113 vec![0.75, 0.65, 0.1],
1114 MemoryType::Semantic,
1115 );
1116 new_mem.agent_id = agent;
1117 new_mem.created_at = 2000;
1118
1119 let judge = MockLlmJudge::new(
1120 r#"{"verdict": "update", "merged_content": "Project migrated from React to Vue in Q2", "reason": "adds temporal context"}"#,
1121 );
1122 let llm = CognitiveLlmService::new(judge);
1123 let engine = WriteInferenceEngine::new();
1124 let actions = engine
1125 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
1126 .await;
1127
1128 assert!(
1129 actions
1130 .iter()
1131 .any(|a| matches!(a, InferredAction::UpdateContent { .. })),
1132 "Expected UpdateContent from LLM update verdict, got: {:?}",
1133 actions
1134 );
1135 }
1136
1137 #[tokio::test]
1138 async fn test_llm_keep_falls_through_to_contradiction_check() {
1139 let agent = AgentId::new();
1140 let mut existing = make_memory("Prefers tabs", vec![0.9, 0.44, 0.0], MemoryType::Semantic);
1142 existing.agent_id = agent;
1143
1144 let mut new_mem = make_memory(
1145 "Prefers spaces",
1146 vec![0.88, 0.47, 0.0],
1147 MemoryType::Semantic,
1148 );
1149 new_mem.agent_id = agent;
1150 new_mem.created_at = 2000;
1151
1152 let judge = MockLlmJudge::new(
1153 r#"{"verdict": "compatible", "reason": "different formatting preferences"}"#,
1154 );
1155 let llm = CognitiveLlmService::new(judge);
1156 let engine = WriteInferenceEngine::new();
1157 let actions = engine
1158 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
1159 .await;
1160
1161 assert!(
1164 !actions
1165 .iter()
1166 .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
1167 "Should not flag contradiction when LLM says compatible",
1168 );
1169 }
1170}