From df6d819add8f5d2e7ba59dcdf0ef815d422ae067 Mon Sep 17 00:00:00 2001 From: Matthias Queitsch Date: Mon, 13 Apr 2026 19:15:31 +0200 Subject: [PATCH] feat: dream connection eviction uses composite score instead of FIFO --- crates/vestige-core/src/advanced/dreams.rs | 36 ++++++++++++++++++-- crates/vestige-mcp/src/dashboard/handlers.rs | 12 +++++-- crates/vestige-mcp/src/tools/dream.rs | 20 ++++++++--- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/crates/vestige-core/src/advanced/dreams.rs b/crates/vestige-core/src/advanced/dreams.rs index 88a86ce..a4ddac5 100644 --- a/crates/vestige-core/src/advanced/dreams.rs +++ b/crates/vestige-core/src/advanced/dreams.rs @@ -1076,6 +1076,9 @@ pub struct DiscoveredConnection { pub connection_type: DiscoveredConnectionType, /// Reasoning for this connection pub reasoning: String, + /// When this connection was discovered (used for recency scoring during eviction) + #[serde(default = "Utc::now")] + pub discovered_at: DateTime, } /// Types of connections discovered during dreaming @@ -1277,6 +1280,7 @@ impl MemoryDreamer { similarity, connection_type, reasoning, + discovered_at: Utc::now(), }); } } @@ -1699,10 +1703,38 @@ impl MemoryDreamer { fn store_connections(&self, connections: &[DiscoveredConnection]) { if let Ok(mut stored) = self.connections.write() { stored.extend(connections.iter().cloned()); - // Keep last 1000 connections + // Keep the 1000 highest-scoring connections using a composite score + // that balances quality (similarity) and recency (age-based decay). + // + // score = similarity * 0.6 + recency * 0.4 + // + // Recency uses exponential decay with a 7-day half-life: + // recency = 0.5 ^ (age_days / 7.0) + // + // This means: + // - A brand-new connection with similarity 0.5 scores 0.70 + // - A week-old connection with similarity 0.9 scores 0.74 + // - A month-old connection with similarity 0.9 scores 0.58 + // Strong old connections are retained longer than weak new ones, + // but eventually yield to fresh high-quality discoveries. let len = stored.len(); if len > 1000 { - stored.drain(0..(len - 1000)); + let now = Utc::now(); + stored.sort_unstable_by(|a, b| { + let score = |c: &DiscoveredConnection| -> f64 { + let age_days = now + .signed_duration_since(c.discovered_at) + .num_seconds() + .max(0) as f64 + / 86_400.0; + let recency = (0.5_f64).powf(age_days / 7.0); + c.similarity * 0.6 + recency * 0.4 + }; + score(b) + .partial_cmp(&score(a)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + stored.truncate(1000); } } } diff --git a/crates/vestige-mcp/src/dashboard/handlers.rs b/crates/vestige-mcp/src/dashboard/handlers.rs index 375664a..af65cc6 100644 --- a/crates/vestige-mcp/src/dashboard/handlers.rs +++ b/crates/vestige-mcp/src/dashboard/handlers.rs @@ -539,17 +539,23 @@ pub async fn trigger_dream( // Run dream through CognitiveEngine let cog = cognitive.lock().await; - let pre_dream_count = cog.dreamer.get_connections().len(); + // Capture start time before the dream — composite-score eviction in store_connections + // reorders the buffer, making positional slicing (pre_dream_count..) unreliable. + let dream_start = Utc::now(); let dream_result = cog.dreamer.dream(&dream_memories).await; let insights = cog.dreamer.synthesize_insights(&dream_memories); let all_connections = cog.dreamer.get_connections(); drop(cog); // Persist new connections - let new_connections = &all_connections[pre_dream_count..]; + // Filter by timestamp — same approach as dream.rs to avoid positional index issues. + let new_connections: Vec<&vestige_core::DiscoveredConnection> = all_connections + .iter() + .filter(|c| c.discovered_at >= dream_start) + .collect(); let mut connections_persisted = 0u64; let now = Utc::now(); - for conn in new_connections { + for conn in new_connections.iter() { let link_type = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => "semantic", vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts", diff --git a/crates/vestige-mcp/src/tools/dream.rs b/crates/vestige-mcp/src/tools/dream.rs index 253a367..ac9c479 100644 --- a/crates/vestige-mcp/src/tools/dream.rs +++ b/crates/vestige-mcp/src/tools/dream.rs @@ -89,7 +89,12 @@ pub async fn execute( }).collect(); let cog = cognitive.lock().await; - let pre_dream_count = cog.dreamer.get_connections().len(); + // Capture start time before the dream so we can identify newly discovered + // connections by timestamp rather than by buffer position. This is robust + // against the composite-score eviction sort in store_connections, which + // reorders the buffer and makes positional slicing (pre_dream_count..) + // unreliable. + let dream_start = Utc::now(); let dream_result = cog.dreamer.dream(&dream_memories).await; let insights = cog.dreamer.synthesize_insights(&dream_memories); let all_connections = cog.dreamer.get_connections(); @@ -115,12 +120,17 @@ pub async fn execute( } } - // v1.9.0: Persist only NEW connections from this dream (skip accumulated ones) - let new_connections = all_connections.get(pre_dream_count..).unwrap_or(&[]); + // Identify new connections from this dream by timestamp rather than buffer + // position — positional slicing is broken after composite-score eviction + // reorders the buffer. + let new_connections: Vec<&vestige_core::DiscoveredConnection> = all_connections + .iter() + .filter(|c| c.discovered_at >= dream_start) + .collect(); let mut connections_persisted = 0u64; { let now = Utc::now(); - for conn in new_connections { + for conn in new_connections.iter() { let link_type = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => "semantic", vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts", @@ -162,7 +172,7 @@ pub async fn execute( // Hydrate live cognitive engine with newly persisted connections if connections_persisted > 0 { let mut cog = cognitive.lock().await; - for conn in new_connections { + for conn in new_connections.iter() { let link_type_enum = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => LinkType::Semantic, vestige_core::DiscoveredConnectionType::SharedConcept => LinkType::Semantic,