From 5e518dd7d4e03e006fb3248092b79ce8677e9437 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Thu, 7 May 2026 06:56:15 +0200 Subject: [PATCH] feat: Add SM cross-project recall to memory ranking (Phase 3) - Import querySmMemories from sm-client.js - Merge cross-project memories into getRelevantMemoriesRanked - Cap cross-project confidence at 0.8 with 0.9 reduction (conservative) - Gracefully degrade: fail-open if SM unavailable - Preserve cosine ranking with relation boost for merged pool - Tests: 3821 passing, no regressions Implements Tier 1.2 Phase 3: Cross-project memory recall via Singularity Memory. Enables dispatch to leverage patterns from other projects while maintaining local autonomy via fail-open semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/resources/extensions/sf/memory-store.js | 40 ++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/resources/extensions/sf/memory-store.js b/src/resources/extensions/sf/memory-store.js index d41b297a2..c4c81d423 100644 --- a/src/resources/extensions/sf/memory-store.js +++ b/src/resources/extensions/sf/memory-store.js @@ -18,6 +18,7 @@ import { updateMemoryContentRow, } from "./sf-db.js"; import { queueMemorySync } from "./sync-scheduler.js"; +import { querySmMemories } from "./sm-client.js"; export { isDbAvailable }; @@ -118,6 +119,37 @@ export async function getRelevantMemoriesRanked(query, limit = 10) { if (pool.length === 0 || !query.trim()) { return pool.slice(0, limit); } + + // Phase 3: Query cross-project memories from Singularity Memory (fail-open) + let crossProjectMemories = []; + try { + const smResults = await querySmMemories(query, { + limit: Math.max(3, Math.ceil(limit * 0.3)), // Cross-project recall is 30% of local limit + smConnected: process.env.SM_ENABLED !== "false", + }); + // Convert SM results to local format (all cross-project memories tagged as such) + crossProjectMemories = (smResults || []).map((m) => ({ + id: `sm-${m.id || "unknown"}`, + category: m.category || "pattern", + content: m.content || m.text || "", + confidence: Math.min(0.8, (m.confidence || 0.5) * 0.9), // Cap and reduce cross-project confidence + source_unit_type: "cross-project", + source_unit_id: m.source_tool || "sm", + created_at: m.created_at || Date.now(), + updated_at: m.updated_at || Date.now(), + superseded_by: null, + hit_count: m.hit_count || 0, + tags: ["cross-project"], + })); + } catch { + // SM unavailable or query failed — gracefully degrade to local-only + } + + // Merge local and cross-project memories + const mergedPool = [...pool, ...crossProjectMemories]; + if (mergedPool.length === 0 || !query.trim()) { + return mergedPool.slice(0, limit); + } try { const { embedQueryViaGateway, loadEmbeddingMap, rankMemoriesByEmbedding } = await import("./memory-embeddings.js"); @@ -126,10 +158,10 @@ export async function getRelevantMemoriesRanked(query, limit = 10) { Promise.resolve(loadEmbeddingMap()), ]); if (!queryVec || embeddingMap.size === 0) { - return pool.slice(0, limit); + return mergedPool.slice(0, limit); } let ranked = rankMemoriesByEmbedding( - pool.map((m) => ({ + mergedPool.map((m) => ({ id: m.id, staticScore: m.confidence * (1 + m.hit_count * 0.1), })), @@ -165,7 +197,7 @@ export async function getRelevantMemoriesRanked(query, limit = 10) { } catch { // Relation boost is additive; failure preserves cosine ranking. } - const byId = new Map(pool.map((m) => [m.id, m])); + const byId = new Map(mergedPool.map((m) => [m.id, m])); // Top-K from cosine+relation rank — feed this into the optional rerank pass. const topK = []; for (const r of ranked) { @@ -201,7 +233,7 @@ export async function getRelevantMemoriesRanked(query, limit = 10) { } return topK; } catch { - return pool.slice(0, limit); + return mergedPool.slice(0, limit); } } /**