diff --git a/backend/src/memory/vectors.ts b/backend/src/memory/vectors.ts index d1b2f7f..b5f3990 100644 --- a/backend/src/memory/vectors.ts +++ b/backend/src/memory/vectors.ts @@ -49,6 +49,22 @@ function memoryText(collision: Collision): string { .join('\n'); } +function cosine(a: number[], b: number[]): number { + const n = Math.min(a.length, b.length); + let dot = 0; + let aNorm = 0; + let bNorm = 0; + for (let i = 0; i < n; i++) { + const av = a[i] ?? 0; + const bv = b[i] ?? 0; + dot += av * bv; + aNorm += av * av; + bNorm += bv * bv; + } + if (!aNorm || !bNorm) return -1; + return dot / (Math.sqrt(aNorm) * Math.sqrt(bNorm)); +} + async function embed(text: string, inputType: 'document' | 'query'): Promise { return (await embedWithVoyage(text, inputType)) ?? embedWithGemini(text, inputType); } @@ -159,8 +175,8 @@ async function recallByVector(collision: Collision): Promise('collisions') .aggregate([ @@ -180,9 +196,27 @@ async function recallByVector(collision: Collision): Promise('collisions') + .find( + { + podId: collision.podId, + id: { $ne: collision.id }, + embedding: { $exists: true }, + }, + { projection: { _id: 0 }, limit: 100 }, + ) + .toArray(); + let best: { match: StoredCollision; score: number } | null = null; + for (const candidate of candidates) { + if (!candidate.embedding?.length) continue; + const score = cosine(queryVector, candidate.embedding); + if (!best || score > best.score) best = { match: candidate, score }; + } + return best && best.score > 0.5 ? attachOutcome(best.match) : null; } async function recallBySignature(collision: Collision): Promise { @@ -212,9 +246,10 @@ async function recallBySignature(collision: Collision): Promise { - return (await recallBySignature(collision)) ?? recallByVector(collision); + return (await recallByVector(collision)) ?? recallBySignature(collision); } diff --git a/scripts/verify-backend.mjs b/scripts/verify-backend.mjs index 266fdd6..9583f5b 100644 --- a/scripts/verify-backend.mjs +++ b/scripts/verify-backend.mjs @@ -163,6 +163,13 @@ async function verifyMemoryRecall() { } const recalled = await recallSimilar({ ...seed, id: `${seed.id}_query` }); if (!recalled) fail('memory recall did not find seeded collision'); + const vectorRecalled = await recallSimilar({ + ...seed, + id: `${seed.id}_vector_query`, + file: 'src/nearby-memory.ts', + symbol: 'nearbyMemory', + }); + if (!vectorRecalled) fail('vector memory recall did not find semantically similar collision'); } async function verifyGraph() {