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

@ -6,10 +6,12 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tokio::sync::Mutex;
use chrono::Utc;
use tokio::sync::{broadcast, Mutex};
use tracing::{debug, info, warn};
use crate::cognitive::CognitiveEngine;
use vestige_mcp::dashboard::events::VestigeEvent;
use crate::protocol::messages::{
CallToolRequest, CallToolResult, InitializeRequest, InitializeResult,
ListResourcesResult, ListToolsResult, ReadResourceRequest, ReadResourceResult,
@ -27,15 +29,41 @@ pub struct McpServer {
initialized: bool,
/// Tool call counter for inline consolidation trigger (every 100 calls)
tool_call_count: AtomicU64,
/// Optional event broadcast channel for dashboard real-time updates.
event_tx: Option<broadcast::Sender<VestigeEvent>>,
}
impl McpServer {
#[allow(dead_code)]
pub fn new(storage: Arc<Storage>, cognitive: Arc<Mutex<CognitiveEngine>>) -> Self {
Self {
storage,
cognitive,
initialized: false,
tool_call_count: AtomicU64::new(0),
event_tx: None,
}
}
/// Create an MCP server that broadcasts events to the dashboard.
pub fn new_with_events(
storage: Arc<Storage>,
cognitive: Arc<Mutex<CognitiveEngine>>,
event_tx: broadcast::Sender<VestigeEvent>,
) -> Self {
Self {
storage,
cognitive,
initialized: false,
tool_call_count: AtomicU64::new(0),
event_tx: Some(event_tx),
}
}
/// Emit an event to the dashboard (no-op if no event channel).
fn emit(&self, event: VestigeEvent) {
if let Some(ref tx) = self.event_tx {
let _ = tx.send(event);
}
}
@ -143,7 +171,7 @@ impl McpServer {
},
ToolDescription {
name: "memory".to_string(),
description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'delete' (remove memory), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete).".to_string()),
description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'delete' (remove memory), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete), 'edit' (update content in-place, preserves FSRS state).".to_string()),
input_schema: tools::memory_unified::schema(),
},
ToolDescription {
@ -287,6 +315,9 @@ impl McpServer {
cog.consolidation_scheduler.record_activity();
}
// Save args for event emission (tool dispatch consumes request.arguments)
let saved_args = if self.event_tx.is_some() { request.arguments.clone() } else { None };
let result = match request.name.as_str() {
// ================================================================
// UNIFIED TOOLS (v1.1+) - Preferred API
@ -611,6 +642,14 @@ impl McpServer {
}
};
// ================================================================
// DASHBOARD EVENT EMISSION (v2.0)
// Emit real-time events to WebSocket clients after successful tool calls.
// ================================================================
if let Ok(ref content) = result {
self.emit_tool_event(&request.name, &saved_args, content);
}
let response = match result {
Ok(content) => {
let call_result = CallToolResult {
@ -784,6 +823,196 @@ impl McpServer {
Err(e) => Err(JsonRpcError::internal_error(&e)),
}
}
/// Extract event data from tool results and emit to dashboard.
fn emit_tool_event(
&self,
tool_name: &str,
args: &Option<serde_json::Value>,
result: &serde_json::Value,
) {
if self.event_tx.is_none() {
return;
}
let now = Utc::now();
match tool_name {
// -- smart_ingest: memory created/updated --
"smart_ingest" | "ingest" | "session_checkpoint" => {
// Single mode: result has "action" (created/updated/superseded/reinforced)
if let Some(action) = result.get("action").and_then(|a| a.as_str()) {
let id = result.get("nodeId").or(result.get("id"))
.and_then(|v| v.as_str()).unwrap_or("").to_string();
let preview = result.get("contentPreview").or(result.get("content"))
.and_then(|v| v.as_str()).unwrap_or("").to_string();
match action {
"created" => {
let node_type = result.get("nodeType")
.and_then(|v| v.as_str()).unwrap_or("fact").to_string();
let tags = result.get("tags")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|t| t.as_str().map(String::from)).collect())
.unwrap_or_default();
self.emit(VestigeEvent::MemoryCreated {
id, content_preview: preview, node_type, tags, timestamp: now,
});
}
"updated" | "superseded" | "reinforced" => {
self.emit(VestigeEvent::MemoryUpdated {
id, content_preview: preview, field: action.to_string(), timestamp: now,
});
}
_ => {}
}
}
// Batch mode: result has "results" array
if let Some(results) = result.get("results").and_then(|r| r.as_array()) {
for item in results {
let action = item.get("action").and_then(|a| a.as_str()).unwrap_or("");
let id = item.get("nodeId").or(item.get("id"))
.and_then(|v| v.as_str()).unwrap_or("").to_string();
let preview = item.get("contentPreview")
.and_then(|v| v.as_str()).unwrap_or("").to_string();
if action == "created" {
self.emit(VestigeEvent::MemoryCreated {
id, content_preview: preview,
node_type: "fact".to_string(), tags: vec![], timestamp: now,
});
} else if !action.is_empty() {
self.emit(VestigeEvent::MemoryUpdated {
id, content_preview: preview,
field: action.to_string(), timestamp: now,
});
}
}
}
}
// -- memory: get/delete/promote/demote --
"memory" | "promote_memory" | "demote_memory" | "delete_knowledge" | "get_memory_state" => {
let action = args.as_ref()
.and_then(|a| a.get("action"))
.and_then(|a| a.as_str())
.unwrap_or(if tool_name == "promote_memory" { "promote" }
else if tool_name == "demote_memory" { "demote" }
else if tool_name == "delete_knowledge" { "delete" }
else { "" });
let id = args.as_ref()
.and_then(|a| a.get("id"))
.and_then(|v| v.as_str()).unwrap_or("").to_string();
match action {
"delete" => {
self.emit(VestigeEvent::MemoryDeleted { id, timestamp: now });
}
"promote" => {
let retention = result.get("newRetention")
.or(result.get("retrievalStrength"))
.and_then(|v| v.as_f64()).unwrap_or(0.0);
self.emit(VestigeEvent::MemoryPromoted {
id, new_retention: retention, timestamp: now,
});
}
"demote" => {
let retention = result.get("newRetention")
.or(result.get("retrievalStrength"))
.and_then(|v| v.as_f64()).unwrap_or(0.0);
self.emit(VestigeEvent::MemoryDemoted {
id, new_retention: retention, timestamp: now,
});
}
_ => {}
}
}
// -- search --
"search" | "recall" | "semantic_search" | "hybrid_search" => {
let query = args.as_ref()
.and_then(|a| a.get("query"))
.and_then(|v| v.as_str()).unwrap_or("").to_string();
let results = result.get("results").and_then(|r| r.as_array());
let result_count = results.map(|r| r.len()).unwrap_or(0);
let result_ids: Vec<String> = results
.map(|r| r.iter()
.filter_map(|item| item.get("id").and_then(|v| v.as_str()).map(String::from))
.collect())
.unwrap_or_default();
let duration_ms = result.get("durationMs")
.or(result.get("duration_ms"))
.and_then(|v| v.as_u64()).unwrap_or(0);
self.emit(VestigeEvent::SearchPerformed {
query, result_count, result_ids, duration_ms, timestamp: now,
});
}
// -- dream --
"dream" => {
let replayed = result.get("memoriesReplayed")
.or(result.get("memories_replayed"))
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let connections = result.get("connectionsFound")
.or(result.get("connections_found"))
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let insights = result.get("insightsGenerated")
.or(result.get("insights"))
.and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0);
let duration_ms = result.get("durationMs")
.or(result.get("duration_ms"))
.and_then(|v| v.as_u64()).unwrap_or(0);
self.emit(VestigeEvent::DreamCompleted {
memories_replayed: replayed, connections_found: connections,
insights_generated: insights, duration_ms, timestamp: now,
});
}
// -- consolidate --
"consolidate" => {
let processed = result.get("nodesProcessed")
.or(result.get("nodes_processed"))
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let decay = result.get("decayApplied")
.or(result.get("decay_applied"))
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let embeddings = result.get("embeddingsGenerated")
.or(result.get("embeddings_generated"))
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let duration_ms = result.get("durationMs")
.or(result.get("duration_ms"))
.and_then(|v| v.as_u64()).unwrap_or(0);
self.emit(VestigeEvent::ConsolidationCompleted {
nodes_processed: processed, decay_applied: decay,
embeddings_generated: embeddings, duration_ms, timestamp: now,
});
}
// -- importance_score --
"importance_score" => {
let preview = args.as_ref()
.and_then(|a| a.get("content"))
.and_then(|v| v.as_str())
.map(|s| if s.len() > 100 { format!("{}...", &s[..100]) } else { s.to_string() })
.unwrap_or_default();
let composite = result.get("compositeScore")
.or(result.get("composite_score"))
.and_then(|v| v.as_f64()).unwrap_or(0.0);
let channels = result.get("channels").or(result.get("breakdown"));
let novelty = channels.and_then(|c| c.get("novelty"))
.and_then(|v| v.as_f64()).unwrap_or(0.0);
let arousal = channels.and_then(|c| c.get("arousal"))
.and_then(|v| v.as_f64()).unwrap_or(0.0);
let reward = channels.and_then(|c| c.get("reward"))
.and_then(|v| v.as_f64()).unwrap_or(0.0);
let attention = channels.and_then(|c| c.get("attention"))
.and_then(|v| v.as_f64()).unwrap_or(0.0);
self.emit(VestigeEvent::ImportanceScored {
content_preview: preview, composite_score: composite,
novelty, arousal, reward, attention, timestamp: now,
});
}
// Other tools don't emit events
_ => {}
}
}
}
// ============================================================================