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,
114 pub value_update_max_tail: usize,
117}
118
119impl Default for WriteInferenceConfig {
120 fn default() -> Self {
121 Self {
122 contradiction_threshold: 0.95,
123 obsolete_threshold: 0.85,
124 related_min: 0.6,
125 related_max: 0.85,
126 correction_threshold: 0.5,
127 confidence_decay_factor: 0.5,
128 confidence_floor: 0.1,
129 value_update_enabled: true,
130 value_update_min_similarity: 0.5,
131 value_update_prefix_share: 0.6,
132 value_update_max_tail: 4,
133 }
134 }
135}
136
137fn frame_tokens(s: &str) -> Vec<String> {
140 s.split(|c: char| !c.is_alphanumeric())
141 .filter(|t| !t.is_empty())
142 .map(|t| t.to_lowercase())
143 .collect()
144}
145
146fn is_value_update(new: &str, old: &str, prefix_share: f32, max_tail: usize) -> bool {
152 let nt = frame_tokens(new);
153 let ot = frame_tokens(old);
154 if nt.is_empty() || ot.is_empty() || nt == ot {
155 return false;
156 }
157 let prefix = nt.iter().zip(ot.iter()).take_while(|(a, b)| a == b).count();
158 let longest = nt.len().max(ot.len());
159 let tail = longest - prefix;
160 prefix as f32 / longest as f32 >= prefix_share && tail > 0 && tail <= max_tail
161}
162
163fn value_update_resolution(
171 new: &MemoryNode,
172 existing: &MemoryNode,
173) -> Option<(MemoryId, MemoryId, f32)> {
174 let new_correction = new.memory_type == MemoryType::Correction;
175 let old_correction = existing.memory_type == MemoryType::Correction;
176 if new_correction && !old_correction {
177 return Some((new.id, existing.id, existing.confidence));
178 }
179 if old_correction && !new_correction {
180 return Some((existing.id, new.id, new.confidence));
181 }
182 if new.created_at > existing.created_at {
183 return Some((new.id, existing.id, existing.confidence));
184 }
185 if existing.created_at > new.created_at {
186 return Some((existing.id, new.id, new.confidence));
187 }
188 None
189}
190
191pub struct WriteInferenceEngine {
192 config: WriteInferenceConfig,
193}
194
195impl WriteInferenceEngine {
196 pub fn new() -> Self {
197 Self {
198 config: WriteInferenceConfig::default(),
199 }
200 }
201
202 pub fn with_config(config: WriteInferenceConfig) -> Self {
203 Self { config }
204 }
205
206 pub fn looks_like_value_update(&self, new: &str, old: &str) -> bool {
213 is_value_update(
214 new,
215 old,
216 self.config.value_update_prefix_share,
217 self.config.value_update_max_tail,
218 )
219 }
220
221 pub fn infer_on_write(
222 &self,
223 new_memory: &MemoryNode,
224 existing_memories: &[MemoryNode],
225 existing_edges: &[(MemoryId, MemoryId, EdgeType)],
226 ) -> Vec<InferredAction> {
227 let _ = existing_edges; let mut actions = Vec::new();
229 let mut value_update_handled: Vec<MemoryId> = Vec::new();
232
233 for existing in existing_memories {
234 if existing.id == new_memory.id {
235 continue;
236 }
237
238 let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
239
240 if existing.content == new_memory.content
254 && sim > self.config.obsolete_threshold
255 && new_memory.created_at > existing.created_at
256 {
257 actions.push(InferredAction::DeduplicateExact {
258 duplicate: existing.id,
259 keeper: new_memory.id,
260 });
261 } else if self.config.value_update_enabled
262 && sim >= self.config.value_update_min_similarity
263 && existing.agent_id == new_memory.agent_id
264 && existing.user_id == new_memory.user_id
265 && is_value_update(
266 &new_memory.content,
267 &existing.content,
268 self.config.value_update_prefix_share,
269 self.config.value_update_max_tail,
270 )
271 {
272 value_update_handled.push(existing.id);
281 if let Some((winner, loser, loser_confidence)) =
282 value_update_resolution(new_memory, existing)
283 {
284 actions.push(InferredAction::CreateEdge {
285 source: winner,
286 target: loser,
287 edge_type: EdgeType::Supersedes,
288 weight: sim,
289 });
290 actions.push(InferredAction::UpdateConfidence {
291 memory: loser,
292 new_confidence: (loser_confidence * self.config.confidence_decay_factor)
293 .max(self.config.confidence_floor),
294 });
295 }
296 } else if sim > self.config.related_min && sim <= self.config.related_max {
300 actions.push(InferredAction::CreateEdge {
301 source: new_memory.id,
302 target: existing.id,
303 edge_type: EdgeType::Related,
304 weight: sim,
305 });
306 }
307 }
308
309 if new_memory.memory_type == MemoryType::Correction
312 && let Some(original) = existing_memories
313 .iter()
314 .filter(|m| m.id != new_memory.id && !value_update_handled.contains(&m.id))
315 .max_by(|a, b| {
316 cosine_similarity(&new_memory.embedding, &a.embedding)
317 .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
318 .unwrap_or(std::cmp::Ordering::Equal)
319 })
320 {
321 let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
322 if sim > self.config.correction_threshold {
323 actions.push(InferredAction::CreateEdge {
324 source: new_memory.id,
325 target: original.id,
326 edge_type: EdgeType::Supersedes,
327 weight: 1.0,
328 });
329 actions.push(InferredAction::UpdateConfidence {
330 memory: original.id,
331 new_confidence: (original.confidence * self.config.confidence_decay_factor)
332 .max(self.config.confidence_floor),
333 });
334 }
335 }
336
337 actions
338 }
339
340 pub async fn infer_on_write_with_llm<J: LlmJudge>(
345 &self,
346 new_memory: &MemoryNode,
347 existing_memories: &[MemoryNode],
348 existing_edges: &[(MemoryId, MemoryId, EdgeType)],
349 llm: &CognitiveLlmService<J>,
350 ) -> Vec<InferredAction> {
351 let _ = existing_edges;
352 let mut actions = Vec::new();
353
354 let new_summary = memory_to_summary(new_memory);
355
356 for existing in existing_memories {
357 if existing.id == new_memory.id {
358 continue;
359 }
360
361 let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
362
363 if sim > 0.5 && existing.agent_id == new_memory.agent_id {
366 let old_summary = memory_to_summary(existing);
367
368 if let Ok(verdict) = llm.judge_invalidation(&old_summary, &new_summary).await {
370 match verdict {
371 InvalidationVerdict::Invalidate { reason: _ } => {
372 actions.push(InferredAction::InvalidateMemory {
375 memory: existing.id,
376 superseded_by: new_memory.id,
377 valid_until: new_memory.created_at,
378 });
379 actions.push(InferredAction::UpdateConfidence {
380 memory: existing.id,
381 new_confidence: (existing.confidence
382 * self.config.confidence_decay_factor)
383 .max(self.config.confidence_floor),
384 });
385 continue;
387 }
388 InvalidationVerdict::Update {
389 merged_content,
390 reason,
391 } => {
392 actions.push(InferredAction::UpdateContent {
393 memory: existing.id,
394 new_content: merged_content,
395 reason,
396 });
397 continue;
398 }
399 InvalidationVerdict::Keep { .. } => {
400 }
402 }
403 }
404
405 if sim > 0.7
407 && existing.content != new_memory.content
408 && let Ok(verdict) = llm.detect_contradiction(&old_summary, &new_summary).await
409 {
410 match verdict {
411 ContradictionVerdict::Contradicts { reason } => {
412 actions.push(InferredAction::FlagContradiction {
413 existing: existing.id,
414 new: new_memory.id,
415 reason,
416 });
417 }
418 ContradictionVerdict::Supersedes { winner, reason: _ } => {
419 let winner_is_new = winner == new_memory.id.to_string();
420 let (obsolete, superseder) = if winner_is_new {
421 (existing.id, new_memory.id)
422 } else {
423 (new_memory.id, existing.id)
424 };
425 actions.push(InferredAction::InvalidateMemory {
426 memory: obsolete,
427 superseded_by: superseder,
428 valid_until: new_memory.created_at,
429 });
430 }
431 ContradictionVerdict::Compatible { .. } => {}
432 }
433 }
434 }
435
436 if sim > self.config.related_min && sim <= self.config.related_max {
438 actions.push(InferredAction::CreateEdge {
439 source: new_memory.id,
440 target: existing.id,
441 edge_type: EdgeType::Related,
442 weight: sim,
443 });
444 }
445 }
446
447 if new_memory.memory_type == MemoryType::Correction
449 && let Some(original) = existing_memories
450 .iter()
451 .filter(|m| m.id != new_memory.id)
452 .max_by(|a, b| {
453 cosine_similarity(&new_memory.embedding, &a.embedding)
454 .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
455 .unwrap_or(std::cmp::Ordering::Equal)
456 })
457 {
458 let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
459 if sim > self.config.correction_threshold {
460 actions.push(InferredAction::InvalidateMemory {
461 memory: original.id,
462 superseded_by: new_memory.id,
463 valid_until: new_memory.created_at,
464 });
465 actions.push(InferredAction::UpdateConfidence {
466 memory: original.id,
467 new_confidence: (original.confidence * self.config.confidence_decay_factor)
468 .max(self.config.confidence_floor),
469 });
470 }
471 }
472
473 actions
474 }
475}
476
477impl Default for WriteInferenceEngine {
478 fn default() -> Self {
479 Self::new()
480 }
481}
482
483fn memory_to_summary(m: &MemoryNode) -> MemorySummary {
484 MemorySummary {
485 id: m.id,
486 content: m.content.clone(),
487 memory_type: m.memory_type,
488 confidence: m.confidence,
489 created_at: m.created_at,
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use mentedb_core::memory::MemoryType;
497 use mentedb_core::types::AgentId;
498
499 use crate::llm::MockLlmJudge;
500
501 fn make_memory(content: &str, embedding: Vec<f32>, mem_type: MemoryType) -> MemoryNode {
502 let mut m = MemoryNode::new(AgentId::new(), mem_type, content.to_string(), embedding);
503 m.created_at = 1000;
504 m
505 }
506
507 #[test]
508 fn test_heuristic_does_not_flag_contradiction_from_similarity() {
509 let agent = AgentId::new();
516 let mut existing =
517 make_memory("uses PostgreSQL", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
518 existing.agent_id = agent;
519
520 let mut new_mem = make_memory("uses MySQL", vec![0.99, 0.01, 0.0], MemoryType::Semantic);
521 new_mem.agent_id = agent;
522 new_mem.created_at = 2000;
523
524 let engine = WriteInferenceEngine::new();
525 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
526 assert!(
527 !actions
528 .iter()
529 .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
530 "Heuristic must not flag contradictions from bare similarity, got: {:?}",
531 actions
532 );
533 }
534
535 #[test]
536 fn value_update_supersedes_the_old_fact() {
537 let agent = AgentId::new();
541 let mut existing = make_memory(
542 "The user's favorite coffee order is a cortado",
543 vec![1.0, 0.0, 0.0],
544 MemoryType::Semantic,
545 );
546 existing.agent_id = agent;
547 let mut new_mem = make_memory(
548 "The user's favorite coffee order is a flat white",
549 vec![0.98, 0.02, 0.0],
550 MemoryType::Semantic,
551 );
552 new_mem.agent_id = agent;
553 new_mem.created_at = 2000;
554
555 let engine = WriteInferenceEngine::new();
556 let actions = engine.infer_on_write(&new_mem, &[existing.clone()], &[]);
557 assert!(
558 actions.iter().any(|a| matches!(
559 a,
560 InferredAction::CreateEdge { source, target, edge_type: EdgeType::Supersedes, .. }
561 if *source == new_mem.id && *target == existing.id
562 )),
563 "value update must create a Supersedes edge, got: {actions:?}"
564 );
565 assert!(
566 actions
567 .iter()
568 .any(|a| matches!(a, InferredAction::UpdateConfidence { memory, .. } if *memory == existing.id)),
569 "superseded fact's confidence must decay"
570 );
571 }
572
573 #[test]
574 fn value_update_fires_at_realistic_embedder_cosine() {
575 let agent = AgentId::new();
583 let mut existing = make_memory(
584 "The user's favorite coffee order is a cortado",
585 vec![1.0, 0.0, 0.0],
586 MemoryType::Semantic,
587 );
588 existing.agent_id = agent;
589 let mut new_mem = make_memory(
591 "The user's favorite coffee order is a flat white",
592 vec![0.6, 0.8, 0.0],
593 MemoryType::Semantic,
594 );
595 new_mem.agent_id = agent;
596 new_mem.created_at = 2000;
597
598 let actions =
599 WriteInferenceEngine::new().infer_on_write(&new_mem, &[existing.clone()], &[]);
600 assert!(
601 actions.iter().any(|a| matches!(
602 a,
603 InferredAction::CreateEdge { source, target, edge_type: EdgeType::Supersedes, .. }
604 if *source == new_mem.id && *target == existing.id
605 )),
606 "a frame-matched correction at realistic cosine 0.6 must supersede, got: {actions:?}"
607 );
608 }
609
610 #[test]
611 fn value_update_below_cosine_floor_does_not_supersede() {
612 let agent = AgentId::new();
617 let mut existing = make_memory(
618 "The user's favorite coffee order is a cortado",
619 vec![1.0, 0.0, 0.0],
620 MemoryType::Semantic,
621 );
622 existing.agent_id = agent;
623 let mut new_mem = make_memory(
625 "The user's favorite coffee order is a flat white",
626 vec![0.4, 0.917, 0.0],
627 MemoryType::Semantic,
628 );
629 new_mem.agent_id = agent;
630 new_mem.created_at = 2000;
631
632 let actions = WriteInferenceEngine::new().infer_on_write(&new_mem, &[existing], &[]);
633 assert!(
634 !actions.iter().any(|a| matches!(
635 a,
636 InferredAction::CreateEdge {
637 edge_type: EdgeType::Supersedes,
638 ..
639 }
640 )),
641 "below the cosine floor a frame match must not supersede, got: {actions:?}"
642 );
643 }
644
645 #[test]
646 fn correction_supersedes_regardless_of_store_order() {
647 let agent = AgentId::new();
652 let mut correction = make_memory(
653 "The user's favorite coffee is a flat white",
654 vec![1.0, 0.0, 0.0],
655 MemoryType::Correction,
656 );
657 correction.agent_id = agent;
658 correction.created_at = 5000;
659 let mut original = make_memory(
660 "The user's favorite coffee is a cortado",
661 vec![0.98, 0.02, 0.0],
662 MemoryType::Semantic,
663 );
664 original.agent_id = agent;
665 original.created_at = 5000; let actions =
669 WriteInferenceEngine::new().infer_on_write(&original, &[correction.clone()], &[]);
670 assert!(
671 actions.iter().any(|a| matches!(
672 a,
673 InferredAction::CreateEdge { source, target, edge_type: EdgeType::Supersedes, .. }
674 if *source == correction.id && *target == original.id
675 )),
676 "the correction must supersede the later-arriving original, got: {actions:?}"
677 );
678 }
679
680 #[test]
681 fn correction_supersedes_original_at_equal_timestamps() {
682 let agent = AgentId::new();
685 let mut original = make_memory(
686 "The user's favorite coffee is a cortado",
687 vec![1.0, 0.0, 0.0],
688 MemoryType::Semantic,
689 );
690 original.agent_id = agent;
691 original.created_at = 5000;
692 let mut correction = make_memory(
693 "The user's favorite coffee is a flat white",
694 vec![0.98, 0.02, 0.0],
695 MemoryType::Correction,
696 );
697 correction.agent_id = agent;
698 correction.created_at = 5000;
699
700 let actions =
701 WriteInferenceEngine::new().infer_on_write(&correction, &[original.clone()], &[]);
702 assert!(
703 actions.iter().any(|a| matches!(
704 a,
705 InferredAction::CreateEdge { source, target, edge_type: EdgeType::Supersedes, .. }
706 if *source == correction.id && *target == original.id
707 )),
708 "the correction must supersede the original at equal timestamps, got: {actions:?}"
709 );
710 }
711
712 #[test]
713 fn multivalued_facts_at_equal_timestamps_are_both_kept() {
714 let agent = AgentId::new();
719 let mut a = make_memory(
720 "The user knows Rust",
721 vec![1.0, 0.0, 0.0],
722 MemoryType::Semantic,
723 );
724 a.agent_id = agent;
725 a.created_at = 5000;
726 let mut b = make_memory(
727 "The user knows Python",
728 vec![0.97, 0.03, 0.0],
729 MemoryType::Semantic,
730 );
731 b.agent_id = agent;
732 b.created_at = 5000;
733
734 let actions = WriteInferenceEngine::new().infer_on_write(&b, &[a], &[]);
735 assert!(
736 !actions.iter().any(|x| matches!(
737 x,
738 InferredAction::CreateEdge {
739 edge_type: EdgeType::Supersedes,
740 ..
741 }
742 )),
743 "coexisting facts at equal timestamps must not supersede, got: {actions:?}"
744 );
745 }
746
747 #[test]
748 fn subject_change_is_not_a_value_update() {
749 let agent = AgentId::new();
752 let mut existing = make_memory(
753 "My dog is allergic to chicken",
754 vec![1.0, 0.0, 0.0],
755 MemoryType::Semantic,
756 );
757 existing.agent_id = agent;
758 let mut new_mem = make_memory(
759 "My cat is allergic to chicken",
760 vec![0.99, 0.01, 0.0],
761 MemoryType::Semantic,
762 );
763 new_mem.agent_id = agent;
764 new_mem.created_at = 2000;
765
766 let engine = WriteInferenceEngine::new();
767 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
768 assert!(
769 !actions.iter().any(|a| matches!(
770 a,
771 InferredAction::CreateEdge {
772 edge_type: EdgeType::Supersedes,
773 ..
774 }
775 )),
776 "a subject change must not supersede, got: {actions:?}"
777 );
778 }
779
780 #[test]
781 fn value_update_respects_owner_and_config() {
782 let mut existing = make_memory(
784 "The user's favorite coffee order is a cortado",
785 vec![1.0, 0.0, 0.0],
786 MemoryType::Semantic,
787 );
788 existing.agent_id = AgentId::new();
789 let mut new_mem = make_memory(
790 "The user's favorite coffee order is a flat white",
791 vec![0.98, 0.02, 0.0],
792 MemoryType::Semantic,
793 );
794 new_mem.agent_id = AgentId::new(); new_mem.created_at = 2000;
796
797 let engine = WriteInferenceEngine::new();
798 let actions = engine.infer_on_write(&new_mem, &[existing.clone()], &[]);
799 assert!(
800 !actions.iter().any(|a| matches!(
801 a,
802 InferredAction::CreateEdge {
803 edge_type: EdgeType::Supersedes,
804 ..
805 }
806 )),
807 "cross-owner memories must never supersede each other"
808 );
809
810 let agent = AgentId::new();
812 existing.agent_id = agent;
813 new_mem.agent_id = agent;
814 let engine_off = WriteInferenceEngine::with_config(WriteInferenceConfig {
815 value_update_enabled: false,
816 ..WriteInferenceConfig::default()
817 });
818 let actions = engine_off.infer_on_write(&new_mem, &[existing], &[]);
819 assert!(
820 !actions.iter().any(|a| matches!(
821 a,
822 InferredAction::CreateEdge {
823 edge_type: EdgeType::Supersedes,
824 ..
825 }
826 )),
827 "disabled rule must not supersede"
828 );
829 }
830
831 #[test]
832 fn is_value_update_frame_analysis() {
833 assert!(is_value_update(
835 "my phone number is 4321",
836 "my phone number is 1234",
837 0.6,
838 4
839 ));
840 assert!(!is_value_update(
842 "my cat is allergic to chicken",
843 "my dog is allergic to chicken",
844 0.6,
845 4
846 ));
847 assert!(!is_value_update("same fact", "same fact", 0.6, 4));
849 assert!(!is_value_update(
851 "the deploy runs every friday and sarah reviews it after standup",
852 "the deploy runs every friday unless the release train is frozen for the quarter end audit",
853 0.6,
854 4
855 ));
856 }
857
858 #[test]
859 fn test_byte_identical_duplicate_is_deduplicated() {
860 let agent = AgentId::new();
863 let mut existing = make_memory(
864 "Ran command: ls -la",
865 vec![1.0, 0.0, 0.0],
866 MemoryType::Episodic,
867 );
868 existing.agent_id = agent;
869
870 let mut new_mem = make_memory(
871 "Ran command: ls -la",
872 vec![1.0, 0.0, 0.0],
873 MemoryType::Episodic,
874 );
875 new_mem.agent_id = agent;
876 new_mem.created_at = 2000;
877
878 let engine = WriteInferenceEngine::new();
879 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
880 assert!(
881 actions
882 .iter()
883 .any(|a| matches!(a, InferredAction::DeduplicateExact { .. })),
884 "Expected identical duplicate to be deduplicated, got: {:?}",
885 actions
886 );
887 }
888
889 #[test]
890 fn test_moderate_similarity_creates_edge() {
891 let existing = make_memory("topic A", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
892 let new_mem = make_memory("topic B", vec![0.7, 0.714, 0.0], MemoryType::Semantic);
894
895 let engine = WriteInferenceEngine::new();
896 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
897 assert!(
898 actions.iter().any(|a| matches!(
899 a,
900 InferredAction::CreateEdge {
901 edge_type: EdgeType::Related,
902 ..
903 }
904 )),
905 "Expected CreateEdge Related, got: {:?}",
906 actions
907 );
908 }
909
910 #[tokio::test]
911 async fn test_llm_invalidation_emits_temporal_actions() {
912 let agent = AgentId::new();
913 let mut existing = make_memory(
914 "Alice works at Acme",
915 vec![0.8, 0.6, 0.0],
916 MemoryType::Semantic,
917 );
918 existing.agent_id = agent;
919
920 let mut new_mem = make_memory(
921 "Alice joined Google last week",
922 vec![0.75, 0.65, 0.1],
923 MemoryType::Semantic,
924 );
925 new_mem.agent_id = agent;
926 new_mem.created_at = 2000;
927
928 let judge =
929 MockLlmJudge::new(r#"{"verdict": "invalidate", "reason": "Alice changed jobs"}"#);
930 let llm = CognitiveLlmService::new(judge);
931 let engine = WriteInferenceEngine::new();
932 let actions = engine
933 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
934 .await;
935
936 assert!(
937 actions
938 .iter()
939 .any(|a| matches!(a, InferredAction::InvalidateMemory { .. })),
940 "Expected InvalidateMemory from LLM verdict, got: {:?}",
941 actions
942 );
943 let invalidation_count = actions
947 .iter()
948 .filter(|a| {
949 matches!(
950 a,
951 InferredAction::InvalidateMemory { .. } | InferredAction::MarkObsolete { .. }
952 )
953 })
954 .count();
955 assert_eq!(
956 invalidation_count, 1,
957 "Expected a single invalidation action, got: {:?}",
958 actions
959 );
960 }
961
962 #[tokio::test]
963 async fn test_llm_update_emits_update_content() {
964 let agent = AgentId::new();
965 let mut existing = make_memory(
966 "Project uses React",
967 vec![0.8, 0.6, 0.0],
968 MemoryType::Semantic,
969 );
970 existing.agent_id = agent;
971
972 let mut new_mem = make_memory(
973 "Project migrated from React to Vue",
974 vec![0.75, 0.65, 0.1],
975 MemoryType::Semantic,
976 );
977 new_mem.agent_id = agent;
978 new_mem.created_at = 2000;
979
980 let judge = MockLlmJudge::new(
981 r#"{"verdict": "update", "merged_content": "Project migrated from React to Vue in Q2", "reason": "adds temporal context"}"#,
982 );
983 let llm = CognitiveLlmService::new(judge);
984 let engine = WriteInferenceEngine::new();
985 let actions = engine
986 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
987 .await;
988
989 assert!(
990 actions
991 .iter()
992 .any(|a| matches!(a, InferredAction::UpdateContent { .. })),
993 "Expected UpdateContent from LLM update verdict, got: {:?}",
994 actions
995 );
996 }
997
998 #[tokio::test]
999 async fn test_llm_keep_falls_through_to_contradiction_check() {
1000 let agent = AgentId::new();
1001 let mut existing = make_memory("Prefers tabs", vec![0.9, 0.44, 0.0], MemoryType::Semantic);
1003 existing.agent_id = agent;
1004
1005 let mut new_mem = make_memory(
1006 "Prefers spaces",
1007 vec![0.88, 0.47, 0.0],
1008 MemoryType::Semantic,
1009 );
1010 new_mem.agent_id = agent;
1011 new_mem.created_at = 2000;
1012
1013 let judge = MockLlmJudge::new(
1014 r#"{"verdict": "compatible", "reason": "different formatting preferences"}"#,
1015 );
1016 let llm = CognitiveLlmService::new(judge);
1017 let engine = WriteInferenceEngine::new();
1018 let actions = engine
1019 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
1020 .await;
1021
1022 assert!(
1025 !actions
1026 .iter()
1027 .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
1028 "Should not flag contradiction when LLM says compatible",
1029 );
1030 }
1031}