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); } } /**