diff --git a/omem-server/src/ingest/reconciler.rs b/omem-server/src/ingest/reconciler.rs index aea0100..e40ea5e 100644 --- a/omem-server/src/ingest/reconciler.rs +++ b/omem-server/src/ingest/reconciler.rs @@ -13,6 +13,7 @@ use crate::ingest::preference_slots; use crate::ingest::prompts; use crate::ingest::types::{BatchDedupResult, ExtractedFact, ReconcileResult}; use crate::llm::{complete_json, LlmService}; +use crate::store::lancedb::SUPERSEDE_CONFIDENCE_PENALTY; use crate::store::LanceStore; const DEFAULT_MAX_EXISTING: usize = 60; @@ -258,7 +259,9 @@ impl Reconciler { return Ok(()); } - let new_mem = self.create_fact_memory(fact, tenant_id).await?; + let mut new_mem = self.create_fact_memory(fact, tenant_id).await?; + new_mem.confidence = (new_mem.confidence * SUPERSEDE_CONFIDENCE_PENALTY).clamp(0.0, 1.0); + self.store.update(&new_mem, None).await?; let mut archived = old; archived.invalidated_at = Some(chrono::Utc::now().to_rfc3339()); @@ -363,6 +366,7 @@ impl Reconciler { } let mut new_mem = self.create_fact_memory(fact, tenant_id).await?; + new_mem.confidence = (new_mem.confidence * SUPERSEDE_CONFIDENCE_PENALTY).clamp(0.0, 1.0); new_mem.relations.push(MemoryRelation { relation_type: RelationType::Contradicts, target_id: real_id.clone(), diff --git a/omem-server/src/store/lancedb.rs b/omem-server/src/store/lancedb.rs index c4a8456..19f17a8 100644 --- a/omem-server/src/store/lancedb.rs +++ b/omem-server/src/store/lancedb.rs @@ -28,6 +28,11 @@ const TABLE_NAME: &str = "memories"; /// Excludes deleted (soft-deleted) and superseded (replaced by another memory). const DEFAULT_STATE_FILTER: &str = "state NOT IN ('deleted', 'superseded')"; +/// Confidence multiplier applied to a memory that supersedes existing ones. +/// A replacement fact starts at reduced confidence until reinforced by further +/// access (the frequency/decay scoring naturally lifts it over time). +pub const SUPERSEDE_CONFIDENCE_PENALTY: f32 = 0.8; + /// State filter that includes superseded memories. /// Use when an explicit caller wants to see historical/replaced entries. const STATE_FILTER_KEEPING_SUPERSEDED: &str = "state != 'deleted'"; @@ -668,7 +673,9 @@ impl LanceStore { ))); } - self.create(new, new_vector).await?; + let mut penalised = new.clone(); + penalised.confidence = (new.confidence * SUPERSEDE_CONFIDENCE_PENALTY).clamp(0.0, 1.0); + self.create(&penalised, new_vector).await?; let now = chrono::Utc::now().to_rfc3339(); let mut update_failures = Vec::new(); @@ -1721,4 +1728,45 @@ mod tests { let direct = store.get_by_id(&old.id).await.unwrap(); assert!(direct.is_some(), "get_by_id should still return superseded"); } + + #[tokio::test] + async fn test_supersede_applies_confidence_penalty() { + let (store, _dir) = setup().await; + let v = vec![0.1f32; DEFAULT_VECTOR_DIM as usize]; + + let old = make_memory("t-001", "original fact"); + store.create(&old, Some(&v)).await.unwrap(); + + let mut new = make_memory("t-001", "replacement fact"); + new.confidence = 0.5; + + store + .supersede_batch(&new, Some(&v), &[old.id.clone()]) + .await + .unwrap(); + + let stored = store.get_by_id(&new.id).await.unwrap().unwrap(); + let expected = 0.5 * SUPERSEDE_CONFIDENCE_PENALTY; + assert!( + (stored.confidence - expected).abs() < f32::EPSILON, + "confidence should be {expected}, got {}", + stored.confidence + ); + } + + #[tokio::test] + async fn test_supersede_no_penalty_without_old_ids() { + let (store, _dir) = setup().await; + + let mut mem = make_memory("t-001", "new fact, no replacement"); + mem.confidence = 0.5; + store.supersede_batch(&mem, None, &[]).await.unwrap(); + + let stored = store.get_by_id(&mem.id).await.unwrap().unwrap(); + assert!( + (stored.confidence - 0.5).abs() < f32::EPSILON, + "confidence should be unchanged at 0.5 when no old_ids, got {}", + stored.confidence + ); + } }