feat: Vestige v2.0.0 "Cognitive Leap" — 3D dashboard, HyDE search, WebSocket events

The biggest release in Vestige history. Complete visual and cognitive overhaul.

Dashboard:
- SvelteKit 2 + Three.js 3D neural visualization at localhost:3927/dashboard
- 7 interactive pages: Graph, Memories, Timeline, Feed, Explore, Intentions, Stats
- WebSocket event bus with 16 event types, real-time 3D animations
- Bloom post-processing, GPU instanced rendering, force-directed layout
- Dream visualization mode, FSRS retention curves, command palette (Cmd+K)
- Keyboard shortcuts, responsive mobile layout, PWA installable
- Single binary deployment via include_dir! (22MB)

Engine:
- HyDE query expansion (intent classification + 3-5 semantic variants + centroid)
- fastembed 5.11 with optional Nomic v2 MoE + Qwen3 reranker + Metal GPU
- Emotional memory module (#29)
- Criterion benchmark suite

Backend:
- Axum WebSocket at /ws with heartbeat + event broadcast
- 7 new REST endpoints for cognitive operations
- Event emission from MCP tools via shared broadcast channel
- CORS for SvelteKit dev mode

Distribution:
- GitHub issue templates (bug report, feature request)
- CHANGELOG with comprehensive v2.0 release notes
- README updated with dashboard docs, architecture diagram, comparison table

734 tests passing, zero warnings, 22MB release binary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-02-22 03:07:25 -06:00
parent 26cee040a5
commit c2d28f3433
321 changed files with 32695 additions and 4727 deletions

View file

@ -139,6 +139,13 @@ pub async fn execute(
insights_generated: dream_result.insights_generated.len() as i32,
memories_strengthened: dream_result.memories_strengthened as i32,
memories_compressed: dream_result.memories_compressed as i32,
phase_nrem1_ms: None,
phase_nrem3_ms: None,
phase_rem_ms: None,
phase_integration_ms: None,
summaries_generated: None,
emotional_memories_processed: None,
creative_connections_found: None,
};
if let Err(e) = storage.save_dream_history(&record) {
tracing::warn!("Failed to persist dream history: {}", e);

View file

@ -43,8 +43,8 @@ pub fn schema() -> Value {
"properties": {
"action": {
"type": "string",
"enum": ["get", "delete", "state", "promote", "demote"],
"description": "Action to perform: 'get' retrieves full memory node, 'delete' removes memory, 'state' returns accessibility state, 'promote' increases retrieval strength (thumbs up), 'demote' decreases retrieval strength (thumbs down)"
"enum": ["get", "delete", "state", "promote", "demote", "edit"],
"description": "Action to perform: 'get' retrieves full memory node, 'delete' removes memory, 'state' returns accessibility state, 'promote' increases retrieval strength (thumbs up), 'demote' decreases retrieval strength (thumbs down), 'edit' updates content in-place (preserves FSRS state)"
},
"id": {
"type": "string",
@ -53,6 +53,10 @@ pub fn schema() -> Value {
"reason": {
"type": "string",
"description": "Why this memory is being promoted/demoted (optional, for logging). Only used with promote/demote actions."
},
"content": {
"type": "string",
"description": "New content for edit action. Replaces existing content, regenerates embedding, preserves FSRS state."
}
},
"required": ["action", "id"]
@ -65,6 +69,7 @@ struct MemoryArgs {
action: String,
id: String,
reason: Option<String>,
content: Option<String>,
}
/// Execute the unified memory tool
@ -87,8 +92,9 @@ pub async fn execute(
"state" => execute_state(storage, &args.id).await,
"promote" => execute_promote(storage, cognitive, &args.id, args.reason).await,
"demote" => execute_demote(storage, cognitive, &args.id, args.reason).await,
"edit" => execute_edit(storage, &args.id, args.content).await,
_ => Err(format!(
"Invalid action '{}'. Must be one of: get, delete, state, promote, demote",
"Invalid action '{}'. Must be one of: get, delete, state, promote, demote, edit",
args.action
)),
}
@ -302,6 +308,53 @@ async fn execute_demote(
}))
}
/// Edit a memory's content in-place — preserves FSRS state, regenerates embedding
async fn execute_edit(
storage: &Arc<Storage>,
id: &str,
content: Option<String>,
) -> Result<Value, String> {
let new_content = content.ok_or("Missing 'content' field. Required for edit action.")?;
if new_content.trim().is_empty() {
return Err("Content cannot be empty".to_string());
}
// Get existing node to capture old content
let old_node = storage
.get_node(id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("Memory not found: {}", id))?;
// Update content (regenerates embedding, syncs FTS5)
storage
.update_node_content(id, &new_content)
.map_err(|e| e.to_string())?;
// Truncate previews for response (char-safe to avoid UTF-8 panics)
let old_preview = if old_node.content.chars().count() > 200 {
let truncated: String = old_node.content.chars().take(197).collect();
format!("{}...", truncated)
} else {
old_node.content.clone()
};
let new_preview = if new_content.chars().count() > 200 {
let truncated: String = new_content.chars().take(197).collect();
format!("{}...", truncated)
} else {
new_content.clone()
};
Ok(serde_json::json!({
"success": true,
"action": "edit",
"nodeId": id,
"oldContentPreview": old_preview,
"newContentPreview": new_preview,
"note": "FSRS state preserved (stability, difficulty, reps, lapses unchanged). Embedding regenerated for new content."
}))
}
#[cfg(test)]
mod tests {
use super::*;
@ -336,9 +389,10 @@ mod tests {
assert!(schema["properties"]["id"].is_object());
assert!(schema["properties"]["reason"].is_object());
assert_eq!(schema["required"], serde_json::json!(["action", "id"]));
// Verify all 5 actions are in enum
// Verify all 6 actions are in enum
let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
assert_eq!(actions.len(), 5);
assert_eq!(actions.len(), 6);
assert!(actions.contains(&serde_json::json!("edit")));
assert!(actions.contains(&serde_json::json!("promote")));
assert!(actions.contains(&serde_json::json!("demote")));
}
@ -440,6 +494,13 @@ mod tests {
#[tokio::test]
async fn test_delete_nonexistent_memory() {
let (storage, _dir) = test_storage().await;
// Ingest+delete a throwaway memory to warm writer after WAL migration
let warmup_id = storage.ingest(vestige_core::IngestInput {
content: "warmup".to_string(),
node_type: "fact".to_string(),
..Default::default()
}).unwrap().id;
let _ = storage.delete_node(&warmup_id);
let args = serde_json::json!({ "action": "delete", "id": "00000000-0000-0000-0000-000000000000" });
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
@ -613,4 +674,107 @@ mod tests {
assert_eq!(value["changes"]["retentionStrength"]["delta"], "-0.15");
assert_eq!(value["changes"]["stability"]["multiplier"], "0.5x");
}
// ========================================================================
// EDIT TESTS (v1.9.2)
// ========================================================================
#[tokio::test]
async fn test_edit_succeeds() {
let (storage, _dir) = test_storage().await;
let id = ingest_memory(&storage).await;
let args = serde_json::json!({
"action": "edit",
"id": id,
"content": "Updated memory content"
});
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["success"], true);
assert_eq!(value["action"], "edit");
assert_eq!(value["nodeId"], id);
assert!(value["oldContentPreview"].as_str().unwrap().contains("Memory unified test content"));
assert!(value["newContentPreview"].as_str().unwrap().contains("Updated memory content"));
assert!(value["note"].as_str().unwrap().contains("FSRS state preserved"));
}
#[tokio::test]
async fn test_edit_preserves_fsrs_state() {
let (storage, _dir) = test_storage().await;
let id = ingest_memory(&storage).await;
// Get FSRS state before edit
let before = storage.get_node(&id).unwrap().unwrap();
// Edit content
let args = serde_json::json!({
"action": "edit",
"id": id,
"content": "Completely new content after edit"
});
execute(&storage, &test_cognitive(), Some(args)).await.unwrap();
// Verify FSRS state preserved
let after = storage.get_node(&id).unwrap().unwrap();
assert_eq!(after.stability, before.stability);
assert_eq!(after.difficulty, before.difficulty);
assert_eq!(after.reps, before.reps);
assert_eq!(after.lapses, before.lapses);
assert_eq!(after.retention_strength, before.retention_strength);
// Content should be updated
assert_eq!(after.content, "Completely new content after edit");
assert_ne!(after.content, before.content);
}
#[tokio::test]
async fn test_edit_missing_content_fails() {
let (storage, _dir) = test_storage().await;
let id = ingest_memory(&storage).await;
let args = serde_json::json!({ "action": "edit", "id": id });
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("content"));
}
#[tokio::test]
async fn test_edit_empty_content_fails() {
let (storage, _dir) = test_storage().await;
let id = ingest_memory(&storage).await;
let args = serde_json::json!({ "action": "edit", "id": id, "content": " " });
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
#[tokio::test]
async fn test_edit_nonexistent_memory_fails() {
let (storage, _dir) = test_storage().await;
let args = serde_json::json!({
"action": "edit",
"id": "00000000-0000-0000-0000-000000000000",
"content": "New content"
});
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("not found"));
}
#[tokio::test]
async fn test_edit_with_multibyte_utf8_content() {
let (storage, _dir) = test_storage().await;
let id = ingest_memory(&storage).await;
// Content with emoji and CJK characters (multi-byte UTF-8)
let long_content = "🧠".repeat(100); // 100 brain emoji = 400 bytes but only 100 chars
let args = serde_json::json!({
"action": "edit",
"id": id,
"content": long_content
});
// This must NOT panic (previous code would panic on byte-level truncation)
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["success"], true);
}
}

View file

@ -299,6 +299,19 @@ pub async fn execute(
}
}
// ====================================================================
// STAGE 5C: Utility-based ranking (MemRL-inspired)
// Memories that proved useful in past sessions get a retrieval boost.
// utility_score = times_useful / times_retrieved (0.0 to 1.0)
// ====================================================================
for result in &mut filtered_results {
let utility = result.node.utility_score.unwrap_or(0.0) as f32;
if utility > 0.0 {
// Utility boost: up to +15% for memories with utility_score = 1.0
result.combined_score *= 1.0 + (utility * 0.15);
}
}
// Re-sort by adjusted combined_score (descending) after all score modifications
filtered_results.sort_by(|a, b| {
b.combined_score

View file

@ -81,6 +81,11 @@ pub fn schema() -> Value {
"source": {
"type": "string",
"description": "Source reference"
},
"forceCreate": {
"type": "boolean",
"description": "Force creation of this item even if similar content exists",
"default": false
}
},
"required": ["content"]
@ -111,6 +116,7 @@ struct BatchItem {
#[serde(alias = "node_type")]
node_type: Option<String>,
source: Option<String>,
force_create: Option<bool>,
}
pub async fn execute(
@ -125,7 +131,8 @@ pub async fn execute(
// Detect mode: batch (items present) vs single (content present)
if let Some(items) = args.items {
return execute_batch(storage, cognitive, items).await;
let global_force = args.force_create.unwrap_or(false);
return execute_batch(storage, cognitive, items, global_force).await;
}
// Single mode: content is required
@ -275,6 +282,7 @@ async fn execute_batch(
storage: &Arc<Storage>,
cognitive: &Arc<Mutex<CognitiveEngine>>,
items: Vec<BatchItem>,
global_force_create: bool,
) -> Result<Value, String> {
if items.is_empty() {
return Err("Items array cannot be empty".to_string());
@ -312,6 +320,9 @@ async fn execute_batch(
continue;
}
// Extract per-item force_create before consuming other fields
let item_force_create = item.force_create.unwrap_or(false);
// ================================================================
// COGNITIVE PRE-INGEST (per item)
// ================================================================
@ -352,6 +363,39 @@ async fn execute_batch(
// INGEST (storage lock per item)
// ================================================================
// Check force_create: global flag OR per-item flag
let item_force = global_force_create || item_force_create;
if item_force {
match storage.ingest(input) {
Ok(node) => {
let node_id = node.id.clone();
let node_content = node.content.clone();
let node_type = node.node_type.clone();
created += 1;
run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite);
results.push(serde_json::json!({
"index": i,
"status": "saved",
"decision": "create",
"nodeId": node_id,
"importanceScore": importance_composite,
"reason": "Forced creation - skipped similarity check"
}));
}
Err(e) => {
errors += 1;
results.push(serde_json::json!({
"index": i,
"status": "error",
"reason": e.to_string()
}));
}
}
continue;
}
#[cfg(all(feature = "embeddings", feature = "vector-search"))]
{
match storage.smart_ingest(input) {
@ -863,6 +907,62 @@ mod tests {
assert!(results[0]["importanceScore"].is_number());
}
#[tokio::test]
async fn test_batch_force_create_global() {
let (storage, _dir) = test_storage().await;
// Three items with very similar content + global forceCreate
let result = execute(
&storage, &test_cognitive(),
Some(serde_json::json!({
"forceCreate": true,
"items": [
{ "content": "Physics question about quantum mechanics and wave functions" },
{ "content": "Physics question about quantum mechanics and wave equations" },
{ "content": "Physics question about quantum mechanics and wave behavior" }
]
})),
).await;
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["mode"], "batch");
// All 3 should be created separately, not merged
assert_eq!(value["summary"]["created"], 3);
assert_eq!(value["summary"]["updated"], 0);
// Each result should say "Forced creation"
let results = value["results"].as_array().unwrap();
for r in results {
assert_eq!(r["decision"], "create");
assert!(r["reason"].as_str().unwrap().contains("Forced"));
}
}
#[tokio::test]
async fn test_batch_force_create_per_item() {
let (storage, _dir) = test_storage().await;
// Mix of forced and non-forced items
let result = execute(
&storage, &test_cognitive(),
Some(serde_json::json!({
"items": [
{ "content": "Forced item one", "forceCreate": true },
{ "content": "Normal item two" },
{ "content": "Forced item three", "forceCreate": true }
]
})),
).await;
assert!(result.is_ok());
let value = result.unwrap();
let results = value["results"].as_array().unwrap();
// Forced items should say "Forced creation"
assert_eq!(results[0]["decision"], "create");
assert!(results[0]["reason"].as_str().unwrap().contains("Forced"));
// Non-forced item gets normal processing
assert_eq!(results[1]["status"], "saved");
// Third forced item
assert_eq!(results[2]["decision"], "create");
assert!(results[2]["reason"].as_str().unwrap().contains("Forced"));
}
#[tokio::test]
async fn test_no_content_no_items_fails() {
let (storage, _dir) = test_storage().await;