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

@ -118,12 +118,11 @@ pub fn system_status_schema() -> Value {
/// Returns system health status, full statistics, FSRS preview,
/// cognitive module health, state distribution, and actionable recommendations.
pub async fn execute_system_status(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
cognitive: &Arc<Mutex<CognitiveEngine>>,
_args: Option<Value>,
) -> Result<Value, String> {
let storage_guard = storage.lock().await;
let stats = storage_guard.get_stats().map_err(|e| e.to_string())?;
let stats = storage.get_stats().map_err(|e| e.to_string())?;
// === Health assessment ===
let status = if stats.total_nodes == 0 {
@ -142,7 +141,7 @@ pub async fn execute_system_status(
0.0
};
let embedding_ready = storage_guard.is_embedding_ready();
let embedding_ready = storage.is_embedding_ready();
let mut warnings = Vec::new();
if stats.average_retention < 0.5 && stats.total_nodes > 0 {
@ -176,7 +175,7 @@ pub async fn execute_system_status(
}
// === State distribution ===
let nodes = storage_guard.get_all_nodes(500, 0).map_err(|e| e.to_string())?;
let nodes = storage.get_all_nodes(500, 0).map_err(|e| e.to_string())?;
let total = nodes.len();
let (active, dormant, silent, unavailable) = if total > 0 {
let mut a = 0usize;
@ -246,15 +245,14 @@ pub async fn execute_system_status(
};
// === Automation triggers (for conditional dream/backup/gc at session start) ===
let last_consolidation = storage_guard.get_last_consolidation().ok().flatten();
let last_dream = storage_guard.get_last_dream().ok().flatten();
let last_consolidation = storage.get_last_consolidation().ok().flatten();
let last_dream = storage.get_last_dream().ok().flatten();
let saves_since_last_dream = match &last_dream {
Some(dt) => storage_guard.count_memories_since(*dt).unwrap_or(0),
Some(dt) => storage.count_memories_since(*dt).unwrap_or(0),
None => stats.total_nodes as i64,
};
let last_backup = Storage::get_last_backup_timestamp();
drop(storage_guard);
Ok(serde_json::json!({
"tool": "system_status",
@ -299,10 +297,9 @@ pub async fn execute_system_status(
/// Health check tool — deprecated in v1.7, use execute_system_status() instead
#[allow(dead_code)]
pub async fn execute_health_check(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
_args: Option<Value>,
) -> Result<Value, String> {
let storage = storage.lock().await;
let stats = storage.get_stats().map_err(|e| e.to_string())?;
let status = if stats.total_nodes == 0 {
@ -369,10 +366,9 @@ pub async fn execute_health_check(
/// Consolidate tool
pub async fn execute_consolidate(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
_args: Option<Value>,
) -> Result<Value, String> {
let mut storage = storage.lock().await;
let result = storage.run_consolidation().map_err(|e| e.to_string())?;
Ok(serde_json::json!({
@ -392,15 +388,14 @@ pub async fn execute_consolidate(
/// Stats tool — deprecated in v1.7, use execute_system_status() instead
#[allow(dead_code)]
pub async fn execute_stats(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
cognitive: &Arc<Mutex<CognitiveEngine>>,
_args: Option<Value>,
) -> Result<Value, String> {
let storage_guard = storage.lock().await;
let stats = storage_guard.get_stats().map_err(|e| e.to_string())?;
let stats = storage.get_stats().map_err(|e| e.to_string())?;
// Compute state distribution from a sample of nodes
let nodes = storage_guard.get_all_nodes(500, 0).map_err(|e| e.to_string())?;
let nodes = storage.get_all_nodes(500, 0).map_err(|e| e.to_string())?;
let total = nodes.len();
let (active, dormant, silent, unavailable) = if total > 0 {
let mut a = 0usize;
@ -543,7 +538,6 @@ pub async fn execute_stats(
} else {
None
};
drop(storage_guard);
Ok(serde_json::json!({
"tool": "stats",
@ -573,7 +567,7 @@ pub async fn execute_stats(
/// Backup tool
pub async fn execute_backup(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
_args: Option<Value>,
) -> Result<Value, String> {
// Determine backup path
@ -591,7 +585,6 @@ pub async fn execute_backup(
// Use VACUUM INTO for a consistent backup (handles WAL properly)
{
let storage = storage.lock().await;
storage.backup_to(&backup_path)
.map_err(|e| format!("Failed to create backup: {}", e))?;
}
@ -619,7 +612,7 @@ struct ExportArgs {
/// Export tool
pub async fn execute_export(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
args: Option<Value>,
) -> Result<Value, String> {
let args: ExportArgs = match args {
@ -650,7 +643,6 @@ pub async fn execute_export(
let tag_filter: Vec<String> = args.tags.unwrap_or_default();
// Fetch all nodes (capped at 100K to prevent OOM)
let storage = storage.lock().await;
let mut all_nodes = Vec::new();
let page_size = 500;
let max_nodes = 100_000;
@ -755,7 +747,7 @@ struct GcArgs {
/// Garbage collection tool
pub async fn execute_gc(
storage: &Arc<Mutex<Storage>>,
storage: &Arc<Storage>,
args: Option<Value>,
) -> Result<Value, String> {
let args: GcArgs = match args {
@ -771,7 +763,6 @@ pub async fn execute_gc(
let max_age_days = args.max_age_days;
let dry_run = args.dry_run.unwrap_or(true); // Default to dry_run for safety
let mut storage = storage.lock().await;
let now = Utc::now();
// Fetch all nodes (capped at 100K to prevent OOM)
@ -883,10 +874,10 @@ 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)
}
#[test]
@ -912,8 +903,7 @@ mod tests {
async fn test_system_status_with_memories() {
let (storage, _dir) = test_storage().await;
{
let mut s = storage.lock().await;
s.ingest(vestige_core::IngestInput {
storage.ingest(vestige_core::IngestInput {
content: "Test memory for status".to_string(),
node_type: "fact".to_string(),
source: None,
@ -961,9 +951,8 @@ mod tests {
async fn test_system_status_automation_triggers_with_memories() {
let (storage, _dir) = test_storage().await;
{
let mut s = storage.lock().await;
for i in 0..3 {
s.ingest(vestige_core::IngestInput {
storage.ingest(vestige_core::IngestInput {
content: format!("Automation trigger test memory {}", i),
node_type: "fact".to_string(),
source: None,