feat: Vestige v1.3.0 — importance scoring, session checkpoints, duplicate detection

3 new MCP tools (16 → 19 total):
- importance_score: 4-channel neuroscience importance scoring (novelty/arousal/reward/attention)
- session_checkpoint: batch smart_ingest up to 20 items with PE Gating
- find_duplicates: cosine similarity clustering with union-find for dedup

CLI: vestige ingest command for memory ingestion via command line
Core: made get_node_embedding public, added get_all_embeddings for dedup scanning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-02-12 05:02:09 -06:00
parent 5cca386d6b
commit 04a3062328
9 changed files with 848 additions and 5 deletions

View file

@ -94,6 +94,21 @@ enum Commands {
#[arg(long)]
no_open: bool,
},
/// Ingest a memory (routes through Prediction Error Gating)
Ingest {
/// Content to remember
content: String,
/// Tags (comma-separated)
#[arg(long)]
tags: Option<String>,
/// Node type (fact, concept, event, person, place, note, pattern, decision)
#[arg(long, default_value = "fact")]
node_type: String,
/// Source reference
#[arg(long)]
source: Option<String>,
},
}
fn main() -> anyhow::Result<()> {
@ -118,6 +133,12 @@ fn main() -> anyhow::Result<()> {
yes,
} => run_gc(min_retention, max_age_days, dry_run, yes),
Commands::Dashboard { port, no_open } => run_dashboard(port, !no_open),
Commands::Ingest {
content,
tags,
node_type,
source,
} => run_ingest(content, tags, node_type, source),
}
}
@ -842,6 +863,83 @@ fn run_gc(
Ok(())
}
/// Ingest a memory via CLI (routes through smart_ingest / PE Gating)
fn run_ingest(
content: String,
tags: Option<String>,
node_type: String,
source: Option<String>,
) -> anyhow::Result<()> {
if content.trim().is_empty() {
anyhow::bail!("Content cannot be empty");
}
let tag_list: Vec<String> = tags
.as_deref()
.map(|t| {
t.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default();
let input = IngestInput {
content: content.clone(),
node_type,
source,
sentiment_score: 0.0,
sentiment_magnitude: 0.0,
tags: tag_list,
valid_from: None,
valid_until: None,
};
let mut storage = Storage::new(None)?;
// Try smart_ingest (PE Gating) if available, otherwise regular ingest
#[cfg(all(feature = "embeddings", feature = "vector-search"))]
{
let result = storage.smart_ingest(input)?;
println!("{}", "=== Vestige Ingest ===".cyan().bold());
println!();
println!("{}: {}", "Decision".white().bold(), result.decision.green());
println!("{}: {}", "Node ID".white().bold(), result.node.id);
if let Some(sim) = result.similarity {
println!("{}: {:.3}", "Similarity".white().bold(), sim);
}
if let Some(pe) = result.prediction_error {
println!("{}: {:.3}", "Prediction Error".white().bold(), pe);
}
println!("{}: {}", "Reason".white().bold(), result.reason);
println!();
println!(
"{}",
format!("Memory {} ({})", result.decision, truncate(&content, 60))
.green()
.bold()
);
}
#[cfg(not(all(feature = "embeddings", feature = "vector-search")))]
{
let node = storage.ingest(input)?;
println!("{}", "=== Vestige Ingest ===".cyan().bold());
println!();
println!("{}: create", "Decision".white().bold());
println!("{}: {}", "Node ID".white().bold(), node.id);
println!();
println!(
"{}",
format!("Memory created ({})", truncate(&content, 60))
.green()
.bold()
);
}
Ok(())
}
/// Run the dashboard web server
fn run_dashboard(port: u16, open_browser: bool) -> anyhow::Result<()> {
println!("{}", "=== Vestige Dashboard ===".cyan().bold());