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>
This commit is contained in:
parent
bfb892eca3
commit
5e518dd7d4
1 changed files with 36 additions and 4 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue