use std::collections::{HashMap, HashSet};
#[cfg(feature = "persistence")]
use std::path::Path;
#[cfg(feature = "persistence")]
use std::sync::Arc;
use serde_json::{Map, Value};
pub type Metadata = Map<String, Value>;
use crate::clock;
use crate::embedder::Embedder;
use crate::error::MemoryError;
use crate::extract::{ExtractedAttribute, ExtractedRelation, Extractor};
use crate::id;
use crate::model::{
ColumnFilter, EntityProfile, EntityRelation, Explanation, Link, MemoryEdge, MemoryNode,
Recollection, RememberedExtraction, UnrelateOutcome,
};
#[cfg(feature = "persistence")]
use crate::mutation::MutationObserver;
#[cfg(feature = "persistence")]
use crate::storage::NativeStore;
use crate::storage::{
is_reserved_key, strip_reserved_keys, ColumnStore, FactStore, GraphStore, RecallStore,
AUTO_DATE_FIELD,
};
#[path = "fused_recall.rs"]
mod fused_recall;
#[path = "service_graph.rs"]
mod graph;
#[path = "service_graph_wiring.rs"]
mod graph_wiring;
#[cfg(feature = "persistence")]
#[allow(dead_code)]
#[path = "online_migration.rs"]
mod online_migration;
#[cfg(feature = "persistence")]
pub(crate) use online_migration::recover_startup;
#[cfg(feature = "mcp")]
pub(crate) use online_migration::{
JobPhase, JobTarget, LiveGenerationSlot, MigrationStartConfig, MigrationStatus,
OnlineMigrationManager,
};
#[cfg(feature = "persistence")]
#[path = "reinforce.rs"]
mod reinforce;
#[cfg(feature = "context")]
#[path = "context/memory_bridge.rs"]
mod memory_bridge;
use crate::storage::HUB_FIELD;
const HUB_ID_SALT: &str = "\u{0}_veles_entity_hub\u{0}";
const MENTIONS_RELATION: &str = "mentions";
const ABOUT_RELATION: &str = "about";
#[cfg(feature = "persistence")]
pub struct MemoryService<E: Embedder, S: FactStore = NativeStore> {
store: S,
embedder: E,
autograph: Option<crate::extract::DynExtractor>,
autograph_queue: AutographQueue,
generation_gate: GenerationGate,
}
#[cfg(not(feature = "persistence"))]
pub struct MemoryService<E: Embedder, S: FactStore> {
store: S,
embedder: E,
autograph: Option<crate::extract::DynExtractor>,
autograph_queue: AutographQueue,
generation_gate: GenerationGate,
}
#[path = "service_generation.rs"]
mod service_generation;
use service_generation::{GenerationGate, GenerationGuard};
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
struct AutographJob {
fact_id: u64,
fact: String,
}
#[derive(Default)]
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
struct AutographQueue {
tx: parking_lot::Mutex<Option<std::sync::mpsc::SyncSender<AutographJob>>>,
dropped: std::sync::atomic::AtomicU64,
failed: std::sync::atomic::AtomicU64,
closing: std::sync::atomic::AtomicBool,
}
pub struct AutographWorkerHandle {
close_queue: Option<Box<dyn FnOnce() + Send + Sync>>,
join: Option<std::thread::JoinHandle<()>>,
}
impl Drop for AutographWorkerHandle {
fn drop(&mut self) {
if let Some(close) = self.close_queue.take() {
close();
}
if let Some(join) = self.join.take() {
let _ = join.join();
}
}
}
#[cfg(feature = "persistence")]
impl<E: Embedder> MemoryService<E, NativeStore> {
pub fn open<P: AsRef<Path>>(path: P, embedder: E) -> Result<Self, MemoryError> {
let store = NativeStore::open(path, embedder.dimension())?;
Ok(Self {
store,
embedder,
autograph: None,
autograph_queue: AutographQueue::default(),
generation_gate: GenerationGate::new(),
})
}
pub(crate) fn install_mutation_observer(
&self,
observer: Option<Arc<dyn MutationObserver>>,
) -> Result<(), MemoryError> {
let _generation = self.generation_gate.write();
self.store.set_mutation_observer(observer)
}
pub(crate) fn migration_capture_active(&self) -> bool {
self.store.mutation_capture_active()
}
}
impl<E: Embedder, S: FactStore> MemoryService<E, S> {
pub fn with_store(store: S, embedder: E) -> Self {
Self {
store,
embedder,
autograph: None,
autograph_queue: AutographQueue::default(),
generation_gate: GenerationGate::new(),
}
}
fn enter_generation(&self) -> GenerationGuard<'_> {
GenerationGuard {
_guard: self.generation_gate.read(),
}
}
#[must_use]
pub fn with_autograph(mut self, extractor: crate::extract::DynExtractor) -> Self {
self.autograph = Some(extractor);
self
}
fn validate_write(
&self,
fact: &str,
links: &[Link],
metadata: Option<&Metadata>,
ttl_seconds: Option<u64>,
) -> Result<(), MemoryError> {
validate_fact(fact)?;
reject_zero_ttl(ttl_seconds)?;
reject_reserved_keys(metadata)?;
reject_oversized_metadata(metadata)?;
self.validate_links(links)
}
fn write_fact(
&self,
fact_id: u64,
fact: &str,
metadata: Option<&Metadata>,
ttl_seconds: Option<u64>,
) -> Result<(), MemoryError> {
let embedding = self.embedder.embed(fact)?;
let stamped = stamp_with_today(metadata);
self.store_fact(fact_id, fact, &embedding, stamped.as_ref(), ttl_seconds)
}
fn validate_links(&self, links: &[Link]) -> Result<(), MemoryError> {
for link in links {
validate_relation(&link.relation)?;
}
self.ensure_link_targets_exist(links)
}
#[must_use]
pub fn has_autograph(&self) -> bool {
self.autograph.is_some()
}
#[must_use]
pub fn fact_count(&self) -> usize {
let _generation = self.enter_generation();
self.store.count()
}
pub fn list(
&self,
cursor: Option<u64>,
limit: usize,
filter: Option<&Metadata>,
include_internal: bool,
) -> Result<(Vec<crate::model::ListedMemory>, Option<u64>), MemoryError> {
let _generation = self.enter_generation();
let limit = crate::limits::clamp_recall_limit(limit.max(1));
let (page, next) = self.store.list(cursor, limit)?;
let memories = page
.into_iter()
.filter_map(|fact| audited(fact, filter, include_internal))
.collect();
Ok((memories, next))
}
}
impl<E: Embedder, S: FactStore> MemoryService<E, S> {
fn fact_exists(&self, fact_id: u64) -> bool {
matches!(self.store.get(fact_id), Ok(Some(_)))
}
pub(crate) fn extract_passage<X: Extractor>(
text: &str,
extractor: &X,
) -> Result<crate::extract::Extraction, MemoryError> {
let text = text.trim();
if text.is_empty() {
return Err(MemoryError::EmptyFact);
}
let mut extraction = extractor.extract_graph(text)?;
crate::extract::orient_kinship(text, &mut extraction.relations);
Ok(extraction)
}
fn resolve_entity_relations(
&self,
scanned: crate::model::BoundedMemoryEdges,
far_end: impl Fn(&MemoryEdge) -> u64,
) -> Result<(Vec<EntityRelation>, bool), MemoryError> {
let mut relations = Vec::new();
let mut truncated = scanned.truncated;
for edge in scanned.edges {
if edge.relation == MENTIONS_RELATION || edge.relation == ABOUT_RELATION {
continue;
}
if relations.len() >= crate::limits::MAX_ENTITY_RELATIONS {
truncated = true;
break;
}
let far = far_end(&edge);
let content = self.store.get(far)?.map(|(content, _)| content);
relations.push(EntityRelation {
predicate: edge.relation,
target_id: far,
target: content.unwrap_or_default(),
});
}
Ok((relations, truncated))
}
fn wire_attributes(
&self,
attributes: &[ExtractedAttribute],
entity_ids: &mut HashMap<String, u64>,
) -> Result<(), MemoryError> {
let mut per_entity: HashMap<String, Metadata> = HashMap::new();
for attribute in attributes {
if is_reserved_key(&attribute.key) {
continue;
}
per_entity
.entry(attribute.entity.clone())
.or_default()
.insert(attribute.key.clone(), attribute.value.clone());
}
for (entity, meta) in per_entity {
if meta.is_empty() {
continue;
}
reject_oversized_metadata(Some(&meta))?;
let hub_id = self.entity_hub(&entity, entity_ids)?;
self.store.update_metadata(hub_id, &meta)?;
}
Ok(())
}
fn entity_hub(
&self,
entity: &str,
entity_ids: &mut HashMap<String, u64>,
) -> Result<u64, MemoryError> {
let key = entity.trim().to_lowercase();
if let Some(&id) = entity_ids.get(&key) {
return Ok(id);
}
let id = self.remember_hub(&key)?;
entity_ids.insert(key, id);
Ok(id)
}
fn remember_hub(&self, key: &str) -> Result<u64, MemoryError> {
let id = id::stable_id(&format!("{HUB_ID_SALT}{key}"));
if self.store.get(id)?.is_some() {
return Ok(id);
}
let content = format!("Entity: {key}");
let embedding = self.embedder.embed(&content)?;
let mut meta = Map::new();
meta.insert(HUB_FIELD.to_string(), Value::Bool(true));
self.store_fact(id, &content, &embedding, Some(&meta), None)?;
Ok(id)
}
fn ensure_exists(&self, id: u64) -> Result<(), MemoryError> {
if self.store.get(id)?.is_none() {
return Err(MemoryError::UnknownMemory(id));
}
Ok(())
}
fn ensure_link_targets_exist(&self, links: &[Link]) -> Result<(), MemoryError> {
for link in links {
self.ensure_exists(link.target)?;
}
Ok(())
}
fn store_fact(
&self,
id: u64,
fact: &str,
embedding: &[f32],
metadata: Option<&Metadata>,
ttl_seconds: Option<u64>,
) -> Result<(), MemoryError> {
match (metadata, ttl_seconds) {
(Some(meta), Some(ttl)) => {
self.store
.store_with_metadata_and_ttl(id, fact, embedding, meta, ttl)?;
}
(Some(meta), None) => self.store.store_with_metadata(id, fact, embedding, meta)?,
(None, Some(ttl)) => self.store.store_with_ttl(id, fact, embedding, ttl)?,
(None, None) => self.store.store(id, fact, embedding)?,
}
Ok(())
}
pub fn recall(
&self,
query: &str,
k: usize,
filter: Option<&Metadata>,
) -> Result<Vec<Recollection>, MemoryError>
where
S: RecallStore,
{
let _generation = self.enter_generation();
self.recall_inner(query, k, filter)
}
fn recall_inner(
&self,
query: &str,
k: usize,
filter: Option<&Metadata>,
) -> Result<Vec<Recollection>, MemoryError>
where
S: RecallStore,
{
let query = query.trim();
if query.is_empty() {
return Ok(Vec::new());
}
reject_reserved_keys(filter)?;
let embedding = self.embedder.embed(query)?;
let hits = self.search(&embedding, k, filter)?;
let ids: Vec<u64> = hits.iter().map(|(id, _, _)| *id).collect();
let payloads = self.store.get_metadata_batch(&ids)?;
#[cfg(feature = "persistence")]
let (hits, payloads) = Self::rl_rerank(hits, payloads);
Ok(hits
.into_iter()
.zip(payloads)
.map(|((id, score, content), payload)| Recollection {
id,
score,
content,
metadata: strip_reserved_keys(payload),
})
.collect())
}
fn search(
&self,
embedding: &[f32],
k: usize,
filter: Option<&Metadata>,
) -> Result<Vec<(u64, f32, String)>, MemoryError>
where
S: RecallStore,
{
match filter {
Some(meta) if !meta.is_empty() => self.store.query_filtered(embedding, k, meta, 0),
_ => self
.store
.query_excluding(embedding, k, &hub_exclude_filter()),
}
}
pub fn recall_where(
&self,
query: &str,
k: usize,
filters: &[ColumnFilter],
) -> Result<Vec<Recollection>, MemoryError>
where
S: ColumnStore + RecallStore,
{
let _generation = self.enter_generation();
let query = query.trim();
if query.is_empty() || k == 0 {
return Ok(Vec::new());
}
if filters.is_empty() {
return self.recall_inner(query, k, None);
}
let embedding = self.embedder.embed(query)?;
self.store.query_columnar(&embedding, k, filters)
}
}
fn hub_exclude_filter() -> Metadata {
let mut exclude = Map::new();
exclude.insert(HUB_FIELD.to_string(), Value::Bool(true));
exclude
}
fn reject_reserved_keys(metadata: Option<&Metadata>) -> Result<(), MemoryError> {
let Some(meta) = metadata else {
return Ok(());
};
for key in meta.keys() {
if is_reserved_key(key) {
return Err(MemoryError::ReservedKey(key.clone()));
}
}
Ok(())
}
fn reject_oversized_metadata(metadata: Option<&Metadata>) -> Result<(), MemoryError> {
let Some(meta) = metadata else {
return Ok(());
};
let bytes = crate::limits::metadata_bytes(meta);
if bytes > crate::limits::MAX_METADATA_BYTES {
return Err(MemoryError::MetadataTooLarge {
bytes,
max: crate::limits::MAX_METADATA_BYTES,
});
}
Ok(())
}
#[cfg(feature = "context")]
pub(crate) fn positive_ttl(ttl_seconds: Option<u64>) -> Option<u64> {
ttl_seconds.filter(|&seconds| seconds > 0)
}
fn audited(
fact: crate::storage::RawListedFact,
filter: Option<&Metadata>,
include_internal: bool,
) -> Option<crate::model::ListedMemory> {
if !include_internal && crate::storage::is_internal_scaffolding(&fact.payload) {
return None;
}
let matches = filter.is_none_or(|wanted| {
wanted
.iter()
.all(|(key, value)| fact.payload.get(key) == Some(value))
});
if !matches {
return None;
}
let metadata = if include_internal {
(!fact.payload.is_empty()).then_some(fact.payload)
} else {
strip_reserved_keys(Some(fact.payload))
};
Some(crate::model::ListedMemory {
id: fact.id,
content: fact.content,
metadata,
})
}
#[must_use]
pub fn canonical_entity_name(name: &str) -> String {
name.trim().to_lowercase()
}
fn validate_fact(fact: &str) -> Result<(), MemoryError> {
if fact.is_empty() {
return Err(MemoryError::EmptyFact);
}
validate_embeddable(fact)
}
pub(crate) fn validate_embeddable(text: &str) -> Result<(), MemoryError> {
if text.len() > crate::limits::MAX_EMBEDDABLE_TEXT_BYTES {
return Err(MemoryError::FactTooLarge {
bytes: text.len(),
max: crate::limits::MAX_EMBEDDABLE_TEXT_BYTES,
});
}
Ok(())
}
#[cfg(feature = "context")]
pub(crate) fn embeddable_prefix(text: &str) -> &str {
let cap = crate::limits::MAX_EMBEDDABLE_TEXT_BYTES;
if text.len() <= cap {
return text;
}
let mut end = cap;
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
&text[..end]
}
fn reject_zero_ttl(ttl_seconds: Option<u64>) -> Result<(), MemoryError> {
if ttl_seconds == Some(0) {
return Err(MemoryError::ZeroTtl);
}
Ok(())
}
fn reject_self_links(fact_id: u64, links: &[Link]) -> Result<(), MemoryError> {
if links.iter().any(|link| link.target == fact_id) {
return Err(MemoryError::SelfRelation(fact_id));
}
Ok(())
}
fn stamp_with_today(metadata: Option<&Metadata>) -> Option<Metadata> {
if metadata.is_some_and(|meta| meta.contains_key(AUTO_DATE_FIELD)) {
return metadata.cloned();
}
let Some(today) = clock::today_ymd() else {
return metadata.cloned();
};
let mut stamped = metadata.cloned().unwrap_or_default();
stamped.insert(AUTO_DATE_FIELD.to_owned(), Value::from(today));
Some(stamped)
}
const MAX_RELATION_BYTES: usize = 512;
fn validate_relation(label: &str) -> Result<(), MemoryError> {
if label.is_empty() {
return Err(MemoryError::InvalidRelation(
"relation label must not be empty".to_owned(),
));
}
if label.len() > MAX_RELATION_BYTES {
return Err(MemoryError::InvalidRelation(format!(
"relation label exceeds maximum of {MAX_RELATION_BYTES} bytes ({} given)",
label.len()
)));
}
if label.chars().any(|c| c.is_ascii_control()) {
return Err(MemoryError::InvalidRelation(
"relation label must not contain ASCII control characters".to_owned(),
));
}
Ok(())
}