fix(mnemopi): routed embed projections through recall

Scored working-memory recall candidates against embed_text when present so FTS matches from the projection survive the lexical gate even without dense embeddings.

Fixes #4395
This commit is contained in:
roboomp
2026-07-03 05:07:13 +00:00
parent ed715eda7b
commit f021ee577c
2 changed files with 10 additions and 5 deletions
+8 -5
View File
@@ -507,6 +507,8 @@ function buildWhere(
const MEMORY_COLUMNS =
"id, content, source, timestamp, session_id, importance, metadata_json, veracity, memory_type, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id, event_date, event_date_precision, temporal_tags";
const WORKING_MEMORY_COLUMNS =
"id, content, embed_text, source, timestamp, session_id, importance, metadata_json, veracity, memory_type, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id, event_date, event_date_precision, temporal_tags";
const EPISODIC_COLUMNS = `${MEMORY_COLUMNS}, rowid, summary_of, tier`;
function ftsRows(
@@ -624,7 +626,7 @@ function fetchCandidates(
if (idsOrRowids.length === 0) return [];
const table = tierLabel === "working" ? "working_memory" : "episodic_memory";
const keyColumn = tierLabel === "working" ? "id" : "rowid";
const columns = tierLabel === "working" ? MEMORY_COLUMNS : EPISODIC_COLUMNS;
const columns = tierLabel === "working" ? WORKING_MEMORY_COLUMNS : EPISODIC_COLUMNS;
const { where, params } = buildWhere(beam, "m", options);
const rows = queryAll(
beam,
@@ -662,7 +664,7 @@ function fallbackCandidates(
options: RecallOptionsInternal,
): MemoryCandidate[] {
const table = tierLabel === "working" ? "working_memory" : "episodic_memory";
const columns = tierLabel === "working" ? MEMORY_COLUMNS : EPISODIC_COLUMNS;
const columns = tierLabel === "working" ? WORKING_MEMORY_COLUMNS : EPISODIC_COLUMNS;
const { where, params } = buildWhere(beam, "", options);
const rows = queryAll(beam, `SELECT ${columns} FROM ${table} WHERE ${where} ORDER BY timestamp DESC LIMIT ?`, [
...params,
@@ -684,10 +686,11 @@ function scoreCandidate(
options: RecallOptionsInternal,
): RecallResult | null {
const content = asString(candidate.row.content);
const searchableContent = asString(candidate.row.embed_text) || content;
const lexical =
queryGroups.length > 0
? lexicalGroupRelevance(queryGroups, content, normalizedQueryLower)
: lexicalRelevance(queryTokens, content, normalizedQueryLower);
? lexicalGroupRelevance(queryGroups, searchableContent, normalizedQueryLower)
: lexicalRelevance(queryTokens, searchableContent, normalizedQueryLower);
const minRel = minimumRelevance(queryTokens);
if (lexical < minRel && candidate.signals.dense < 0.65) return null;
const [vecWeight, ftsWeight, importanceWeight] = weights;
@@ -732,7 +735,7 @@ function scoreCandidate(
const tierWeight = degradationTier === 1 ? 1 : degradationTier === 2 ? 0.85 : 0.7;
score *= tierWeight;
}
score *= veracityWeight * currentContentAdjustment(content, options.currentSensitive === true);
score *= veracityWeight * currentContentAdjustment(searchableContent, options.currentSensitive === true);
const result: RecallResult = {
...candidate.row,
id: asString(candidate.row.id),
@@ -120,6 +120,8 @@ describe("issue #1832 — embedding write/read coverage", () => {
]);
expect(memory.conn.query("SELECT id FROM fts_working WHERE fts_working MATCH ?").all("role")).toEqual([]);
expect(JSON.parse(readEmbeddings(memory)[0]?.embedding_json ?? "[]")).toEqual([1, 0, 0, 0]);
const ftsOnlyRecall = await memory.recall("clean", 5, { queryEmbedding: null });
expect(ftsOnlyRecall[0]).toMatchObject({ id, content: raw });
} finally {
memory.close();
}