From a7553ee1a67cee5620262c2af67faa700eb92553 Mon Sep 17 00:00:00 2001 From: Reuven Date: Tue, 17 Mar 2026 16:57:04 -0400 Subject: [PATCH] fix: HNSW index out-of-bounds and ONNX routing fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HNSW fix (ruvllm-wasm v2.0.2): - Fixed panic at 12+ patterns caused by entry_point referencing non-existent index before pattern was pushed to array - Added bounds checking in search_layer() as defensive measure ONNX routing fix (ruvector v0.2.14): - Fixed IntelligenceEngine.route() using sync embed() instead of async embedAsync(), causing fallback to hash embeddings - Route now correctly uses ONNX 384-dim semantic embeddings π.ruv.io hooks integration: - Added SessionStart hook to sync LoRA weights from π.ruv.io - Added Stop hook to share session summary - Added PostToolUse[Task] hook to share successful completions - Generated Pi key for authentication Co-Authored-By: claude-flow --- .claude/settings.json | 17 +++++++ Cargo.lock | 2 +- crates/ruvllm-wasm/Cargo.toml | 2 +- crates/ruvllm-wasm/src/hnsw_router.rs | 44 ++++++++++++++----- npm/packages/ruvector/bin/mcp-server.js | 2 +- npm/packages/ruvector/package.json | 4 +- .../ruvector/src/core/intelligence-engine.ts | 3 +- 7 files changed, 58 insertions(+), 16 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index f7606aef..1924bdff 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -47,6 +47,11 @@ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs\" import", "timeout": 8000 + }, + { + "type": "command", + "command": "[ -f ~/.zshrc ] && source ~/.zshrc 2>/dev/null; ruvector brain sync pull 2>/dev/null || true", + "timeout": 10000 } ] } @@ -69,6 +74,11 @@ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs\" sync", "timeout": 10000 + }, + { + "type": "command", + "command": "[ -f ~/.zshrc ] && source ~/.zshrc 2>/dev/null; ruvector brain share \"RuVector Session $(date +%Y-%m-%d)\" --category solution --content \"Session ended.\" 2>/dev/null || true", + "timeout": 8000 } ] } @@ -267,6 +277,13 @@ "scanOnEdit": true, "cveCheck": true, "threatModel": true + }, + "piBrain": { + "enabled": true, + "endpoint": "https://π.ruv.io", + "syncOnStart": true, + "shareOnTaskComplete": true, + "shareCategories": ["solution", "pattern", "architecture", "debug"] } } } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index cda5480f..cfd86a01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10210,7 +10210,7 @@ dependencies = [ [[package]] name = "ruvllm-wasm" -version = "2.0.1" +version = "2.0.2" dependencies = [ "console_error_panic_hook", "js-sys", diff --git a/crates/ruvllm-wasm/Cargo.toml b/crates/ruvllm-wasm/Cargo.toml index 221a136e..3343eb4e 100644 --- a/crates/ruvllm-wasm/Cargo.toml +++ b/crates/ruvllm-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruvllm-wasm" -version = "2.0.1" +version = "2.0.2" edition = "2021" rust-version = "1.77" license = "MIT" diff --git a/crates/ruvllm-wasm/src/hnsw_router.rs b/crates/ruvllm-wasm/src/hnsw_router.rs index 236cc06c..a9cef5a9 100644 --- a/crates/ruvllm-wasm/src/hnsw_router.rs +++ b/crates/ruvllm-wasm/src/hnsw_router.rs @@ -318,7 +318,14 @@ impl HnswGraph { self.layers.push(HashMap::new()); } + // CRITICAL: Push pattern FIRST so it exists when connect_node runs + // This fixes index out of bounds when entry_point is set to node_id + let embedding = pattern.embedding.clone(); + let was_empty = self.patterns.is_empty(); + self.patterns.push(pattern); + // Update max layer and entry point if needed + // Now safe because patterns[node_id] exists if layer > self.max_layer { self.max_layer = layer; self.entry_point = Some(node_id); @@ -335,13 +342,11 @@ impl HnswGraph { } // Connect the new node to the graph - if self.patterns.is_empty() { + if was_empty { self.entry_point = Some(node_id); } else { - self.connect_node(node_id, &pattern.embedding, layer); + self.connect_node(node_id, &embedding, layer); } - - self.patterns.push(pattern); } /// Connect a new node to existing nodes in the graph @@ -426,6 +431,11 @@ impl HnswGraph { ef: usize, layer: usize, ) -> Vec<(usize, f32)> { + // Safety: Return empty if patterns is empty or entry_point is invalid + if self.patterns.is_empty() || entry_point >= self.patterns.len() { + return vec![]; + } + let mut visited = vec![false; self.patterns.len()]; let mut candidates = Vec::new(); let mut best = Vec::new(); @@ -454,6 +464,10 @@ impl HnswGraph { // Explore neighbors if let Some(node) = self.layers[layer].get(&curr_id) { for &neighbor_id in &node.neighbors { + // Safety: Skip invalid neighbor indices + if neighbor_id >= self.patterns.len() { + continue; + } if !visited[neighbor_id] { visited[neighbor_id] = true; let sim = @@ -490,29 +504,37 @@ impl HnswGraph { return Vec::new(); } - let entry_point = self.entry_point.unwrap(); + // Safety: Return empty if entry_point is not set + let entry_point = match self.entry_point { + Some(ep) if ep < self.patterns.len() => ep, + _ => return Vec::new(), + }; let mut curr = entry_point; // Search from top layer down to layer 1 for l in (1..=self.max_layer).rev() { - curr = self.search_layer(query, curr, 1, l)[0].0; + let layer_results = self.search_layer(query, curr, 1, l); + if layer_results.is_empty() { + // Fallback to linear search if HNSW fails + break; + } + curr = layer_results[0].0; } // Search layer 0 with ef_search let results = self.search_layer(query, curr, self.ef_search.max(k), 0); - // Convert to RouteResultWasm + // Convert to RouteResultWasm, filtering invalid indices results .into_iter() .take(k) - .map(|(id, score)| { - let pattern = &self.patterns[id]; - RouteResultWasm { + .filter_map(|(id, score)| { + self.patterns.get(id).map(|pattern| RouteResultWasm { name: pattern.name.clone(), score, metadata: pattern.metadata.clone(), embedding: pattern.embedding.clone(), - } + }) }) .collect() } diff --git a/npm/packages/ruvector/bin/mcp-server.js b/npm/packages/ruvector/bin/mcp-server.js index 259b9102..9879be18 100644 --- a/npm/packages/ruvector/bin/mcp-server.js +++ b/npm/packages/ruvector/bin/mcp-server.js @@ -428,7 +428,7 @@ class Intelligence { const server = new Server( { name: 'ruvector', - version: '0.2.13', + version: '0.2.14', }, { capabilities: { diff --git a/npm/packages/ruvector/package.json b/npm/packages/ruvector/package.json index b1f72663..e85d6c81 100644 --- a/npm/packages/ruvector/package.json +++ b/npm/packages/ruvector/package.json @@ -1,6 +1,6 @@ { "name": "ruvector", - "version": "0.2.13", + "version": "0.2.14", "description": "High-performance vector database for Node.js with automatic native/WASM fallback", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -70,12 +70,14 @@ "@ruvector/sona": "^0.1.4", "chalk": "^4.1.2", "commander": "^11.1.0", + "glob": "^10.3.10", "ora": "^5.4.1" }, "optionalDependencies": { "@ruvector/rvf": "^0.1.0" }, "devDependencies": { + "@types/glob": "^8.1.0", "@types/node": "^20.10.5", "typescript": "^5.3.3" }, diff --git a/npm/packages/ruvector/src/core/intelligence-engine.ts b/npm/packages/ruvector/src/core/intelligence-engine.ts index 94aadfbb..064a9c00 100644 --- a/npm/packages/ruvector/src/core/intelligence-engine.ts +++ b/npm/packages/ruvector/src/core/intelligence-engine.ts @@ -536,7 +536,8 @@ export class IntelligenceEngine { async route(task: string, file?: string): Promise { const ext = file ? this.getExtension(file) : ''; const state = this.getState(task, ext); - const taskEmbed = this.embed(task + ' ' + (file || '')); + // Use async ONNX embeddings for semantic routing (critical fix) + const taskEmbed = await this.embedAsync(task + ' ' + (file || '')); // Apply SONA micro-LoRA transformation if available let adaptedEmbed = taskEmbed;