diff --git a/.gitignore b/.gitignore index a5523d3..4236e68 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ apps/dashboard/node_modules/ # External repos (forks, submodules) # ============================================================================= fastembed-rs/ +.mcp.json diff --git a/Cargo.toml b/Cargo.toml index 83cccd2..89761ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,6 @@ resolver = "2" members = [ "crates/vestige-core", "crates/vestige-mcp", - "crates/vestige-agent", - "crates/vestige-agent-py", "tests/e2e", ] exclude = [ diff --git a/apps/dashboard/src/lib/components/TimeSlider.svelte b/apps/dashboard/src/lib/components/TimeSlider.svelte index 6d96318..acdc19a 100644 --- a/apps/dashboard/src/lib/components/TimeSlider.svelte +++ b/apps/dashboard/src/lib/components/TimeSlider.svelte @@ -51,6 +51,7 @@ } function playLoop() { + if (!playing) return; animFrameId = requestAnimationFrame((now) => { const deltaSeconds = (now - lastTime) / 1000; lastTime = now; @@ -78,6 +79,7 @@ } onDestroy(() => { + playing = false; cancelAnimationFrame(animFrameId); }); diff --git a/apps/dashboard/src/lib/graph/__tests__/effects.test.ts b/apps/dashboard/src/lib/graph/__tests__/effects.test.ts index dcadbe7..9c80986 100644 --- a/apps/dashboard/src/lib/graph/__tests__/effects.test.ts +++ b/apps/dashboard/src/lib/graph/__tests__/effects.test.ts @@ -245,11 +245,11 @@ describe('EffectManager', () => { expect(n1Pulses.length).toBeLessThanOrEqual(1); }); - it('applies scale bump to contacted nodes', () => { + it('adds pulse to contacted nodes instead of direct scale mutation', () => { const nodePositions = new Map([ ['bump', new Vector3(3, 0, 0)], ]); - const mesh = createMockMesh('bump', new Vector3(3, 0, 0)); + createMockMesh('bump', new Vector3(3, 0, 0)); effects.createRippleWave(new Vector3(0, 0, 0) as any); @@ -258,8 +258,9 @@ describe('EffectManager', () => { effects.update(nodeMeshMap, camera, nodePositions); } - // Scale should have been bumped (1.3x) - expect(mesh.scale.x).toBeGreaterThan(1.0); + // Ripple wave should add a pulse effect (not a direct scale mutation) + const bumpPulses = effects.pulseEffects.filter(p => p.nodeId === 'bump'); + expect(bumpPulses.length).toBeGreaterThan(0); }); it('completes and cleans up after 90 frames', () => { diff --git a/apps/dashboard/src/lib/graph/effects.ts b/apps/dashboard/src/lib/graph/effects.ts index 0e37a8e..1402476 100644 --- a/apps/dashboard/src/lib/graph/effects.ts +++ b/apps/dashboard/src/lib/graph/effects.ts @@ -335,11 +335,8 @@ export class EffectManager { rw.pulsedNodes.add(id); // Mini-pulse on contact this.addPulse(id, 0.8, new THREE.Color(0x00ffd1), 0.03); - // Mini scale bump on the mesh - const mesh = nodeMeshMap.get(id); - if (mesh) { - mesh.scale.multiplyScalar(1.3); - } + // Pulse handles the visual bump — no direct scale mutation + // (multiplyScalar was cumulative and fought with animation system) } }); } diff --git a/apps/dashboard/src/lib/graph/events.ts b/apps/dashboard/src/lib/graph/events.ts index f1d2c54..a554709 100644 --- a/apps/dashboard/src/lib/graph/events.ts +++ b/apps/dashboard/src/lib/graph/events.ts @@ -115,7 +115,7 @@ export function mapEventToEffects( id: data.id, label: (data.content ?? '').slice(0, 60), type: data.node_type ?? 'fact', - retention: data.retention ?? 0.9, + retention: Math.max(0, Math.min(1, data.retention ?? 0.9)), tags: data.tags ?? [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/apps/dashboard/src/lib/graph/nodes.ts b/apps/dashboard/src/lib/graph/nodes.ts index e4b879a..0bb6e39 100644 --- a/apps/dashboard/src/lib/graph/nodes.ts +++ b/apps/dashboard/src/lib/graph/nodes.ts @@ -205,7 +205,11 @@ export class NodeManager { private createTextSprite(text: string, color: string): THREE.Sprite { const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d')!; + const ctx = canvas.getContext('2d'); + if (!ctx) { + const tex = new THREE.Texture(); + return new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0 })); + } canvas.width = 512; canvas.height = 64; diff --git a/apps/dashboard/src/lib/stores/websocket.ts b/apps/dashboard/src/lib/stores/websocket.ts index 41827bd..c9985f1 100644 --- a/apps/dashboard/src/lib/stores/websocket.ts +++ b/apps/dashboard/src/lib/stores/websocket.ts @@ -45,8 +45,8 @@ function createWebSocketStore() { const events = [parsed, ...s.events].slice(0, MAX_EVENTS); return { ...s, events }; }); - } catch { - // Ignore malformed messages + } catch (e) { + console.warn('[vestige] Failed to parse WebSocket message:', e); } }; diff --git a/crates/vestige-core/src/consolidation/phases.rs b/crates/vestige-core/src/consolidation/phases.rs index 71f3724..bc50e56 100644 --- a/crates/vestige-core/src/consolidation/phases.rs +++ b/crates/vestige-core/src/consolidation/phases.rs @@ -380,9 +380,6 @@ impl DreamEngine { let mut strengthened_ids = Vec::new(); let replay_set: HashSet<&String> = replay_queue.iter().collect(); - let _triaged_map: HashMap<&str, &TriagedMemory> = triaged.iter() - .map(|m| (m.id.as_str(), m)) - .collect(); // Process replay queue in oscillation waves let wave_count = replay_queue.len().div_ceil(self.wave_batch_size); @@ -726,10 +723,12 @@ impl DreamEngine { let mut seen_pairs: HashSet<(String, String)> = HashSet::new(); insights.retain(|i| { if i.source_memory_ids.len() >= 2 { - let pair = ( - i.source_memory_ids[0].clone().min(i.source_memory_ids[1].clone()), - i.source_memory_ids[0].clone().max(i.source_memory_ids[1].clone()), - ); + let (a, b) = (&i.source_memory_ids[0], &i.source_memory_ids[1]); + let pair = if a <= b { + (a.clone(), b.clone()) + } else { + (b.clone(), a.clone()) + }; seen_pairs.insert(pair) } else { true diff --git a/crates/vestige-mcp/src/tools/cross_reference.rs b/crates/vestige-mcp/src/tools/cross_reference.rs index 9327be7..6cfc3f5 100644 --- a/crates/vestige-mcp/src/tools/cross_reference.rs +++ b/crates/vestige-mcp/src/tools/cross_reference.rs @@ -219,18 +219,18 @@ fn generate_reasoning_chain( "PRIMARY FINDING (trust {:.0}%, {}): {}\n", primary.trust * 100.0, primary.updated_at.format("%b %d, %Y"), - primary.content.chars().take(150).collect::(), + primary.content.chars().take(300).collect::(), )); - // Superseded memories + // Superseded memories — with reasoning arrows let superseded: Vec<_> = relations.iter() .filter(|(_, _, r)| matches!(r.relation, Relation::Supersedes)) .collect(); for (preview, trust, rel) in &superseded { chain.push_str(&format!( - " SUPERSEDES (trust {:.0}%): \"{}\" — {}\n", + " SUPERSEDES (trust {:.0}%): \"{}\"\n -> {}\n", trust * 100.0, - preview.chars().take(80).collect::(), + preview.chars().take(100).collect::(), rel.reasoning, )); } @@ -240,7 +240,7 @@ fn generate_reasoning_chain( .filter(|(_, _, r)| matches!(r.relation, Relation::Supports)) .collect(); if !supporting.is_empty() { - chain.push_str(&format!("\nSUPPORTED BY {} MEMOR{}:\n", + chain.push_str(&format!("SUPPORTED BY {} MEMOR{}:\n", supporting.len(), if supporting.len() == 1 { "Y" } else { "IES" }, )); @@ -248,7 +248,7 @@ fn generate_reasoning_chain( chain.push_str(&format!( " + (trust {:.0}%): \"{}\"\n", trust * 100.0, - preview.chars().take(80).collect::(), + preview.chars().take(100).collect::(), )); } } @@ -258,19 +258,23 @@ fn generate_reasoning_chain( .filter(|(_, _, r)| matches!(r.relation, Relation::Contradicts)) .collect(); if !contradicting.is_empty() { - chain.push_str(&format!("\nCONTRADICTING EVIDENCE ({}):\n", contradicting.len())); + chain.push_str(&format!("CONTRADICTING EVIDENCE ({}):\n", contradicting.len())); for (preview, trust, rel) in contradicting.iter().take(3) { chain.push_str(&format!( - " ! (trust {:.0}%): \"{}\" — {}\n", + " ! (trust {:.0}%): \"{}\"\n -> {}\n", trust * 100.0, - preview.chars().take(80).collect::(), + preview.chars().take(100).collect::(), rel.reasoning, )); } } - // Confidence summary - chain.push_str(&format!("\nOVERALL CONFIDENCE: {:.0}%\n", confidence * 100.0)); + // If no relations found, still provide useful output + if superseded.is_empty() && supporting.is_empty() && contradicting.is_empty() { + chain.push_str("NO CONTRADICTIONS DETECTED. Evidence is consistent.\n"); + } + + chain.push_str(&format!("OVERALL CONFIDENCE: {:.0}%\n", confidence * 100.0)); chain } @@ -541,12 +545,15 @@ pub async fn execute( .max_by(|a, b| a.trust.partial_cmp(&b.trust).unwrap_or(std::cmp::Ordering::Equal)) { for other in scored.iter().filter(|s| s.id != primary.id).take(15) { + // Use combined_score as a proxy for semantic similarity (already reranked) + // Fall back to topic_overlap for keyword-level comparison let sim = topic_overlap(&primary.content, &other.content); + let effective_sim = if other.combined_score > 0.2 { sim.max(0.3) } else { sim }; let rel = assess_relation( &primary.content, &other.content, primary.trust, other.trust, primary.updated_at, other.updated_at, - sim, + effective_sim, ); if !matches!(rel.relation, Relation::Irrelevant) { pair_relations.push((