1use std::collections::{HashMap, HashSet};
8use std::io::Write;
9use std::path::Path;
10
11use mentedb_core::edge::EdgeType;
12use mentedb_core::error::MenteResult;
13use mentedb_core::memory::AttributeValue;
14use mentedb_core::types::{AgentId, MemoryId, Timestamp};
15use mentedb_core::{MemoryEdge, MemoryNode, MenteError};
16use tracing::warn;
17
18use crate::reranker::Reranker;
19use crate::{MenteDb, StoreOutcome};
20
21pub const ORIGIN_ATTRIBUTE: &str = "origin";
23pub const ORIGIN_DERIVED: &str = "derived";
25pub const DERIVED_KIND_ATTRIBUTE: &str = "derived_kind";
27const WATERMARK_FILE: &str = "enrichment_watermark";
29
30#[derive(Clone, Copy, Default)]
32pub struct HybridOptions<'a> {
33 pub mmr_lambda: Option<f32>,
36 pub reranker: Option<&'a dyn Reranker>,
39 pub relevance_first: bool,
43 pub record_access: bool,
47}
48
49#[derive(Clone, Debug, Default)]
52pub struct MemoryPatch {
53 pub attributes: HashMap<String, AttributeValue>,
55 pub tags: Option<Vec<String>>,
57 pub valid_until: Option<Option<Timestamp>>,
60}
61
62#[derive(Clone, Debug, Default)]
64pub struct MemoryPage {
65 pub memories: Vec<MemoryNode>,
67 pub has_more: bool,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum DerivedKind {
74 Fact,
75 Entity,
76 Community,
77 Profile,
78}
79
80impl DerivedKind {
81 pub fn as_str(self) -> &'static str {
82 match self {
83 Self::Fact => "fact",
84 Self::Entity => "entity",
85 Self::Community => "community",
86 Self::Profile => "profile",
87 }
88 }
89}
90
91pub(crate) fn mark_derived(
95 node: &mut MemoryNode,
96 kind: DerivedKind,
97 inherited: &HashMap<String, AttributeValue>,
98) {
99 for (k, v) in inherited {
100 node.attributes
101 .entry(k.clone())
102 .or_insert_with(|| v.clone());
103 }
104 node.attributes.insert(
105 ORIGIN_ATTRIBUTE.to_string(),
106 AttributeValue::String(ORIGIN_DERIVED.to_string()),
107 );
108 node.attributes.insert(
109 DERIVED_KIND_ATTRIBUTE.to_string(),
110 AttributeValue::String(kind.as_str().to_string()),
111 );
112}
113
114pub(crate) fn read_enrichment_watermark(dir: &Path) -> u64 {
116 std::fs::read_to_string(dir.join(WATERMARK_FILE))
117 .ok()
118 .and_then(|s| s.trim().parse().ok())
119 .unwrap_or(0)
120}
121
122pub(crate) fn write_enrichment_watermark(dir: &Path, value: u64) -> std::io::Result<()> {
125 let tmp = dir.join(format!("{WATERMARK_FILE}.tmp"));
126 {
127 let mut f = std::fs::File::create(&tmp)?;
128 f.write_all(value.to_string().as_bytes())?;
129 f.sync_all()?;
130 }
131 std::fs::rename(&tmp, dir.join(WATERMARK_FILE))
132}
133
134fn now_micros() -> Timestamp {
135 std::time::SystemTime::now()
136 .duration_since(std::time::UNIX_EPOCH)
137 .unwrap_or_default()
138 .as_micros() as u64
139}
140
141impl MenteDb {
142 pub(crate) fn inherited_attributes(
145 &self,
146 sources: &[MemoryId],
147 ) -> HashMap<String, AttributeValue> {
148 let keys = &self
149 .cognitive_config
150 .enrichment_config
151 .copy_source_attributes;
152 let mut out = HashMap::new();
153 if keys.is_empty() {
154 return out;
155 }
156 for id in sources {
157 if out.len() == keys.len() {
158 break;
159 }
160 let Ok(src) = self.get_memory(*id) else {
161 continue;
162 };
163 for key in keys {
164 if !out.contains_key(key)
165 && let Some(v) = src.attributes.get(key)
166 {
167 out.insert(key.clone(), v.clone());
168 }
169 }
170 }
171 out
172 }
173
174 pub fn update_memory_fields(
178 &self,
179 id: MemoryId,
180 patch: &MemoryPatch,
181 ) -> MenteResult<MemoryNode> {
182 let page = self.page_of(id).ok_or(MenteError::MemoryNotFound(id))?;
183 let old = self.storage.load_memory(page)?;
184 let mut node = old.clone();
185 for (k, v) in &patch.attributes {
186 node.attributes.insert(k.clone(), v.clone());
187 }
188 if let Some(tags) = &patch.tags {
189 node.tags = tags.clone();
190 }
191 if let Some(until) = patch.valid_until {
192 node.valid_until = until;
193 }
194 self.update_node(page, &node)?;
195 if node.tags != old.tags {
196 self.index.reindex_tags(&node);
197 if self
198 .scope_counts_ready
199 .load(std::sync::atomic::Ordering::Relaxed)
200 {
201 let hidden = &self.cognitive_config.hidden_count_tags;
202 let mut counts = self.scope_counts.write();
203 counts.apply(&old, -1, hidden);
204 counts.apply(&node, 1, hidden);
205 }
206 }
207 Ok(node)
208 }
209
210 pub fn supersede_memory(
218 &self,
219 old: MemoryId,
220 new: MemoryNode,
221 at: Timestamp,
222 ) -> MenteResult<MemoryId> {
223 let previous = self.get_memory(old)?;
224 let new_id = new.id;
225 match self.store_with_outcome(new)? {
226 StoreOutcome::Stored => {}
227 StoreOutcome::Duplicate { of } => {
228 return Err(MenteError::Storage(format!(
229 "the replacement duplicates memory {of}; nothing was stored"
230 )));
231 }
232 }
233 if previous.valid_until.is_none_or(|u| u > at) {
235 self.update_memory_fields(
236 old,
237 &MemoryPatch {
238 valid_until: Some(Some(at)),
239 ..MemoryPatch::default()
240 },
241 )?;
242 }
243 let already_linked = self
244 .graph
245 .graph()
246 .outgoing(new_id)
247 .iter()
248 .any(|(t, e)| *t == old && e.edge_type == EdgeType::Supersedes);
249 if !already_linked {
250 self.relate(MemoryEdge {
251 source: new_id,
252 target: old,
253 edge_type: EdgeType::Supersedes,
254 weight: 1.0,
255 created_at: at,
256 valid_from: Some(at),
257 valid_until: None,
258 label: None,
259 })?;
260 }
261 Ok(new_id)
262 }
263
264 fn superseded_at(&self, id: MemoryId, at: Timestamp) -> bool {
266 self.graph
267 .graph()
268 .incoming(id)
269 .iter()
270 .any(|(_, e)| e.edge_type == EdgeType::Supersedes && e.is_valid_at(at))
271 }
272
273 pub fn is_active_at(&self, node: &MemoryNode, at: Timestamp) -> bool {
276 node.is_valid_at(at) && !self.superseded_at(node.id, at)
277 }
278
279 pub fn list_memories_page(
288 &self,
289 tag: Option<&str>,
290 agent: Option<AgentId>,
291 include_inactive: bool,
292 at: Timestamp,
293 offset: usize,
294 limit: usize,
295 ) -> MemoryPage {
296 let mut allowed: Option<HashSet<MemoryId>> =
297 tag.map(|t| self.index.bitmap.query_tag(t).into_iter().collect());
298 if let Some(a) = agent {
299 let keys = [
300 mentedb_index::manager::agent_index_key(a),
301 mentedb_index::manager::agent_index_key(AgentId::nil()),
302 ];
303 let refs: Vec<&str> = keys.iter().map(String::as_str).collect();
304 let owned: HashSet<MemoryId> =
305 self.index.bitmap.query_tags_or(&refs).into_iter().collect();
306 allowed = Some(match allowed {
307 Some(prev) => prev.intersection(&owned).copied().collect(),
308 None => owned,
309 });
310 }
311 let ordered = self
312 .index
313 .temporal
314 .range(0, Timestamp::MAX)
315 .into_iter()
316 .filter(|id| allowed.as_ref().is_none_or(|a| a.contains(id)));
317
318 let mut page = MemoryPage::default();
319 let mut skipped = 0usize;
320 for id in ordered {
321 if include_inactive && skipped < offset {
322 skipped += 1;
323 continue;
324 }
325 let Ok(node) = self.get_memory(id) else {
326 continue;
327 };
328 if !include_inactive && !self.is_active_at(&node, at) {
329 continue;
330 }
331 if skipped < offset {
332 skipped += 1;
333 continue;
334 }
335 if page.memories.len() == limit {
336 page.has_more = true;
337 break;
338 }
339 page.memories.push(node);
340 }
341 page
342 }
343
344 pub fn note_access(&self, ids: impl IntoIterator<Item = MemoryId>) {
347 let now = now_micros();
348 let mut pending = self.pending_access.lock();
349 for id in ids {
350 let entry = pending.entry(id).or_insert((now, 0));
351 entry.0 = entry.0.max(now);
352 entry.1 = entry.1.saturating_add(1);
353 }
354 }
355
356 pub fn pending_access_count(&self) -> usize {
358 self.pending_access.lock().len()
359 }
360
361 pub(crate) fn apply_pending_access(&self) {
363 let pending = std::mem::take(&mut *self.pending_access.lock());
364 for (id, (at, hits)) in pending {
365 let Some(page) = self.page_of(id) else {
366 continue;
367 };
368 let Ok(mut node) = self.storage.load_memory(page) else {
369 continue;
370 };
371 node.accessed_at = node.accessed_at.max(at);
372 node.access_count = node.access_count.saturating_add(hits);
373 if let Err(e) = self.update_node(page, &node) {
374 warn!(%id, error = %e, "writing access statistics failed");
375 }
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use crate::CognitiveConfig;
384 use mentedb_core::memory::MemoryType;
385
386 fn db(dir: &Path) -> MenteDb {
387 MenteDb::open_with_config(
388 dir,
389 CognitiveConfig {
390 write_inference: false,
391 memory_dedup: false,
392 ..CognitiveConfig::default()
393 },
394 )
395 .unwrap()
396 }
397
398 fn node(agent: AgentId, content: &str, tags: &[&str]) -> MemoryNode {
399 let mut n = MemoryNode::new(agent, MemoryType::Semantic, content.into(), vec![]);
400 n.tags = tags.iter().map(|t| t.to_string()).collect();
401 n
402 }
403
404 #[test]
405 fn update_merges_attributes_replaces_tags_and_reindexes() {
406 let dir = tempfile::tempdir().unwrap();
407 let db = db(dir.path());
408 let mut n = node(AgentId::nil(), "the sky is blue", &["a"]);
409 n.attributes
410 .insert("keep".into(), AttributeValue::String("x".into()));
411 let id = n.id;
412 db.store(n).unwrap();
413
414 let mut patch = MemoryPatch {
415 tags: Some(vec!["b".into()]),
416 ..MemoryPatch::default()
417 };
418 patch
419 .attributes
420 .insert("added".into(), AttributeValue::Integer(7));
421 let updated = db.update_memory_fields(id, &patch).unwrap();
422 assert_eq!(updated.id, id);
423 assert!(updated.attributes.contains_key("keep"));
424 assert!(updated.attributes.contains_key("added"));
425 assert!(db.memory_ids_with_tag("a").is_empty());
426 assert_eq!(db.memory_ids_with_tag("b"), vec![id]);
427
428 db.close().unwrap();
430 drop(db);
431 let db = super::tests::db(dir.path());
432 let back = db.get_memory(id).unwrap();
433 assert_eq!(back.tags, vec!["b".to_string()]);
434 assert!(matches!(
435 back.attributes.get("added"),
436 Some(AttributeValue::Integer(7))
437 ));
438 }
439
440 #[test]
441 fn supersede_closes_the_old_memory_and_links_the_new_one() {
442 let dir = tempfile::tempdir().unwrap();
443 let db = db(dir.path());
444 let old = node(AgentId::nil(), "phone number is 1234", &[]);
445 let old_id = old.id;
446 db.store(old).unwrap();
447 let at = now_micros();
448 let new = node(AgentId::nil(), "phone number is 4321", &[]);
449 let new_id = db.supersede_memory(old_id, new, at).unwrap();
450 assert_ne!(new_id, old_id);
451 let closed = db.get_memory(old_id).unwrap();
452 assert_eq!(closed.valid_until, Some(at));
453 let history = db.supersession_history(new_id, 4).unwrap();
454 assert_eq!(history.len(), 1);
455 assert_eq!(history[0].id, old_id);
456 assert!(!db.is_active_at(&closed, at + 1));
457 assert!(
458 db.supersede_memory(MemoryId::new(), node(AgentId::nil(), "x", &[]), at)
459 .is_err()
460 );
461 }
462
463 #[test]
464 fn pages_are_stable_and_narrowed_by_tag_agent_and_activity() {
465 let dir = tempfile::tempdir().unwrap();
466 let db = db(dir.path());
467 let a = AgentId::new();
468 let b = AgentId::new();
469 let mut ids = Vec::new();
470 for i in 0..10 {
471 let owner = if i % 2 == 0 { a } else { b };
472 let mut n = node(
473 owner,
474 &format!("memory {i}"),
475 if i < 6 { &["t"] } else { &[] },
476 );
477 n.created_at = 1_000 + i as u64;
478 ids.push(n.id);
479 db.store(n).unwrap();
480 }
481 let shared = node(AgentId::nil(), "shared", &["t"]);
482 let shared_id = shared.id;
483 db.store(shared).unwrap();
484 db.invalidate_memory(ids[2], 1).unwrap();
486 let now = now_micros();
487
488 let all = db.list_memories_page(None, None, true, now, 0, 100);
489 assert_eq!(all.memories.len(), 11);
490 let order: Vec<MemoryId> = all.memories.iter().map(|m| m.id).collect();
491 assert_eq!(&order[..10], &ids[..], "creation order");
492
493 let tagged_a = db.list_memories_page(Some("t"), Some(a), false, now, 0, 100);
494 let got: Vec<MemoryId> = tagged_a.memories.iter().map(|m| m.id).collect();
495 assert_eq!(got, vec![ids[0], ids[4], shared_id]);
496
497 let with_inactive = db.list_memories_page(Some("t"), Some(a), true, now, 0, 100);
498 assert_eq!(with_inactive.memories.len(), 4);
499
500 let p1 = db.list_memories_page(None, None, true, now, 0, 4);
502 let p2 = db.list_memories_page(None, None, true, now, 4, 4);
503 let p3 = db.list_memories_page(None, None, true, now, 8, 4);
504 assert!(p1.has_more && p2.has_more && !p3.has_more);
505 let walked: Vec<MemoryId> = [p1, p2, p3]
506 .into_iter()
507 .flat_map(|p| p.memories.into_iter().map(|m| m.id))
508 .collect();
509 assert_eq!(walked, order);
510 }
511
512 #[test]
513 fn access_statistics_are_batched_until_flush() {
514 let dir = tempfile::tempdir().unwrap();
515 let db = db(dir.path());
516 let n = node(AgentId::nil(), "x", &[]);
517 let id = n.id;
518 db.store(n).unwrap();
519 let before = db.get_memory(id).unwrap();
520 db.note_access([id, id]);
521 assert_eq!(db.pending_access_count(), 1);
522 assert_eq!(db.get_memory(id).unwrap().access_count, before.access_count);
523 db.flush().unwrap();
524 assert_eq!(db.pending_access_count(), 0);
525 assert_eq!(
526 db.get_memory(id).unwrap().access_count,
527 before.access_count + 2
528 );
529 }
530
531 #[test]
532 fn watermark_round_trips() {
533 let dir = tempfile::tempdir().unwrap();
534 assert_eq!(read_enrichment_watermark(dir.path()), 0);
535 write_enrichment_watermark(dir.path(), 42).unwrap();
536 assert_eq!(read_enrichment_watermark(dir.path()), 42);
537 }
538
539 #[test]
540 fn enrichment_watermark_survives_reopen() {
541 let dir = tempfile::tempdir().unwrap();
542 let db = db(dir.path());
543 db.mark_enrichment_complete(12_345);
544 db.close().unwrap();
545 drop(db);
546 let db = super::tests::db(dir.path());
547 assert_eq!(db.last_enrichment_turn(), 12_345);
548 }
549
550 #[test]
551 fn decay_exemption_by_attribute() {
552 let dir = tempfile::tempdir().unwrap();
553 let db = db(dir.path());
554 let mut n = node(AgentId::nil(), "x", &[]);
555 assert!(!db.is_decay_exempt(&n));
556 n.attributes
557 .insert("decay_exempt".into(), AttributeValue::Boolean(true));
558 assert!(db.is_decay_exempt(&n));
559 n.attributes
560 .insert("decay_exempt".into(), AttributeValue::Boolean(false));
561 assert!(!db.is_decay_exempt(&n));
562 }
563
564 #[test]
565 fn derived_memories_carry_provenance() {
566 let dir = tempfile::tempdir().unwrap();
567 let mut cfg = CognitiveConfig {
568 write_inference: false,
569 memory_dedup: false,
570 ..CognitiveConfig::default()
571 };
572 cfg.enrichment_config.copy_source_attributes = vec!["doc".into(), "absent".into()];
573 let db = MenteDb::open_with_config(dir.path(), cfg).unwrap();
574 let mut src = node(AgentId::nil(), "source turn", &[]);
575 src.memory_type = MemoryType::Episodic;
576 src.attributes
577 .insert("doc".into(), AttributeValue::String("d-1".into()));
578 src.attributes
579 .insert("private".into(), AttributeValue::String("p".into()));
580 let src_id = src.id;
581 db.store(src).unwrap();
582
583 let fact = node(AgentId::nil(), "a fact", &[]);
584 let fact_id = fact.id;
585 let entity = node(AgentId::nil(), "an entity", &["entity:x"]);
586 let entity_id = entity.id;
587 db.store_enrichment_memories(vec![fact, entity], &[src_id])
588 .unwrap();
589 let fact = db.get_memory(fact_id).unwrap();
590 fn s<'a>(n: &'a MemoryNode, key: &str) -> Option<&'a str> {
591 match n.attributes.get(key) {
592 Some(AttributeValue::String(v)) => Some(v.as_str()),
593 _ => None,
594 }
595 }
596 assert_eq!(s(&fact, "doc"), Some("d-1"));
597 assert!(!fact.attributes.contains_key("private"));
598 assert_eq!(s(&fact, ORIGIN_ATTRIBUTE), Some("derived"));
599 assert_eq!(s(&fact, DERIVED_KIND_ATTRIBUTE), Some("fact"));
600 let entity = db.get_memory(entity_id).unwrap();
601 assert_eq!(s(&entity, DERIVED_KIND_ATTRIBUTE), Some("entity"));
602
603 let profile = db
604 .store_user_profile(
605 "likes tea",
606 mentedb_core::types::UserId::nil(),
607 AgentId::nil(),
608 )
609 .unwrap();
610 let profile = db.get_memory(profile).unwrap();
611 assert_eq!(s(&profile, DERIVED_KIND_ATTRIBUTE), Some("profile"));
612 }
613
614 #[test]
615 fn enrichment_candidate_types_are_configurable() {
616 let dir = tempfile::tempdir().unwrap();
617 let mut cfg = CognitiveConfig {
618 write_inference: false,
619 memory_dedup: false,
620 ..CognitiveConfig::default()
621 };
622 let semantic = node(AgentId::nil(), "a semantic note", &[]);
623 let mut episodic = node(AgentId::nil(), "a turn", &[]);
624 episodic.memory_type = MemoryType::Episodic;
625 {
626 let db = MenteDb::open_with_config(dir.path(), cfg.clone()).unwrap();
627 db.store(semantic).unwrap();
628 db.store(episodic).unwrap();
629 let c = db.enrichment_candidates(mentedb_core::types::UserId::nil(), AgentId::nil());
630 assert_eq!(c.len(), 1, "episodic only by default");
631 db.close().unwrap();
632 }
633 cfg.enrichment_config.candidate_types = vec![MemoryType::Episodic, MemoryType::Semantic];
634 let db = MenteDb::open_with_config(dir.path(), cfg).unwrap();
635 let c = db.enrichment_candidates(mentedb_core::types::UserId::nil(), AgentId::nil());
636 assert_eq!(c.len(), 2);
637 }
638}