1use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use crate::auth::TenantScope;
8use crate::error::Error;
9
10use super::{Confidentiality, Memory, MemoryEntry, MemoryQuery};
11
12pub struct NamespacedMemory {
22 inner: Arc<dyn Memory>,
23 agent_name: String,
24 max_confidentiality: Option<Confidentiality>,
25 default_store_confidentiality: Confidentiality,
26}
27
28impl NamespacedMemory {
29 pub fn new(inner: Arc<dyn Memory>, agent_name: impl Into<String>) -> Self {
31 Self {
32 inner,
33 agent_name: agent_name.into(),
34 max_confidentiality: None,
35 default_store_confidentiality: Confidentiality::Public,
36 }
37 }
38
39 pub fn with_max_confidentiality(mut self, cap: Option<Confidentiality>) -> Self {
44 self.max_confidentiality = cap;
45 self
46 }
47
48 pub fn with_default_store_confidentiality(mut self, level: Confidentiality) -> Self {
56 self.default_store_confidentiality = level;
57 self
58 }
59
60 fn prefix_id(&self, id: &str) -> String {
61 format!("{}:{}", self.agent_name, id)
62 }
63}
64
65impl Memory for NamespacedMemory {
66 fn store(
67 &self,
68 scope: &TenantScope,
69 mut entry: MemoryEntry,
70 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
71 entry.id = self.prefix_id(&entry.id);
72 entry.agent = self.agent_name.clone();
73 if entry.confidentiality < self.default_store_confidentiality {
78 entry.confidentiality = self.default_store_confidentiality;
79 }
80 let scope = scope.clone();
82 Box::pin(async move { self.inner.store(&scope, entry).await })
83 }
84
85 fn recall(
86 &self,
87 scope: &TenantScope,
88 query: MemoryQuery,
89 ) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryEntry>, Error>> + Send + '_>> {
90 let mut query = MemoryQuery {
93 agent: Some(self.agent_name.clone()),
94 ..query
95 };
96 if let Some(cap) = self.max_confidentiality {
98 query.max_confidentiality = Some(match query.max_confidentiality {
99 Some(existing) if existing < cap => existing,
100 _ => cap,
101 });
102 }
103 let prefix = format!("{}:", self.agent_name);
104 let scope = scope.clone();
105 Box::pin(async move {
106 let mut entries = self.inner.recall(&scope, query).await?;
107 for entry in &mut entries {
110 if let Some(stripped) = entry.id.strip_prefix(&prefix) {
111 entry.id = stripped.to_string();
112 }
113 }
114 Ok(entries)
115 })
116 }
117
118 fn update(
119 &self,
120 scope: &TenantScope,
121 id: &str,
122 content: String,
123 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
124 let prefixed = self.prefix_id(id);
125 let scope = scope.clone();
126 Box::pin(async move { self.inner.update(&scope, &prefixed, content).await })
127 }
128
129 fn forget(
130 &self,
131 scope: &TenantScope,
132 id: &str,
133 ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + '_>> {
134 let prefixed = self.prefix_id(id);
135 let scope = scope.clone();
136 Box::pin(async move { self.inner.forget(&scope, &prefixed).await })
137 }
138
139 fn add_link(
140 &self,
141 scope: &TenantScope,
142 id: &str,
143 related_id: &str,
144 ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>> {
145 let prefixed_id = self.prefix_id(id);
146 let prefixed_related = self.prefix_id(related_id);
147 let scope = scope.clone();
148 Box::pin(async move {
149 self.inner
150 .add_link(&scope, &prefixed_id, &prefixed_related)
151 .await
152 })
153 }
154
155 fn prune(
156 &self,
157 scope: &TenantScope,
158 min_strength: f64,
159 min_age: chrono::Duration,
160 _agent_prefix: Option<&str>,
161 ) -> Pin<Box<dyn Future<Output = Result<usize, Error>> + Send + '_>> {
162 let scope = scope.clone();
165 let agent_name = self.agent_name.clone();
166 Box::pin(async move {
167 self.inner
168 .prune(&scope, min_strength, min_age, Some(&agent_name))
169 .await
170 })
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::memory::in_memory::InMemoryStore;
178 use chrono::Utc;
179
180 use super::super::{Confidentiality, MemoryType};
181
182 fn test_scope() -> TenantScope {
183 TenantScope::default()
184 }
185
186 fn make_entry(id: &str, content: &str) -> MemoryEntry {
187 MemoryEntry {
188 id: id.into(),
189 agent: String::new(),
190 content: content.into(),
191 category: "fact".into(),
192 tags: vec![],
193 created_at: Utc::now(),
194 last_accessed: Utc::now(),
195 access_count: 0,
196 importance: 5,
197 memory_type: MemoryType::default(),
198 keywords: vec![],
199 summary: None,
200 strength: 1.0,
201 related_ids: vec![],
202 source_ids: vec![],
203 embedding: None,
204 confidentiality: Confidentiality::default(),
205 author_user_id: None,
206 author_tenant_id: None,
207 }
208 }
209
210 #[tokio::test]
211 async fn store_prefixes_id_and_agent() {
212 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
213 let ns = NamespacedMemory::new(inner.clone(), "researcher");
214
215 ns.store(&test_scope(), make_entry("m1", "test data"))
216 .await
217 .unwrap();
218
219 let all = inner
221 .recall(
222 &test_scope(),
223 MemoryQuery {
224 limit: 10,
225 ..Default::default()
226 },
227 )
228 .await
229 .unwrap();
230 assert_eq!(all.len(), 1);
231 assert_eq!(all[0].id, "researcher:m1");
232 assert_eq!(all[0].agent, "researcher");
233
234 let ns_results = ns
236 .recall(
237 &test_scope(),
238 MemoryQuery {
239 limit: 10,
240 ..Default::default()
241 },
242 )
243 .await
244 .unwrap();
245 assert_eq!(ns_results[0].id, "m1"); }
247
248 #[tokio::test]
249 async fn recall_filters_by_agent() {
250 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
251 let ns_a = NamespacedMemory::new(inner.clone(), "agent_a");
252 let ns_b = NamespacedMemory::new(inner.clone(), "agent_b");
253
254 ns_a.store(&test_scope(), make_entry("m1", "data from A"))
255 .await
256 .unwrap();
257 ns_b.store(&test_scope(), make_entry("m2", "data from B"))
258 .await
259 .unwrap();
260
261 let results = ns_a
263 .recall(
264 &test_scope(),
265 MemoryQuery {
266 limit: 10,
267 ..Default::default()
268 },
269 )
270 .await
271 .unwrap();
272 assert_eq!(results.len(), 1);
273 assert_eq!(results[0].content, "data from A");
274
275 let results = ns_b
277 .recall(
278 &test_scope(),
279 MemoryQuery {
280 limit: 10,
281 ..Default::default()
282 },
283 )
284 .await
285 .unwrap();
286 assert_eq!(results.len(), 1);
287 assert_eq!(results[0].content, "data from B");
288 }
289
290 #[tokio::test]
291 async fn namespace_forces_own_agent_even_with_explicit_override() {
292 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
296 let ns_a = NamespacedMemory::new(inner.clone(), "agent_a");
297 let ns_b = NamespacedMemory::new(inner.clone(), "agent_b");
298
299 ns_a.store(&test_scope(), make_entry("m1", "from A"))
300 .await
301 .unwrap();
302 ns_b.store(&test_scope(), make_entry("m2", "from B"))
303 .await
304 .unwrap();
305
306 let results = ns_a
308 .recall(
309 &test_scope(),
310 MemoryQuery {
311 agent: Some(String::new()),
312 limit: 10,
313 ..Default::default()
314 },
315 )
316 .await
317 .unwrap();
318 assert_eq!(results.len(), 1);
320 assert_eq!(results[0].content, "from A");
321
322 let all = inner
324 .recall(
325 &test_scope(),
326 MemoryQuery {
327 limit: 10,
328 ..Default::default()
329 },
330 )
331 .await
332 .unwrap();
333 assert_eq!(all.len(), 2);
334 }
335
336 #[tokio::test]
337 async fn recall_then_update_roundtrip() {
338 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
341 let ns = NamespacedMemory::new(inner.clone(), "agent_a");
342
343 ns.store(&test_scope(), make_entry("m1", "original"))
344 .await
345 .unwrap();
346
347 let results = ns
349 .recall(
350 &test_scope(),
351 MemoryQuery {
352 limit: 10,
353 ..Default::default()
354 },
355 )
356 .await
357 .unwrap();
358 assert_eq!(results[0].id, "m1");
359
360 ns.update(
362 &test_scope(),
363 &results[0].id,
364 "updated via recall ID".into(),
365 )
366 .await
367 .unwrap();
368
369 let results = ns
371 .recall(
372 &test_scope(),
373 MemoryQuery {
374 limit: 10,
375 ..Default::default()
376 },
377 )
378 .await
379 .unwrap();
380 assert_eq!(results[0].content, "updated via recall ID");
381 }
382
383 #[tokio::test]
384 async fn update_uses_prefixed_id() {
385 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
386 let ns = NamespacedMemory::new(inner.clone(), "agent_a");
387
388 ns.store(&test_scope(), make_entry("m1", "original"))
389 .await
390 .unwrap();
391 ns.update(&test_scope(), "m1", "updated".into())
392 .await
393 .unwrap();
394
395 let results = ns
396 .recall(
397 &test_scope(),
398 MemoryQuery {
399 limit: 10,
400 ..Default::default()
401 },
402 )
403 .await
404 .unwrap();
405 assert_eq!(results[0].content, "updated");
406 }
407
408 #[tokio::test]
409 async fn forget_uses_prefixed_id() {
410 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
411 let ns = NamespacedMemory::new(inner.clone(), "agent_a");
412
413 ns.store(&test_scope(), make_entry("m1", "to delete"))
414 .await
415 .unwrap();
416 assert!(ns.forget(&test_scope(), "m1").await.unwrap());
417
418 let results = ns
419 .recall(
420 &test_scope(),
421 MemoryQuery {
422 limit: 10,
423 ..Default::default()
424 },
425 )
426 .await
427 .unwrap();
428 assert!(results.is_empty());
429 }
430
431 #[tokio::test]
432 async fn add_link_delegates_with_prefix() {
433 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
434 let ns = NamespacedMemory::new(inner.clone(), "agent_a");
435
436 ns.store(&test_scope(), make_entry("m1", "first"))
437 .await
438 .unwrap();
439 ns.store(&test_scope(), make_entry("m2", "second"))
440 .await
441 .unwrap();
442
443 ns.add_link(&test_scope(), "m1", "m2").await.unwrap();
445
446 let all = inner
448 .recall(
449 &test_scope(),
450 MemoryQuery {
451 limit: 10,
452 ..Default::default()
453 },
454 )
455 .await
456 .unwrap();
457 let m1 = all.iter().find(|e| e.id == "agent_a:m1").unwrap();
458 let m2 = all.iter().find(|e| e.id == "agent_a:m2").unwrap();
459 assert!(m1.related_ids.contains(&"agent_a:m2".to_string()));
460 assert!(m2.related_ids.contains(&"agent_a:m1".to_string()));
461 }
462
463 #[tokio::test]
464 async fn max_confidentiality_caps_recall() {
465 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
466 let ns = NamespacedMemory::new(inner.clone(), "agent_a")
467 .with_max_confidentiality(Some(Confidentiality::Public));
468
469 let mut public_entry = make_entry("m1", "public data");
471 public_entry.confidentiality = Confidentiality::Public;
472 ns.store(&test_scope(), public_entry).await.unwrap();
473
474 let mut confidential_entry = make_entry("m2", "confidential data");
475 confidential_entry.confidentiality = Confidentiality::Confidential;
476 confidential_entry.id = "agent_a:m2".into();
478 confidential_entry.agent = "agent_a".into();
479 inner
480 .store(&test_scope(), confidential_entry)
481 .await
482 .unwrap();
483
484 let results = ns
486 .recall(
487 &test_scope(),
488 MemoryQuery {
489 limit: 10,
490 ..Default::default()
491 },
492 )
493 .await
494 .unwrap();
495 assert_eq!(results.len(), 1);
496 assert_eq!(results[0].content, "public data");
497 }
498
499 #[tokio::test]
500 async fn no_confidentiality_cap_returns_all() {
501 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
502 let ns = NamespacedMemory::new(inner.clone(), "agent_a");
503
504 let mut public_entry = make_entry("m1", "public data");
505 public_entry.confidentiality = Confidentiality::Public;
506 ns.store(&test_scope(), public_entry).await.unwrap();
507
508 let mut confidential_entry = make_entry("m2", "confidential data");
509 confidential_entry.confidentiality = Confidentiality::Confidential;
510 ns.store(&test_scope(), confidential_entry).await.unwrap();
511
512 let results = ns
513 .recall(
514 &test_scope(),
515 MemoryQuery {
516 limit: 10,
517 ..Default::default()
518 },
519 )
520 .await
521 .unwrap();
522 assert_eq!(results.len(), 2);
523 }
524
525 #[tokio::test]
526 async fn confidentiality_cap_uses_stricter_of_two() {
527 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
528 let ns = NamespacedMemory::new(inner.clone(), "agent_a")
530 .with_max_confidentiality(Some(Confidentiality::Internal));
531
532 let mut public_entry = make_entry("m1", "public data");
533 public_entry.confidentiality = Confidentiality::Public;
534 ns.store(&test_scope(), public_entry).await.unwrap();
535
536 let mut internal_entry = make_entry("m2", "internal data");
537 internal_entry.confidentiality = Confidentiality::Internal;
538 ns.store(&test_scope(), internal_entry).await.unwrap();
539
540 let mut confidential_entry = make_entry("m3", "confidential data");
541 confidential_entry.confidentiality = Confidentiality::Confidential;
542 confidential_entry.id = "agent_a:m3".into();
544 confidential_entry.agent = "agent_a".into();
545 inner
546 .store(&test_scope(), confidential_entry)
547 .await
548 .unwrap();
549
550 let results = ns
552 .recall(
553 &test_scope(),
554 MemoryQuery {
555 limit: 10,
556 max_confidentiality: Some(Confidentiality::Confidential),
557 ..Default::default()
558 },
559 )
560 .await
561 .unwrap();
562 assert_eq!(results.len(), 2); let results = ns
566 .recall(
567 &test_scope(),
568 MemoryQuery {
569 limit: 10,
570 max_confidentiality: Some(Confidentiality::Public),
571 ..Default::default()
572 },
573 )
574 .await
575 .unwrap();
576 assert_eq!(results.len(), 1); }
578
579 #[tokio::test]
580 async fn default_store_confidentiality_upgrades_public() {
581 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
582 let ns = NamespacedMemory::new(inner.clone(), "tg_agent")
583 .with_default_store_confidentiality(Confidentiality::Confidential);
584
585 let entry = make_entry("m1", "private chat data");
587 ns.store(&test_scope(), entry).await.unwrap();
588
589 let all = inner
591 .recall(
592 &test_scope(),
593 MemoryQuery {
594 limit: 10,
595 ..Default::default()
596 },
597 )
598 .await
599 .unwrap();
600 assert_eq!(all.len(), 1);
601 assert_eq!(all[0].confidentiality, Confidentiality::Confidential);
602 }
603
604 #[tokio::test]
605 async fn default_store_confidentiality_enforces_minimum_floor() {
606 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
607 let ns = NamespacedMemory::new(inner.clone(), "tg_agent")
608 .with_default_store_confidentiality(Confidentiality::Confidential);
609
610 let mut entry = make_entry("m1", "internal data");
612 entry.confidentiality = Confidentiality::Internal;
613 ns.store(&test_scope(), entry).await.unwrap();
614
615 let all = inner
616 .recall(
617 &test_scope(),
618 MemoryQuery {
619 limit: 10,
620 ..Default::default()
621 },
622 )
623 .await
624 .unwrap();
625 assert_eq!(all.len(), 1);
626 assert_eq!(all[0].confidentiality, Confidentiality::Confidential);
627 }
628
629 #[tokio::test]
630 async fn default_store_confidentiality_preserves_higher_level() {
631 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
632 let ns = NamespacedMemory::new(inner.clone(), "tg_agent")
633 .with_default_store_confidentiality(Confidentiality::Confidential);
634
635 let mut entry = make_entry("m1", "secret data");
637 entry.confidentiality = Confidentiality::Restricted;
638 ns.store(&test_scope(), entry).await.unwrap();
639
640 let all = inner
641 .recall(
642 &test_scope(),
643 MemoryQuery {
644 limit: 10,
645 ..Default::default()
646 },
647 )
648 .await
649 .unwrap();
650 assert_eq!(all.len(), 1);
651 assert_eq!(all[0].confidentiality, Confidentiality::Restricted);
652 }
653
654 #[tokio::test]
655 async fn prune_delegates_to_inner() {
656 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
657 let ns = NamespacedMemory::new(inner.clone(), "agent_a");
658
659 let mut entry = make_entry("m1", "weak memory");
660 entry.strength = 0.01;
661 entry.created_at = Utc::now() - chrono::Duration::hours(48);
662 entry.last_accessed = Utc::now() - chrono::Duration::hours(48);
663 ns.store(&test_scope(), entry).await.unwrap();
664
665 let pruned = ns
666 .prune(&test_scope(), 0.1, chrono::Duration::hours(1), None)
667 .await
668 .unwrap();
669 assert_eq!(pruned, 1);
670
671 let results = ns
673 .recall(
674 &test_scope(),
675 MemoryQuery {
676 limit: 10,
677 ..Default::default()
678 },
679 )
680 .await
681 .unwrap();
682 assert!(results.is_empty());
683 }
684
685 #[tokio::test]
687 async fn prune_scoped_to_own_namespace() {
688 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
689 let ns_a = NamespacedMemory::new(inner.clone(), "agent_a");
690 let ns_b = NamespacedMemory::new(inner.clone(), "agent_b");
691
692 let mut weak_a = make_entry("m1", "weak from A");
693 weak_a.strength = 0.01;
694 weak_a.created_at = Utc::now() - chrono::Duration::hours(48);
695 weak_a.last_accessed = Utc::now() - chrono::Duration::hours(48);
696 ns_a.store(&test_scope(), weak_a).await.unwrap();
697
698 let mut weak_b = make_entry("m1", "weak from B");
699 weak_b.strength = 0.01;
700 weak_b.created_at = Utc::now() - chrono::Duration::hours(48);
701 weak_b.last_accessed = Utc::now() - chrono::Duration::hours(48);
702 ns_b.store(&test_scope(), weak_b).await.unwrap();
703
704 let pruned = ns_a
706 .prune(&test_scope(), 0.1, chrono::Duration::hours(1), None)
707 .await
708 .unwrap();
709 assert_eq!(pruned, 1, "should only prune agent_a's entry");
710
711 let a_results = ns_a
713 .recall(
714 &test_scope(),
715 MemoryQuery {
716 limit: 10,
717 ..Default::default()
718 },
719 )
720 .await
721 .unwrap();
722 assert!(a_results.is_empty());
723
724 let b_results = ns_b
726 .recall(
727 &test_scope(),
728 MemoryQuery {
729 limit: 10,
730 ..Default::default()
731 },
732 )
733 .await
734 .unwrap();
735 assert_eq!(
736 b_results.len(),
737 1,
738 "agent_b's entry must survive agent_a's prune"
739 );
740 assert_eq!(b_results[0].content, "weak from B");
741 }
742
743 #[tokio::test]
745 async fn multi_tenant_prune_isolation() {
746 let shared: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
747 let alice = NamespacedMemory::new(shared.clone(), "user:alice");
748 let bob = NamespacedMemory::new(shared.clone(), "user:bob");
749
750 let mut weak_alice = make_entry("m1", "alice weak");
752 weak_alice.strength = 0.01;
753 weak_alice.created_at = Utc::now() - chrono::Duration::hours(48);
754 weak_alice.last_accessed = Utc::now() - chrono::Duration::hours(48);
755 alice.store(&test_scope(), weak_alice).await.unwrap();
756
757 let mut strong_alice = make_entry("m2", "alice strong");
758 strong_alice.strength = 0.9;
759 alice.store(&test_scope(), strong_alice).await.unwrap();
760
761 let mut weak_bob = make_entry("m1", "bob weak");
762 weak_bob.strength = 0.01;
763 weak_bob.created_at = Utc::now() - chrono::Duration::hours(48);
764 weak_bob.last_accessed = Utc::now() - chrono::Duration::hours(48);
765 bob.store(&test_scope(), weak_bob).await.unwrap();
766
767 let mut strong_bob = make_entry("m2", "bob strong");
768 strong_bob.strength = 0.9;
769 bob.store(&test_scope(), strong_bob).await.unwrap();
770
771 let pruned = alice
773 .prune(&test_scope(), 0.1, chrono::Duration::hours(1), None)
774 .await
775 .unwrap();
776 assert_eq!(pruned, 1, "should only prune alice's weak entry");
777
778 let pruned = bob
781 .prune(&test_scope(), 0.1, chrono::Duration::hours(1), None)
782 .await
783 .unwrap();
784 assert_eq!(pruned, 1, "bob's weak entry must survive alice's prune");
785
786 let alice_results = alice
788 .recall(
789 &test_scope(),
790 MemoryQuery {
791 limit: 10,
792 ..Default::default()
793 },
794 )
795 .await
796 .unwrap();
797 assert_eq!(alice_results.len(), 1);
798 assert_eq!(alice_results[0].content, "alice strong");
799
800 let bob_results = bob
801 .recall(
802 &test_scope(),
803 MemoryQuery {
804 limit: 10,
805 ..Default::default()
806 },
807 )
808 .await
809 .unwrap();
810 assert_eq!(bob_results.len(), 1);
811 assert_eq!(bob_results[0].content, "bob strong");
812 }
813
814 #[tokio::test]
816 async fn prune_ignores_explicit_agent_prefix_override() {
817 let shared: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
818 let alice = NamespacedMemory::new(shared.clone(), "user:alice");
819 let bob = NamespacedMemory::new(shared.clone(), "user:bob");
820
821 let mut weak_bob = make_entry("m1", "bob weak");
822 weak_bob.strength = 0.01;
823 weak_bob.created_at = Utc::now() - chrono::Duration::hours(48);
824 weak_bob.last_accessed = Utc::now() - chrono::Duration::hours(48);
825 bob.store(&test_scope(), weak_bob).await.unwrap();
826
827 let pruned = alice
829 .prune(
830 &test_scope(),
831 0.1,
832 chrono::Duration::hours(1),
833 Some("user:bob"),
834 )
835 .await
836 .unwrap();
837 assert_eq!(
838 pruned, 0,
839 "alice's prune must not affect bob even with explicit prefix"
840 );
841
842 let bob_results = bob
843 .recall(
844 &test_scope(),
845 MemoryQuery {
846 limit: 10,
847 ..Default::default()
848 },
849 )
850 .await
851 .unwrap();
852 assert_eq!(bob_results.len(), 1, "bob's entry must survive");
853 }
854
855 #[tokio::test]
857 async fn recall_ignores_explicit_agent_override() {
858 let inner: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
859 let ns_a = NamespacedMemory::new(inner.clone(), "user:alice");
860 let ns_b = NamespacedMemory::new(inner.clone(), "user:bob");
861
862 ns_a.store(&test_scope(), make_entry("m1", "alice data"))
863 .await
864 .unwrap();
865 ns_b.store(&test_scope(), make_entry("m1", "bob data"))
866 .await
867 .unwrap();
868
869 let results = ns_a
872 .recall(
873 &test_scope(),
874 MemoryQuery {
875 agent: Some("user:bob".into()),
876 limit: 10,
877 ..Default::default()
878 },
879 )
880 .await
881 .unwrap();
882 assert_eq!(results.len(), 1);
883 assert_eq!(results[0].content, "alice data");
884 }
885
886 #[tokio::test]
889 async fn per_user_namespace_isolation() {
890 let shared: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
891 let alice = NamespacedMemory::new(shared.clone(), "user:alice");
892 let bob = NamespacedMemory::new(shared.clone(), "user:bob");
893
894 alice
895 .store(&test_scope(), make_entry("m1", "Alice's deal notes"))
896 .await
897 .unwrap();
898 bob.store(&test_scope(), make_entry("m1", "Bob's pipeline review"))
899 .await
900 .unwrap();
901
902 let alice_results = alice
904 .recall(
905 &test_scope(),
906 MemoryQuery {
907 limit: 10,
908 ..Default::default()
909 },
910 )
911 .await
912 .unwrap();
913 assert_eq!(alice_results.len(), 1);
914 assert_eq!(alice_results[0].content, "Alice's deal notes");
915 assert_eq!(alice_results[0].id, "m1"); let bob_results = bob
919 .recall(
920 &test_scope(),
921 MemoryQuery {
922 limit: 10,
923 ..Default::default()
924 },
925 )
926 .await
927 .unwrap();
928 assert_eq!(bob_results.len(), 1);
929 assert_eq!(bob_results[0].content, "Bob's pipeline review");
930
931 let all = shared
933 .recall(
934 &test_scope(),
935 MemoryQuery {
936 limit: 10,
937 ..Default::default()
938 },
939 )
940 .await
941 .unwrap();
942 assert_eq!(all.len(), 2);
943 let ids: Vec<&str> = all.iter().map(|e| e.id.as_str()).collect();
944 assert!(ids.contains(&"user:alice:m1"));
945 assert!(ids.contains(&"user:bob:m1"));
946 }
947
948 #[tokio::test]
951 async fn per_user_can_coexist_with_shared_institutional_memory() {
952 let shared: Arc<dyn Memory> = Arc::new(InMemoryStore::new());
953
954 let mut institutional = make_entry("shared:playbook", "Always follow up within 24h");
956 institutional.agent = "shared".into();
957 institutional.id = "shared:playbook".into();
958 shared.store(&test_scope(), institutional).await.unwrap();
959
960 let alice = NamespacedMemory::new(shared.clone(), "user:alice");
962 alice
963 .store(&test_scope(), make_entry("m1", "Alice's note"))
964 .await
965 .unwrap();
966
967 let alice_results = alice
969 .recall(
970 &test_scope(),
971 MemoryQuery {
972 limit: 10,
973 ..Default::default()
974 },
975 )
976 .await
977 .unwrap();
978 assert_eq!(alice_results.len(), 1);
979 assert_eq!(alice_results[0].content, "Alice's note");
980
981 let all = shared
983 .recall(
984 &test_scope(),
985 MemoryQuery {
986 limit: 10,
987 ..Default::default()
988 },
989 )
990 .await
991 .unwrap();
992 assert_eq!(all.len(), 2);
993 }
994}