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

@ -55,7 +55,7 @@ struct IngestArgs {
}
pub async fn execute(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
cognitive: &Arc<Mutex<CognitiveEngine>>,
args: Option<Value>,
) -> Result<Value, String> {
@ -123,20 +123,18 @@ pub async fn execute(
// ====================================================================
// INGEST (storage lock)
// ====================================================================
let mut storage_guard = storage.lock().await;
// Route through smart_ingest when embeddings are available to prevent duplicates.
// Falls back to raw ingest only when embeddings aren't ready.
#[cfg(all(feature = "embeddings", feature = "vector-search"))]
{
let fallback_input = input.clone();
match storage_guard.smart_ingest(input) {
match storage.smart_ingest(input) {
Ok(result) => {
let node_id = result.node.id.clone();
let node_content = result.node.content.clone();
let node_type = result.node.node_type.clone();
let has_embedding = result.node.has_embedding.unwrap_or(false);
drop(storage_guard);
run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite);
@ -153,12 +151,11 @@ pub async fn execute(
}))
}
Err(_) => {
let node = storage_guard.ingest(fallback_input).map_err(|e| e.to_string())?;
let node = storage.ingest(fallback_input).map_err(|e| e.to_string())?;
let node_id = node.id.clone();
let node_content = node.content.clone();
let node_type = node.node_type.clone();
let has_embedding = node.has_embedding.unwrap_or(false);
drop(storage_guard);
run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite);
@ -178,12 +175,11 @@ pub async fn execute(
// Fallback for builds without embedding features
#[cfg(not(all(feature = "embeddings", feature = "vector-search")))]
{
let node = storage_guard.ingest(input).map_err(|e| e.to_string())?;
let node = storage.ingest(input).map_err(|e| e.to_string())?;
let node_id = node.id.clone();
let node_content = node.content.clone();
let node_type = node.node_type.clone();
let has_embedding = node.has_embedding.unwrap_or(false);
drop(storage_guard);
run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite);
@ -249,10 +245,10 @@ mod tests {
}
/// Create a test storage instance with a temporary database
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)
}
// ========================================================================
@ -412,8 +408,7 @@ mod tests {
// Verify node was created - the default type is "fact"
let node_id = result.unwrap()["nodeId"].as_str().unwrap().to_string();
let storage_lock = storage.lock().await;
let node = storage_lock.get_node(&node_id).unwrap().unwrap();
let node = storage.get_node(&node_id).unwrap().unwrap();
assert_eq!(node.node_type, "fact");
}