#[cfg(test)]
use crate::embedding::l2_normalize; use crate::errors::Error;
use crate::memory::lifecycle::{MemoryStatus, MemoryType};
use crate::memory_types::{AddResult, ConflictMemory, IngestPolicy};
use crate::sqlite::Memory;
use super::store::MemoryStore;
#[derive(Clone, Debug, Default)]
pub struct UpdateParams<'a> {
pub text: Option<&'a str>,
pub metadata: Option<&'a str>,
pub memory_type: Option<MemoryType>,
pub status: Option<MemoryStatus>,
}
#[cfg(test)]
pub(crate) fn mock_embedding_for_content(content: &str) -> Vec<f32> {
let mut hash: u64 = 0x123456789abcdef; for byte in content.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(byte as u64);
}
let mut embedding = Vec::with_capacity(384);
for i in 0..384 {
let mut dim_hash = hash.wrapping_add(i as u64);
dim_hash ^= dim_hash >> 33;
dim_hash = dim_hash.wrapping_mul(0xff51afd7ed558ccd);
dim_hash ^= dim_hash >> 33;
dim_hash = dim_hash.wrapping_mul(0xc4ceb9fe1a85ec53);
let value = ((dim_hash % 2000) as f32 - 1000.0) / 1000.0;
embedding.push(value);
}
embedding
}
#[cfg(test)]
pub(crate) fn test_fake_embedder(content: &str) -> Result<Vec<f32>, Error> {
Ok(l2_normalize(&mock_embedding_for_content(content)))
}
impl MemoryStore {
#[must_use = "the new memory ID is needed for downstream operations"]
pub fn add_with_conflict(
&mut self,
project_id: &str,
content: &str,
metadata: Option<&str>,
force: bool,
memory_type: MemoryType,
status: MemoryStatus,
) -> Result<AddResult, Error> {
Self::validate_input_length(content)?;
let embedding = self.get_embedding(content)?;
let memory_type_str = memory_type.as_str();
let status_str = status.as_str();
if force {
let id = self.db.insert(
project_id,
content,
&embedding,
metadata,
memory_type_str,
status_str,
)?;
return Ok(AddResult::Added { id });
}
let similars =
self.db
.find_similar(project_id, &embedding, self.config.similarity_threshold)?;
let conflicts: Vec<ConflictMemory> = similars
.into_iter()
.map(|m| ConflictMemory {
id: m.id,
content: m.content,
similarity: m.similarity.unwrap_or(0.0),
})
.collect();
if conflicts.is_empty() {
let id = self.db.insert(
project_id,
content,
&embedding,
metadata,
memory_type_str,
status_str,
)?;
Ok(AddResult::Added { id })
} else {
Ok(AddResult::Conflicts {
proposed: content.to_string(),
conflicts,
})
}
}
#[must_use = "handle the error or results may be lost"]
#[allow(dead_code)] pub fn ingest(
&mut self,
project_id: &str,
content: &str,
metadata: Option<&str>,
policy: IngestPolicy,
) -> Result<AddResult, Error> {
self.ingest_with_type_status(
project_id,
content,
metadata,
policy,
MemoryType::Fact,
MemoryStatus::Active,
)
}
#[must_use = "handle the error or results may be lost"]
pub fn ingest_with_type_status(
&mut self,
project_id: &str,
content: &str,
metadata: Option<&str>,
policy: IngestPolicy,
memory_type: MemoryType,
status: MemoryStatus,
) -> Result<AddResult, Error> {
match policy {
IngestPolicy::ConflictAware => {
self.add_with_conflict(project_id, content, metadata, false, memory_type, status)
}
IngestPolicy::Force => {
self.add_with_conflict(project_id, content, metadata, true, memory_type, status)
}
}
}
#[must_use = "handle the error or results may be lost"]
pub fn get(&self, id: &str, project_id: &str) -> Result<Option<Memory>, Error> {
Ok(self.db.get(id, project_id)?)
}
#[must_use = "handle the error or results may be lost"]
pub fn list(
&self,
project_id: &str,
limit: usize,
memory_types: Option<&[&str]>,
statuses: Option<&[&str]>,
) -> Result<Vec<Memory>, Error> {
use super::store::validate_limit;
validate_limit(limit)?;
Ok(self.db.list(project_id, limit, memory_types, statuses)?)
}
#[must_use = "handle the error or results may be lost"]
pub fn update(
&mut self,
id: &str,
project_id: &str,
params: UpdateParams<'_>,
) -> Result<(), Error> {
let UpdateParams {
text: content,
metadata,
memory_type,
status,
} = params;
if content.is_none() && metadata.is_none() && memory_type.is_none() && status.is_none() {
return Err(Error::InvalidInput(
"At least one of content, metadata, memory_type, or status must be provided"
.to_string(),
));
}
if let Some(meta) = metadata {
if meta.trim().is_empty() {
return Err(Error::InvalidInput("metadata cannot be empty".to_string()));
}
serde_json::from_str::<serde_json::Value>(meta)
.map_err(|e| Error::InvalidInput(format!("invalid metadata JSON: {}", e)))?;
}
if let Some(s) = status {
if s == MemoryStatus::Superseded {
return Err(Error::InvalidInput(
"Cannot set status to 'superseded'. Use --supersedes flag instead.".to_string(),
));
}
}
let embedding = if let Some(text) = content {
Self::validate_input_length(text)?;
Some(self.get_embedding(text)?)
} else {
None
};
Ok(self.db.update(
id,
project_id,
crate::sqlite::UpdateOptions {
content,
embedding: embedding.as_deref(),
metadata,
memory_type: memory_type.map(|t| t.as_str()),
status: status.map(|s| s.as_str()),
},
)?)
}
#[must_use = "handle the error or results may be lost"]
pub fn delete(&self, id: &str, project_id: &str) -> Result<bool, Error> {
Ok(self.db.delete(id, project_id)?)
}
#[allow(dead_code)] pub fn touch_memories(&self, ids: &[&str]) -> Result<(), Error> {
Ok(self.db.touch_memories(ids)?)
}
#[allow(dead_code)] #[must_use = "handle the error or results may be lost"]
pub fn list_since(
&self,
project_id: &str,
since_timestamp: &str,
limit: usize,
memory_types: Option<&[&str]>,
statuses: Option<&[&str]>,
) -> Result<Vec<Memory>, Error> {
use super::store::validate_limit;
validate_limit(limit)?;
Ok(self
.db
.list_since(project_id, since_timestamp, limit, memory_types, statuses)?)
}
#[allow(dead_code)] #[must_use = "handle the error or results may be lost"]
pub fn get_many(&self, ids: &[&str]) -> Result<Vec<Option<Memory>>, Error> {
Ok(self.db.get_many(ids)?)
}
pub(crate) fn get_embedding(&mut self, content: &str) -> Result<Vec<f32>, Error> {
#[cfg(test)]
{
if let Some(f) = &self.test_embedder {
return f(content);
}
}
self.embedder()?.embed(content)
}
}