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