diff --git a/web/package.json b/web/package.json index dfee8d0..f010716 100755 --- a/web/package.json +++ b/web/package.json @@ -95,6 +95,7 @@ "@cloudflare/workers-oauth-provider": "^0.2.4", "@stackbilt/contracts": "^0.2.1", "@stackbilt/llm-providers": "^1.6.4", + "@stackbilt/wasm-core": "0.2.0", "agents": "^0.12.3", "hono": "^4.12.12", "partysocket": "^1.1.19", @@ -108,6 +109,8 @@ "@types/react-dom": "^19.2.3", "typescript": "^5.7.3", "vite": "^8.0.5", + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-wasm": "^3.6.0", "vitest": "^4.0.18", "wrangler": "^4.76.0" }, @@ -121,7 +124,11 @@ "schema.sql" ], "pnpm": { - "onlyBuiltDependencies": ["esbuild", "sharp", "workerd"] + "onlyBuiltDependencies": [ + "esbuild", + "sharp", + "workerd" + ] }, "keywords": [ "cloudflare-workers", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 3da2b14..93559bb 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@stackbilt/llm-providers': specifier: ^1.6.4 version: 1.18.0 + '@stackbilt/wasm-core': + specifier: 0.2.0 + version: 0.2.0 agents: specifier: ^0.12.3 version: 0.12.4(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@4.20241218.0)(ai@6.0.208(zod@4.4.3))(react@19.2.7)(rolldown@1.1.2)(vite@8.1.0(esbuild@0.28.1))(zod@4.4.3) @@ -54,6 +57,12 @@ importers: vite: specifier: ^8.0.5 version: 8.1.0(esbuild@0.28.1) + vite-plugin-top-level-await: + specifier: ^1.6.0 + version: 1.6.0(vite@8.1.0(esbuild@0.28.1)) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.1.0(esbuild@0.28.1)) vitest: specifier: ^4.0.18 version: 4.1.9(@opentelemetry/api@1.9.1)(vite@8.1.0(esbuild@0.28.1)) @@ -756,6 +765,15 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/plugin-virtual@3.0.2': + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -770,9 +788,108 @@ packages: resolution: {integrity: sha512-EGu3yBoigd44LgVniUoGEjg1eYbr4NprVdj47tj33mfXiLLU1GmOlICWSXwYtpnadWV2qV9AQtAO+oKnmD4Q5Q==} engines: {node: '>=20.0.0'} + '@stackbilt/wasm-core@0.2.0': + resolution: {integrity: sha512-5omMk49nBmzF+FXe41y4tfOS6B9uUUrrgGr3SVLuil5fNw56BTaH9oHcEkZv7oIX3SFYXfBDJRXx7UoFbj+KdQ==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/core-darwin-arm64@1.15.43': + resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.43': + resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.43': + resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.43': + resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.43': + resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.43': + resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.43': + resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.43': + resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.43': + resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.43': + resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.43': + resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.43': + resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.43': + resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.27': + resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} + + '@swc/wasm@1.15.43': + resolution: {integrity: sha512-jYqeckrzZGAU+9OSfmL15MWfhkKWRCC8QMssL/MZu/MaIP76mU3VHjzqxlwKIz9DOSR/9jCqi1+QlWE0ILNehA==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1573,10 +1690,25 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-plugin-top-level-await@1.6.0: + resolution: {integrity: sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==} + peerDependencies: + vite: '>=2.8' + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + vite@8.1.0: resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2285,6 +2417,8 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/plugin-virtual@3.0.2': {} + '@sindresorhus/is@7.2.0': {} '@speed-highlight/core@1.2.17': {} @@ -2295,8 +2429,72 @@ snapshots: '@stackbilt/llm-providers@1.18.0': {} + '@stackbilt/wasm-core@0.2.0': {} + '@standard-schema/spec@1.1.0': {} + '@swc/core-darwin-arm64@1.15.43': + optional: true + + '@swc/core-darwin-x64@1.15.43': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.43': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.43': + optional: true + + '@swc/core-linux-arm64-musl@1.15.43': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.43': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.43': + optional: true + + '@swc/core-linux-x64-gnu@1.15.43': + optional: true + + '@swc/core-linux-x64-musl@1.15.43': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.43': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.43': + optional: true + + '@swc/core-win32-x64-msvc@1.15.43': + optional: true + + '@swc/core@1.15.43': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.27 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.43 + '@swc/core-darwin-x64': 1.15.43 + '@swc/core-linux-arm-gnueabihf': 1.15.43 + '@swc/core-linux-arm64-gnu': 1.15.43 + '@swc/core-linux-arm64-musl': 1.15.43 + '@swc/core-linux-ppc64-gnu': 1.15.43 + '@swc/core-linux-s390x-gnu': 1.15.43 + '@swc/core-linux-x64-gnu': 1.15.43 + '@swc/core-linux-x64-musl': 1.15.43 + '@swc/core-win32-arm64-msvc': 1.15.43 + '@swc/core-win32-ia32-msvc': 1.15.43 + '@swc/core-win32-x64-msvc': 1.15.43 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.27': + dependencies: + '@swc/counter': 0.1.3 + + '@swc/wasm@1.15.43': {} + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -3094,8 +3292,25 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uuid@10.0.0: {} + vary@1.1.2: {} + vite-plugin-top-level-await@1.6.0(vite@8.1.0(esbuild@0.28.1)): + dependencies: + '@rollup/plugin-virtual': 3.0.2 + '@swc/core': 1.15.43 + '@swc/wasm': 1.15.43 + uuid: 10.0.0 + vite: 8.1.0(esbuild@0.28.1) + transitivePeerDependencies: + - '@swc/helpers' + - rollup + + vite-plugin-wasm@3.6.0(vite@8.1.0(esbuild@0.28.1)): + dependencies: + vite: 8.1.0(esbuild@0.28.1) + vite@8.1.0(esbuild@0.28.1): dependencies: lightningcss: 1.32.0 diff --git a/web/src/kernel/memory/graph.ts b/web/src/kernel/memory/graph.ts index 75488a2..e068072 100755 --- a/web/src/kernel/memory/graph.ts +++ b/web/src/kernel/memory/graph.ts @@ -3,9 +3,13 @@ // Phase 2 of the cognitive layer. Populates kg_nodes/kg_edges tables using // zero-cost regex/heuristic NER (no LLM calls). Provides spreading activation // for context-aware retrieval. +// +// activateGraph() wires to @stackbilt/wasm-core WasmGraph: 2 bulk D1 SELECTs +// replace the previous O(2N) sequential edge queries (aegis-oss#68). // ─── Node Type Classification ──────────────────────────────────────────────── +import { WasmGraph } from '@stackbilt/wasm-core'; import type { NodeType, SourceSystem } from '../../schema-enums.js'; export type { SourceSystem } from '../../schema-enums.js'; @@ -199,92 +203,24 @@ export async function activateGraph( ): Promise { if (!query || query.trim().length < 2) return []; - // Find seed nodes by keyword matching against labels - const keywords = query.toLowerCase().split(/[\s,;:()\[\]]+/).filter(w => w.length >= 3); - if (keywords.length === 0) return []; - - // Build LIKE conditions for seed node matching - const conditions = keywords.map(() => 'LOWER(label) LIKE ?').join(' OR '); - const binds = keywords.map(k => `%${k}%`); - - const seedResult = await db.prepare( - `SELECT id, label, node_type FROM kg_nodes WHERE ${conditions} LIMIT 20` - ).bind(...binds).all<{ id: number; label: string; node_type: string }>(); - - if (seedResult.results.length === 0) return []; - - // Activation map: nodeId -> activation score - const activations = new Map(); - const nodeInfo = new Map(); - - // Seed nodes get activation 1.0 - for (const seed of seedResult.results) { - activations.set(seed.id, 1.0); - nodeInfo.set(seed.id, { label: seed.label, type: seed.node_type }); + // Bulk-load full graph snapshot: 2 queries total regardless of graph size. + // WasmGraph runs the BFS in WASM linear memory — zero additional D1 round trips. + const [nodesResult, edgesResult] = await Promise.all([ + db.prepare( + 'SELECT id, label, node_type, activation FROM kg_nodes' + ).all<{ id: number; label: string; node_type: string; activation: number }>(), + db.prepare( + 'SELECT source_id, target_id, weight FROM kg_edges' + ).all<{ source_id: number; target_id: number; weight: number }>(), + ]); + + if (nodesResult.results.length === 0) return []; + + const graph = WasmGraph.fromSnapshotArrays(nodesResult.results, edgesResult.results); + try { + const raw = graph.spreadActivation(query, hops, 10); + return raw as ActivatedNode[]; + } finally { + graph.free(); } - - // Spread activation through edges - const DECAY = 0.7; - let frontier = [...seedResult.results.map(s => s.id)]; - - for (let hop = 0; hop < hops && frontier.length > 0; hop++) { - const nextFrontier: number[] = []; - - for (const nodeId of frontier) { - const sourceActivation = activations.get(nodeId) ?? 0; - if (sourceActivation < 0.05) continue; // Prune weak activations - - // Find neighbors via edges (both directions) - const neighbors = await db.prepare( - `SELECT - CASE WHEN source_id = ? THEN target_id ELSE source_id END as neighbor_id, - weight, - e.id as edge_id - FROM kg_edges e - WHERE source_id = ? OR target_id = ? - LIMIT 20` - ).bind(nodeId, nodeId, nodeId).all<{ neighbor_id: number; weight: number; edge_id: number }>(); - - for (const neighbor of neighbors.results) { - const spreadActivation = sourceActivation * neighbor.weight * DECAY; - const current = activations.get(neighbor.neighbor_id) ?? 0; - - if (spreadActivation > current) { - activations.set(neighbor.neighbor_id, spreadActivation); - nextFrontier.push(neighbor.neighbor_id); - } - } - } - - frontier = nextFrontier; - } - - // Fetch info for any activated nodes we don't have info for yet - const missingIds = [...activations.keys()].filter(id => !nodeInfo.has(id)); - if (missingIds.length > 0) { - // Batch fetch in chunks to avoid overly long queries - for (let i = 0; i < missingIds.length; i += 20) { - const chunk = missingIds.slice(i, i + 20); - const placeholders = chunk.map(() => '?').join(','); - const infoResult = await db.prepare( - `SELECT id, label, node_type FROM kg_nodes WHERE id IN (${placeholders})` - ).bind(...chunk).all<{ id: number; label: string; node_type: string }>(); - - for (const row of infoResult.results) { - nodeInfo.set(row.id, { label: row.label, type: row.node_type }); - } - } - } - - // Build result: top 10 by activation score - const results: ActivatedNode[] = []; - for (const [nodeId, activation] of activations.entries()) { - const info = nodeInfo.get(nodeId); - if (info) { - results.push({ label: info.label, type: info.type, activation }); - } - } - - results.sort((a, b) => b.activation - a.activation); - return results.slice(0, 10); } diff --git a/web/src/kernel/router.ts b/web/src/kernel/router.ts index 74824c3..3cd4ac2 100755 --- a/web/src/kernel/router.ts +++ b/web/src/kernel/router.ts @@ -99,9 +99,9 @@ const DEFAULT_ROUTES: Record = { request_clarification: 'direct', bizops_read: 'gpt_oss', bizops_mutate: 'gpt_oss', - general_knowledge: 'gpt_oss', - memory_recall: 'gpt_oss', - greeting: 'gpt_oss', + general_knowledge: 'workers_ai', + memory_recall: 'gpt_oss', // equity recall failure 2026-03-04 — keep on gpt_oss + greeting: 'workers_ai', code_task: 'claude_code', code_review: 'gpt_oss', self_improvement: 'composite', @@ -109,8 +109,8 @@ const DEFAULT_ROUTES: Record = { goal_execution: 'composite', symbolic_consultation: 'gpt_oss', support_triage: 'gpt_oss', - tarot_pulse: 'gpt_oss', - tarot_trajectory: 'gpt_oss', + tarot_pulse: 'workers_ai', + tarot_trajectory: 'workers_ai', tarot_multi_angle: 'gpt_oss', tarot_deep: 'gpt_oss', tarot_shadow: 'gpt_oss', @@ -134,7 +134,7 @@ function selectDefaultExecutor(classification: string, intent: KernelIntent): Ex // Fixed executors (unchanged regardless of complexity or confidence) if (classification === 'heartbeat') return 'direct'; - if (classification === 'greeting') return 'gpt_oss'; // GPT-OSS 120B — smart enough for re-entry briefing + if (classification === 'greeting') return 'workers_ai'; if (classification === 'code_task') return 'claude_code'; // TarotScript is a classifier, not a responder — all classifications route to LLM executors if (classification === 'symbolic_consultation') return 'gpt_oss'; @@ -169,7 +169,7 @@ function selectDefaultExecutor(classification: string, intent: KernelIntent): Ex // preserve thread history and avoid orchestrator intent drift. if (confidence < CONFIDENCE_TRUST) { if (needsTools) return 'gpt_oss'; - if (complexity <= 1) return 'gpt_oss'; // workers_ai → gpt_oss + if (complexity <= 1) return 'workers_ai'; return 'claude'; // moderate no-tool → claude } @@ -449,7 +449,7 @@ export async function route( if (procedure) { if (procedure.status === 'degraded' || procedure.status === 'broken') { - const executor = DEFAULT_ROUTES[classification] ?? 'claude'; + const executor = DEFAULT_ROUTES[classification] ?? 'workers_ai'; return { plan: { executor, diff --git a/web/tests/memory/graph.test.ts b/web/tests/memory/graph.test.ts index b4bd71b..0bd32ac 100755 --- a/web/tests/memory/graph.test.ts +++ b/web/tests/memory/graph.test.ts @@ -166,9 +166,10 @@ describe('activateGraph', () => { expect(result).toEqual([]); }); - it('returns empty when no seed nodes match', async () => { + it('returns empty when graph has no nodes', async () => { const db = createMockDb({ - allResults: [[]], // seed query returns no results + // Bulk load: nodes empty, edges empty (Promise.all fires both) + allResults: [[], []], }); const result = await activateGraph(db, 'nonexistent thing'); @@ -178,9 +179,9 @@ describe('activateGraph', () => { it('returns seed nodes with activation 1.0', async () => { const db = createMockDb({ allResults: [ - // seed nodes found - [{ id: 1, label: 'aegis', node_type: 'project' }], - // hop 0: neighbors of node 1 + // Bulk SELECT kg_nodes (all rows) + [{ id: 1, label: 'aegis', node_type: 'project', activation: 0.5 }], + // Bulk SELECT kg_edges (all rows) [], ], }); @@ -194,14 +195,13 @@ describe('activateGraph', () => { it('spreads activation to neighbors with decay', async () => { const db = createMockDb({ allResults: [ - // seed nodes - [{ id: 1, label: 'aegis', node_type: 'project' }], - // hop 0: neighbors of node 1 - [{ neighbor_id: 2, weight: 0.8, edge_id: 10 }], - // hop 1: neighbors of node 2 - [], - // fetch missing node info for node 2 - [{ id: 2, label: 'cloudflare', node_type: 'tool' }], + // Bulk SELECT kg_nodes + [ + { id: 1, label: 'aegis', node_type: 'project', activation: 0.5 }, + { id: 2, label: 'cloudflare', node_type: 'tool', activation: 0.3 }, + ], + // Bulk SELECT kg_edges + [{ source_id: 1, target_id: 2, weight: 0.8 }], ], }); diff --git a/web/tests/recall-baseline.test.ts b/web/tests/recall-baseline.test.ts index 36f552e..5a52033 100755 --- a/web/tests/recall-baseline.test.ts +++ b/web/tests/recall-baseline.test.ts @@ -63,26 +63,24 @@ describe('recall-baseline: activateGraph', () => { }); it('performs 2-hop spreading activation by default', async () => { - // Seed: node 1 (aegis) → hop 0 neighbor: node 2 (cloudflare) → hop 1 neighbor: node 3 (d1) + // Bulk load: all nodes + all edges. WASM runs BFS internally. + // Chain: aegis(1) --0.8--> cloudflare(2) --0.6--> d1(3) const db = createMockDb({ allResults: [ - // seed query: find nodes matching "aegis" - [{ id: 1, label: 'aegis', node_type: 'project' }], - // hop 0: neighbors of node 1 - [{ neighbor_id: 2, weight: 0.8, edge_id: 10 }], - // hop 1: neighbors of node 2 - [{ neighbor_id: 3, weight: 0.6, edge_id: 11 }], - // fetch missing node info for nodes 2 and 3 [ - { id: 2, label: 'cloudflare', node_type: 'tool' }, - { id: 3, label: 'd1', node_type: 'tool' }, + { id: 1, label: 'aegis', node_type: 'project', activation: 0.5 }, + { id: 2, label: 'cloudflare', node_type: 'tool', activation: 0.3 }, + { id: 3, label: 'd1', node_type: 'tool', activation: 0.3 }, + ], + [ + { source_id: 1, target_id: 2, weight: 0.8 }, + { source_id: 2, target_id: 3, weight: 0.6 }, ], ], }); const result = await activateGraph(db, 'aegis'); - // Seed gets 1.0 const seed = result.find(n => n.label === 'aegis'); expect(seed?.activation).toBe(1.0); @@ -99,13 +97,11 @@ describe('recall-baseline: activateGraph', () => { // Single hop with weight 1.0 → activation = 1.0 * 1.0 * 0.7 = 0.7 const db = createMockDb({ allResults: [ - [{ id: 1, label: 'stripe', node_type: 'tool' }], - // hop 0: neighbor with weight 1.0 - [{ neighbor_id: 2, weight: 1.0, edge_id: 1 }], - // hop 1: no further neighbors - [], - // fetch node info for node 2 - [{ id: 2, label: 'billing', node_type: 'concept' }], + [ + { id: 1, label: 'stripe', node_type: 'tool', activation: 0.5 }, + { id: 2, label: 'billing', node_type: 'concept', activation: 0.3 }, + ], + [{ source_id: 1, target_id: 2, weight: 1.0 }], ], }); @@ -115,23 +111,17 @@ describe('recall-baseline: activateGraph', () => { }); it('caps results at 10 nodes', async () => { - // Seed with many neighbors — only top 10 should be returned - const db = createMockDb({ - allResults: [ - // 3 seed nodes - Array.from({ length: 3 }, (_, i) => ({ id: i + 1, label: `seed${i}`, node_type: 'concept' })), - // hop 0: seed 0 has 5 neighbors - Array.from({ length: 5 }, (_, i) => ({ neighbor_id: 100 + i, weight: 0.5, edge_id: i })), - // hop 0: seed 1 has 5 neighbors - Array.from({ length: 5 }, (_, i) => ({ neighbor_id: 200 + i, weight: 0.5, edge_id: i + 10 })), - // hop 0: seed 2 has 5 neighbors - Array.from({ length: 5 }, (_, i) => ({ neighbor_id: 300 + i, weight: 0.5, edge_id: i + 20 })), - // hop 1: no further neighbors for any of the 15 new frontier nodes - ...Array.from({ length: 15 }, () => [] as Record[]), - // fetch missing node info (15 nodes) - Array.from({ length: 15 }, (_, i) => ({ id: [100, 200, 300][Math.floor(i / 5)] + (i % 5), label: `neighbor${i}`, node_type: 'concept' })), - ], - }); + // 3 seeds + 15 neighbors (connected via edges), top 10 returned + const nodes = [ + ...Array.from({ length: 3 }, (_, i) => ({ id: i + 1, label: `seed${i}`, node_type: 'concept', activation: 0.5 })), + ...Array.from({ length: 15 }, (_, i) => ({ id: 100 + i, label: `neighbor${i}`, node_type: 'concept', activation: 0.3 })), + ]; + const edges = [ + ...Array.from({ length: 5 }, (_, i) => ({ source_id: 1, target_id: 100 + i, weight: 0.5 })), + ...Array.from({ length: 5 }, (_, i) => ({ source_id: 2, target_id: 105 + i, weight: 0.5 })), + ...Array.from({ length: 5 }, (_, i) => ({ source_id: 3, target_id: 110 + i, weight: 0.5 })), + ]; + const db = createMockDb({ allResults: [nodes, edges] }); const result = await activateGraph(db, 'seed0 seed1 seed2'); expect(result.length).toBeLessThanOrEqual(10); @@ -140,19 +130,14 @@ describe('recall-baseline: activateGraph', () => { it('results are sorted by activation descending', async () => { const db = createMockDb({ allResults: [ - [{ id: 1, label: 'aegis', node_type: 'project' }], - // Two neighbors with different weights [ - { neighbor_id: 2, weight: 0.9, edge_id: 1 }, - { neighbor_id: 3, weight: 0.3, edge_id: 2 }, + { id: 1, label: 'aegis', node_type: 'project', activation: 0.5 }, + { id: 2, label: 'high', node_type: 'concept', activation: 0.3 }, + { id: 3, label: 'low', node_type: 'concept', activation: 0.3 }, ], - // hop 1: no further neighbors - [], - [], - // fetch node info [ - { id: 2, label: 'high', node_type: 'concept' }, - { id: 3, label: 'low', node_type: 'concept' }, + { source_id: 1, target_id: 2, weight: 0.9 }, + { source_id: 1, target_id: 3, weight: 0.3 }, ], ], }); @@ -164,27 +149,21 @@ describe('recall-baseline: activateGraph', () => { }); it('prunes activations below 0.05 threshold', async () => { - // A neighbor with very low weight gets activation 1.0 * 0.05 * 0.7 = 0.035 - // On hop 1, this node's source activation (0.035) is below the 0.05 prune threshold, - // so the `continue` skips it BEFORE the db query — no further spreading. + // aegis(1) --weight=0.05--> weak(2): spread = 1.0 * 0.05 * 0.7 = 0.035 < PRUNE_THRESHOLD + // 'weak' should not appear in results const db = createMockDb({ allResults: [ - // seed query - [{ id: 1, label: 'aegis', node_type: 'project' }], - // hop 0: neighbors of seed node 1 - [{ neighbor_id: 2, weight: 0.05, edge_id: 1 }], - // hop 1: node 2 is in frontier but activation 0.035 < 0.05 → continue (no db call) - // So NO more all() calls for hop 1 neighbors. - // fetch missing node info for node 2 - [{ id: 2, label: 'weak', node_type: 'concept' }], + [ + { id: 1, label: 'aegis', node_type: 'project', activation: 0.5 }, + { id: 2, label: 'weak', node_type: 'concept', activation: 0.3 }, + ], + [{ source_id: 1, target_id: 2, weight: 0.05 }], ], }); const result = await activateGraph(db, 'aegis'); - // Node 2 was activated (0.035) but since it's below prune threshold, - // it didn't propagate further. It still appears in results though. const weak = result.find(n => n.label === 'weak'); - expect(weak?.activation).toBeCloseTo(0.035, 3); + expect(weak).toBeUndefined(); }); }); diff --git a/web/tests/recall-mindspring.test.ts b/web/tests/recall-mindspring.test.ts index 847a73b..d389106 100755 --- a/web/tests/recall-mindspring.test.ts +++ b/web/tests/recall-mindspring.test.ts @@ -134,11 +134,13 @@ describe('recall-pipeline: Stage 3.5 MindSpring', () => { allResults: [ // Stage 1: blocks empty [], - // Stage 2: activateGraph — "billing" seed → "stripe" neighbor - [{ id: 1, label: 'billing', node_type: 'concept' }], - [{ neighbor_id: 2, weight: 0.9, edge_id: 1 }], - [], // hop 1 - [{ id: 2, label: 'stripe', node_type: 'tool' }], + // Stage 2: activateGraph — bulk nodes: billing + stripe + [ + { id: 1, label: 'billing', node_type: 'concept', activation: 0.5 }, + { id: 2, label: 'stripe', node_type: 'tool', activation: 0.3 }, + ], + // Stage 2: activateGraph — bulk edges: billing → stripe + [{ source_id: 1, target_id: 2, weight: 0.9 }], // fetchNodeMemoryIds [], ], @@ -152,31 +154,38 @@ describe('recall-pipeline: Stage 3.5 MindSpring', () => { mindspringToken: 'test-token', }); - // Verify the request body includes graph-expanded terms const fetchCall = (ms.fetch as ReturnType).mock.calls[0]; const body = JSON.parse((fetchCall[1] as RequestInit).body as string) as { query: string }; expect(body.query).toContain('billing'); - // Graph expansion terms should be appended - expect(body.query).toContain('billing stripe'); + expect(body.query).toContain('stripe'); }); it('limits graph expansions in MindSpring query to 5', async () => { + // hub node activates as seed (matches query 'hub'), then spreads to 6 neighbors + // via strictly decreasing edge weights → deterministic activation order const db = createMockDb({ allResults: [ // blocks empty [], - // activateGraph returns 7 seed nodes + // bulk nodes: hub + 6 neighbors [ - { id: 1, label: 'n1', node_type: 'concept' }, - { id: 2, label: 'n2', node_type: 'concept' }, - { id: 3, label: 'n3', node_type: 'concept' }, - { id: 4, label: 'n4', node_type: 'concept' }, - { id: 5, label: 'n5', node_type: 'concept' }, - { id: 6, label: 'n6', node_type: 'concept' }, - { id: 7, label: 'n7', node_type: 'concept' }, + { id: 1, label: 'hub', node_type: 'concept', activation: 0.5 }, + { id: 2, label: 'node-a', node_type: 'concept', activation: 0.3 }, + { id: 3, label: 'node-b', node_type: 'concept', activation: 0.3 }, + { id: 4, label: 'node-c', node_type: 'concept', activation: 0.3 }, + { id: 5, label: 'node-d', node_type: 'concept', activation: 0.3 }, + { id: 6, label: 'node-e', node_type: 'concept', activation: 0.3 }, + { id: 7, label: 'node-f', node_type: 'concept', activation: 0.3 }, + ], + // bulk edges: hub → each neighbor with strictly decreasing weights + [ + { source_id: 1, target_id: 2, weight: 0.9 }, + { source_id: 1, target_id: 3, weight: 0.8 }, + { source_id: 1, target_id: 4, weight: 0.7 }, + { source_id: 1, target_id: 5, weight: 0.6 }, + { source_id: 1, target_id: 6, weight: 0.5 }, + { source_id: 1, target_id: 7, weight: 0.4 }, ], - // hop 0 for each: no neighbors - [], [], [], [], [], [], [], // fetchNodeMemoryIds [], ], @@ -184,7 +193,7 @@ describe('recall-pipeline: Stage 3.5 MindSpring', () => { const ms = createMockMindspring([]); - await recallForQuery('test', { + await recallForQuery('hub', { db, mindspringFetcher: ms, mindspringToken: 'test-token', @@ -192,10 +201,11 @@ describe('recall-pipeline: Stage 3.5 MindSpring', () => { const fetchCall = (ms.fetch as ReturnType).mock.calls[0]; const body = JSON.parse((fetchCall[1] as RequestInit).body as string) as { query: string }; - // Should only have first 5 graph expansions (n1-n5), not n6/n7 - expect(body.query).toContain('n5'); - expect(body.query).not.toContain('n6'); - expect(body.query).not.toContain('n7'); + // hub(1.0) → node-a(0.63) → node-b(0.56) → node-c(0.49) → node-d(0.42) in top 5 + // node-e(0.35) and node-f(0.28) are excluded from the MindSpring query + expect(body.query).toContain('node-d'); + expect(body.query).not.toContain('node-e'); + expect(body.query).not.toContain('node-f'); }); it('skips MindSpring when fetcher is not provided', async () => { diff --git a/web/tests/recall-pipeline.test.ts b/web/tests/recall-pipeline.test.ts index 431792b..512521c 100755 --- a/web/tests/recall-pipeline.test.ts +++ b/web/tests/recall-pipeline.test.ts @@ -132,24 +132,24 @@ describe('recall-pipeline: rrf', () => { describe('recall-pipeline: recallForQuery', () => { it('produces results when all 3 layers have data', async () => { // Blocks: active_context mentions "aegis" - // Graph: activateGraph finds "aegis" → "cloudflare" → "d1" + // Graph: WasmGraph BFS from aegis → cloudflare → d1 // Memory Worker: returns fragments const db = createMockDb({ allResults: [ // Stage 1: getAllBlocks [{ id: 'active_context', content: '**aegis** is the main project', version: 2, priority: 4, max_bytes: 3072, updated_by: 'consolidation', updated_at: '2026-03-13' }], - // Stage 2: activateGraph seed query - [{ id: 1, label: 'aegis', node_type: 'project' }], - // hop 0: neighbors of node 1 - [{ neighbor_id: 2, weight: 0.8, edge_id: 10 }], - // hop 1: neighbors of node 2 - [{ neighbor_id: 3, weight: 0.6, edge_id: 11 }], - // fetch node info for 2, 3 + // Stage 2: activateGraph — bulk SELECT kg_nodes [ - { id: 2, label: 'cloudflare', node_type: 'tool' }, - { id: 3, label: 'd1', node_type: 'tool' }, + { id: 1, label: 'aegis', node_type: 'project', activation: 0.5 }, + { id: 2, label: 'cloudflare', node_type: 'tool', activation: 0.3 }, + { id: 3, label: 'd1', node_type: 'tool', activation: 0.3 }, ], - // fetchNodeMemoryIds: query for labels + // Stage 2: activateGraph — bulk SELECT kg_edges + [ + { source_id: 1, target_id: 2, weight: 0.8 }, + { source_id: 2, target_id: 3, weight: 0.6 }, + ], + // fetchNodeMemoryIds [ { label: 'aegis', memory_ids: '["frag-1"]' }, { label: 'cloudflare', memory_ids: '["frag-2"]' }, @@ -203,11 +203,12 @@ describe('recall-pipeline: recallForQuery', () => { allResults: [ // Stage 1: getAllBlocks (empty) [], - // Stage 2: activateGraph - [{ id: 1, label: 'stripe', node_type: 'tool' }], - // hop 0 + // Stage 2: activateGraph — bulk nodes + [{ id: 1, label: 'stripe', node_type: 'tool', activation: 0.5 }], + // Stage 2: activateGraph — bulk edges + [], + // fetchNodeMemoryIds (called since stripe activated) [], - // no missing nodes to fetch ], }); @@ -226,11 +227,13 @@ describe('recall-pipeline: recallForQuery', () => { allResults: [ // blocks: empty [], - // activateGraph: "pricing" seed → "unified_credits" neighbor - [{ id: 1, label: 'pricing', node_type: 'concept' }], - [{ neighbor_id: 2, weight: 0.9, edge_id: 1 }], - [], // hop 1: no more neighbors - [{ id: 2, label: 'unified_credits', node_type: 'concept' }], + // activateGraph — bulk nodes: pricing + unified_credits + [ + { id: 1, label: 'pricing', node_type: 'concept', activation: 0.5 }, + { id: 2, label: 'unified_credits', node_type: 'concept', activation: 0.3 }, + ], + // activateGraph — bulk edges: pricing → unified_credits + [{ source_id: 1, target_id: 2, weight: 0.9 }], // fetchNodeMemoryIds [{ label: 'unified_credits', memory_ids: '[]' }], ], @@ -242,8 +245,6 @@ describe('recall-pipeline: recallForQuery', () => { const result = await recallForQuery('pricing', { db, memoryBinding: mem }); - // "unified_credits" should appear in graph expansions — semantic search for - // just "pricing" might not find it, but graph activation does expect(result.graphExpansions).toContain('unified_credits'); }); @@ -252,9 +253,10 @@ describe('recall-pipeline: recallForQuery', () => { allResults: [ // blocks [], - // activateGraph - [{ id: 1, label: 'oauth', node_type: 'concept' }], - [], // no neighbors + // activateGraph — bulk nodes + [{ id: 1, label: 'oauth', node_type: 'concept', activation: 0.5 }], + // activateGraph — bulk edges (none) + [], // fetchNodeMemoryIds: oauth links to frag-1 [{ label: 'oauth', memory_ids: '["frag-1"]' }], ], @@ -267,14 +269,11 @@ describe('recall-pipeline: recallForQuery', () => { const result = await recallForQuery('oauth', { db, memoryBinding: mem }); - // frag-1 has both semantic + graph signal, frag-2 has only semantic const bothFact = result.facts.find(f => f.id === 'frag-1'); const semanticOnly = result.facts.find(f => f.id === 'frag-2'); expect(bothFact).toBeDefined(); expect(semanticOnly).toBeDefined(); - - // The "both" fact should rank higher (higher fused score) expect(bothFact!.score).toBeGreaterThan(semanticOnly!.score); expect(bothFact!.source).toBe('both'); expect(semanticOnly!.source).toBe('memory_worker'); @@ -282,7 +281,7 @@ describe('recall-pipeline: recallForQuery', () => { it('timing object is populated', async () => { const db = createMockDb({ - allResults: [[], []], + allResults: [[], [], []], }); const result = await recallForQuery('test', { db }); @@ -302,30 +301,29 @@ describe('recall-pipeline: recallForQuery', () => { allResults: [ // blocks [], - // activateGraph - [{ id: 1, label: 'aegis', node_type: 'project' }], - [], // no neighbors - // fetchNodeMemoryIds: aegis links to frag-1 (same as semantic result) + // activateGraph — bulk nodes + [{ id: 1, label: 'aegis', node_type: 'project', activation: 0.5 }], + // activateGraph — bulk edges + [], + // fetchNodeMemoryIds [{ label: 'aegis', memory_ids: '["frag-1"]' }], ], }); - // Memory Worker returns frag-1 — same as the graph-linked one const mem = createMockMemory([ makeFragment('frag-1', 'AEGIS is the core agent', 'system', 0.9), - makeFragment('frag-1', 'AEGIS is the core agent', 'system', 0.9), // duplicate + makeFragment('frag-1', 'AEGIS is the core agent', 'system', 0.9), ]); const result = await recallForQuery('aegis', { db, memoryBinding: mem }); - // Should only appear once despite being in both semantic and graph results const aegisFacts = result.facts.filter(f => f.id === 'frag-1'); expect(aegisFacts.length).toBe(1); }); it('respects maxResults option', async () => { const db = createMockDb({ - allResults: [[], []], + allResults: [[], [], []], }); const fragments = Array.from({ length: 20 }, (_, i) => @@ -342,16 +340,17 @@ describe('recall-pipeline: recallForQuery', () => { allResults: [ // blocks [], - // activateGraph - [{ id: 1, label: 'aegis', node_type: 'project' }], - [], // no neighbors + // activateGraph — bulk nodes + [{ id: 1, label: 'aegis', node_type: 'project', activation: 0.5 }], + // activateGraph — bulk edges + [], + // fetchNodeMemoryIds (no memory binding so still called but result unused for fusion) ], }); - // No memory binding const result = await recallForQuery('aegis', { db }); - expect(result.facts).toEqual([]); // no memory worker = no facts + expect(result.facts).toEqual([]); expect(result.graphExpansions.length).toBeGreaterThan(0); expect(result.graphExpansions).toContain('aegis'); }); @@ -359,7 +358,7 @@ describe('recall-pipeline: recallForQuery', () => { it('includeGraph=false skips graph activation', async () => { const db = createMockDb({ allResults: [ - // blocks only + // blocks only — graph skipped [], ], }); @@ -381,13 +380,13 @@ describe('recall-pipeline: recallForQuery', () => { describe('recall-pipeline: superset of baseline', () => { it('pipeline includes all memory worker results that baseline would return', async () => { - // Baseline: Memory Worker returns frag-1, frag-2 - // Pipeline should include at least these same fragments const db = createMockDb({ allResults: [ // blocks [], - // activateGraph: no seeds found + // activateGraph — bulk nodes (none matching query) + [], + // activateGraph — bulk edges [], ], }); @@ -400,7 +399,6 @@ describe('recall-pipeline: superset of baseline', () => { const result = await recallForQuery('LLC formation status', { db, memoryBinding: mem }); - // Pipeline result should contain all fragments from the baseline const resultIds = new Set(result.facts.map(f => f.id)); for (const frag of fragments) { expect(resultIds.has(frag.id)).toBe(true); @@ -408,16 +406,17 @@ describe('recall-pipeline: superset of baseline', () => { }); it('pipeline adds graph-enriched results beyond what baseline provides', async () => { - // Baseline would just search for "billing" — pipeline also gets graph expansions const db = createMockDb({ allResults: [ // blocks [], - // activateGraph: "billing" → "stripe" neighbor - [{ id: 1, label: 'billing', node_type: 'concept' }], - [{ neighbor_id: 2, weight: 0.9, edge_id: 1 }], - [], // hop 1 - [{ id: 2, label: 'stripe', node_type: 'tool' }], + // activateGraph — bulk nodes: billing + stripe + [ + { id: 1, label: 'billing', node_type: 'concept', activation: 0.5 }, + { id: 2, label: 'stripe', node_type: 'tool', activation: 0.3 }, + ], + // activateGraph — bulk edges: billing → stripe + [{ source_id: 1, target_id: 2, weight: 0.9 }], // fetchNodeMemoryIds [{ label: 'stripe', memory_ids: '["frag-stripe"]' }], ], @@ -430,8 +429,6 @@ describe('recall-pipeline: superset of baseline', () => { const result = await recallForQuery('billing', { db, memoryBinding: mem }); - // Pipeline should find "stripe" via graph expansion even though baseline - // search for "billing" might not return it expect(result.graphExpansions).toContain('stripe'); expect(result.facts.some(f => f.id === 'frag-stripe')).toBe(true); }); diff --git a/web/tests/router.test.ts b/web/tests/router.test.ts index 2020067..5f5e7eb 100755 --- a/web/tests/router.test.ts +++ b/web/tests/router.test.ts @@ -128,14 +128,14 @@ describe('Router', () => { expect(mockAskGroq).toHaveBeenCalled(); }); - it('parses JSON classification and routes greeting to groq', async () => { + it('parses JSON classification and routes greeting to workers_ai', async () => { mockAskGroq.mockResolvedValue(jsonClass('greeting', 0, false, 0.99)); const intent = makeIntent('hi there'); const { plan } = await route(intent, mockDb, 'fake-key', 'fake-model'); expect(intent.classified).toBe('greeting'); - expect(plan.executor).toBe('gpt_oss'); + expect(plan.executor).toBe('workers_ai'); expect(intent.complexity).toBe(0); expect(intent.needsTools).toBe(false); }); @@ -181,9 +181,33 @@ describe('Router', () => { expect(plan.executor).toBe('direct'); }); - it('greeting → gpt_oss', async () => { + it('greeting → workers_ai', async () => { mockAskGroq.mockResolvedValue(jsonClass('greeting', 0, false, 0.95)); const { plan } = await route(makeIntent('hi'), mockDb, 'k', 'm'); + expect(plan.executor).toBe('workers_ai'); + }); + + it('general_knowledge (no tools, trust zone) → workers_ai via DEFAULT_ROUTES', async () => { + mockAskGroq.mockResolvedValue(jsonClass('general_knowledge', 1, false, 0.95)); + const { plan } = await route(makeIntent('what is REST?'), mockDb, 'k', 'm'); + expect(plan.executor).toBe('workers_ai'); + }); + + it('tarot_pulse → workers_ai', async () => { + mockAskGroq.mockResolvedValue(jsonClass('tarot_pulse', 1, false, 0.92)); + const { plan } = await route(makeIntent('pull a pulse'), mockDb, 'k', 'm'); + expect(plan.executor).toBe('workers_ai'); + }); + + it('tarot_trajectory → workers_ai', async () => { + mockAskGroq.mockResolvedValue(jsonClass('tarot_trajectory', 1, false, 0.90)); + const { plan } = await route(makeIntent('trajectory reading'), mockDb, 'k', 'm'); + expect(plan.executor).toBe('workers_ai'); + }); + + it('tarot_deep still → gpt_oss (complex, tool-capable)', async () => { + mockAskGroq.mockResolvedValue(jsonClass('tarot_deep', 2, false, 0.88)); + const { plan } = await route(makeIntent('deep reading'), mockDb, 'k', 'm'); expect(plan.executor).toBe('gpt_oss'); }); @@ -339,7 +363,7 @@ describe('Router', () => { expect(plan.executor).toBe('gpt_oss'); }); - it('verify zone no-tool low complexity → gpt_oss (bumped from workers_ai)', async () => { + it('verify zone no-tool low complexity → workers_ai', async () => { mockAskGroq.mockResolvedValue(jsonClass('general_knowledge', 1, false, 0.65)); mockAskGroqWithLogprobs.mockResolvedValue({ pattern: 'general_knowledge', @@ -350,7 +374,7 @@ describe('Router', () => { }); const { plan } = await route(makeIntent('simple uncertain'), mockDb, 'k', 'm'); - expect(plan.executor).toBe('gpt_oss'); + expect(plan.executor).toBe('workers_ai'); }); it('verify zone moderate no-tool → claude', async () => { @@ -493,7 +517,7 @@ describe('Router', () => { vi.mocked(getProcedure).mockResolvedValue({ id: 11, task_pattern: 'general_knowledge:mid', - executor: 'workers_ai', + executor: 'claude', executor_config: '{}', success_count: 3, fail_count: 3, @@ -506,7 +530,8 @@ describe('Router', () => { const { plan } = await route(makeIntent('test'), mockDb, 'k', 'm'); expect(plan.reasoning).toContain('degraded'); - expect(plan.executor).toBe('gpt_oss'); + // DEFAULT_ROUTES['general_knowledge'] is now workers_ai + expect(plan.executor).toBe('workers_ai'); }); it('replans broken procedure', async () => { @@ -527,7 +552,7 @@ describe('Router', () => { const { plan } = await route(makeIntent('hi'), mockDb, 'k', 'm'); expect(plan.reasoning).toContain('broken'); - expect(plan.executor).toBe('gpt_oss'); // DEFAULT_ROUTES['greeting'] + expect(plan.executor).toBe('workers_ai'); // DEFAULT_ROUTES['greeting'] }); it('falls to Phase 3 when procedure has insufficient successes', async () => { @@ -606,7 +631,7 @@ describe('Router', () => { const { plan } = await route(makeIntent('hello'), mockDb, 'k', 'm', undefined, mockAi); expect(mockAi.run).toHaveBeenCalled(); - expect(plan.executor).toBe('gpt_oss'); // greeting → gpt_oss + expect(plan.executor).toBe('workers_ai'); // greeting → workers_ai expect(mockAskGroq).not.toHaveBeenCalled(); }); diff --git a/web/vitest.config.ts b/web/vitest.config.ts index 4b29bb6..c4e117a 100755 --- a/web/vitest.config.ts +++ b/web/vitest.config.ts @@ -1,7 +1,14 @@ import { defineConfig } from 'vitest/config'; +import wasm from 'vite-plugin-wasm'; export default defineConfig({ + plugins: [wasm()], test: { include: ['tests/**/*.test.ts'], + server: { + deps: { + inline: ['@stackbilt/wasm-core'], + }, + }, }, });