feat: Vestige v1.9.1 AUTONOMIC — self-regulating memory with graph visualization

Retention Target System: auto-GC low-retention memories during consolidation
(VESTIGE_RETENTION_TARGET env var, default 0.8). Auto-Promote: memories
accessed 3+ times in 24h get frequency-dependent potentiation. Waking SWR
Tagging: promoted memories get preferential 70/30 dream replay. Improved
Consolidation Scheduler: triggers on 6h staleness or 2h active use.

New tools: memory_health (retention dashboard with distribution buckets,
trend tracking, recommendations) and memory_graph (subgraph export with
Fruchterman-Reingold force-directed layout, up to 200 nodes).

Dream connections now persist to database via save_connection(), enabling
memory_graph traversal. Schema Migration V8 adds waking_tag, utility_score,
times_retrieved/useful columns and retention_snapshots table. 21 MCP tools.

v1.9.1 fixes: ConnectionRecord export, UTF-8 safe truncation, link_type
normalization, utility_score clamping, only-new-connections persistence,
70/30 split capacity fill, nonexistent center_id error handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-02-21 02:02:06 -06:00
parent c29023dd80
commit 5b90a73055
62 changed files with 2922 additions and 931 deletions

View file

@ -22,7 +22,7 @@ pub fn schema() -> serde_json::Value {
}
pub async fn execute(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
cognitive: &Arc<Mutex<CognitiveEngine>>,
args: Option<serde_json::Value>,
) -> Result<serde_json::Value, String> {
@ -32,10 +32,42 @@ pub async fn execute(
.and_then(|v| v.as_u64())
.unwrap_or(50) as usize;
let storage_guard = storage.lock().await;
let all_nodes = storage_guard.get_all_nodes(memory_count as i32, 0)
// v1.9.0: Waking SWR tagging — preferential replay of tagged memories (70/30 split)
let tagged_nodes = storage.get_waking_tagged_memories(memory_count as i32)
.unwrap_or_default();
let tagged_count = tagged_nodes.len();
// Calculate how many tagged vs random to include
let tagged_target = (memory_count * 7 / 10).min(tagged_count); // 70% tagged
let _random_target = memory_count.saturating_sub(tagged_target); // 30% random (used for logging)
// Build the dream memory set: tagged memories first, then fill with random
let tagged_ids: std::collections::HashSet<String> = tagged_nodes.iter()
.take(tagged_target)
.map(|n| n.id.clone())
.collect();
let random_nodes = storage.get_all_nodes(memory_count as i32, 0)
.map_err(|e| format!("Failed to load memories: {}", e))?;
let mut all_nodes: Vec<_> = tagged_nodes.into_iter().take(tagged_target).collect();
for node in random_nodes {
if !tagged_ids.contains(&node.id) && all_nodes.len() < memory_count {
all_nodes.push(node);
}
}
// If still under capacity (e.g., all memories are tagged), fill from remaining tagged
if all_nodes.len() < memory_count {
let used_ids: std::collections::HashSet<String> = all_nodes.iter().map(|n| n.id.clone()).collect();
let remaining_tagged = storage.get_waking_tagged_memories(memory_count as i32)
.unwrap_or_default();
for node in remaining_tagged {
if !used_ids.contains(&node.id) && all_nodes.len() < memory_count {
all_nodes.push(node);
}
}
}
if all_nodes.len() < 5 {
return Ok(serde_json::json!({
"status": "insufficient_memories",
@ -48,23 +80,57 @@ pub async fn execute(
vestige_core::DreamMemory {
id: n.id.clone(),
content: n.content.clone(),
embedding: storage_guard.get_node_embedding(&n.id).ok().flatten(),
embedding: storage.get_node_embedding(&n.id).ok().flatten(),
tags: n.tags.clone(),
created_at: n.created_at,
access_count: n.reps as u32,
}
}).collect();
// Drop storage lock before taking cognitive lock (strict ordering)
drop(storage_guard);
let cog = cognitive.lock().await;
let pre_dream_count = cog.dreamer.get_connections().len();
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);
// v1.9.0: Persist only NEW connections from this dream (skip accumulated ones)
let new_connections = &all_connections[pre_dream_count..];
let mut connections_persisted = 0u64;
{
let now = Utc::now();
for conn in new_connections {
let link_type = match conn.connection_type {
vestige_core::DiscoveredConnectionType::Semantic => "semantic",
vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts",
vestige_core::DiscoveredConnectionType::Temporal => "temporal",
vestige_core::DiscoveredConnectionType::Complementary => "complementary",
vestige_core::DiscoveredConnectionType::CausalChain => "causal",
};
let record = vestige_core::ConnectionRecord {
source_id: conn.from_id.clone(),
target_id: conn.to_id.clone(),
strength: conn.similarity,
link_type: link_type.to_string(),
created_at: now,
last_activated: now,
activation_count: 1,
};
if storage.save_connection(&record).is_ok() {
connections_persisted += 1;
}
}
if connections_persisted > 0 {
tracing::info!(
connections_persisted = connections_persisted,
"Dream: persisted {} connections to database",
connections_persisted
);
}
}
// Persist dream history (non-fatal on failure — dream still happened)
{
let mut storage_guard = storage.lock().await;
let record = DreamHistoryRecord {
dreamed_at: Utc::now(),
duration_ms: dream_result.duration_ms as i64,
@ -74,14 +140,19 @@ pub async fn execute(
memories_strengthened: dream_result.memories_strengthened as i32,
memories_compressed: dream_result.memories_compressed as i32,
};
if let Err(e) = storage_guard.save_dream_history(&record) {
if let Err(e) = storage.save_dream_history(&record) {
tracing::warn!("Failed to persist dream history: {}", e);
}
}
// v1.9.0: Clear waking tags after dream processes them
let tags_cleared = storage.clear_waking_tags().unwrap_or(0);
Ok(serde_json::json!({
"status": "dreamed",
"memoriesReplayed": dream_memories.len(),
"wakingTagsProcessed": tagged_target,
"wakingTagsCleared": tags_cleared,
"insights": insights.iter().map(|i| serde_json::json!({
"insight_type": format!("{:?}", i.insight_type),
"insight": i.insight,
@ -89,8 +160,10 @@ pub async fn execute(
"confidence": i.confidence,
"novelty_score": i.novelty_score,
})).collect::<Vec<_>>(),
"connectionsPersisted": connections_persisted,
"stats": {
"new_connections_found": dream_result.new_connections_found,
"connections_persisted": connections_persisted,
"memories_strengthened": dream_result.memories_strengthened,
"memories_compressed": dream_result.memories_compressed,
"insights_generated": dream_result.insights_generated.len(),
@ -109,16 +182,15 @@ mod tests {
Arc::new(Mutex::new(CognitiveEngine::new()))
}
async fn test_storage() -> (Arc<Mutex<Storage>>, TempDir) {
async fn test_storage() -> (Arc<Storage>, TempDir) {
let dir = TempDir::new().unwrap();
let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap();
(Arc::new(Mutex::new(storage)), dir)
(Arc::new(storage), dir)
}
async fn ingest_n_memories(storage: &Arc<Mutex<Storage>>, n: usize) {
let mut s = storage.lock().await;
async fn ingest_n_memories(storage: &Arc<Storage>, n: usize) {
for i in 0..n {
s.ingest(vestige_core::IngestInput {
storage.ingest(vestige_core::IngestInput {
content: format!("Dream test memory number {}", i),
node_type: "fact".to_string(),
source: None,
@ -216,8 +288,7 @@ mod tests {
// Before dream: no dream history
{
let s = storage.lock().await;
assert!(s.get_last_dream().unwrap().is_none());
assert!(storage.get_last_dream().unwrap().is_none());
}
let result = execute(&storage, &test_cognitive(), None).await;
@ -227,8 +298,7 @@ mod tests {
// After dream: dream history should exist
{
let s = storage.lock().await;
let last = s.get_last_dream().unwrap();
let last = storage.get_last_dream().unwrap();
assert!(last.is_some(), "Dream should have been persisted to database");
}
}