fix(mnemopi): populated memory_embeddings on remember and auto-derived queryEmbedding on recall

The beam backend never invoked the embedding pipeline during normal
operation: `remember()`/`rememberBatch()`/`updateWorking()` skipped `embed()`
entirely and `recall()`/`recallEnhanced()` never called `embedQuery()` on
the query text. As a result `memory_embeddings` stayed empty in every
deployment and recall silently degraded to FTS-only regardless of the
configured provider (fastembed, OpenAI-compatible API, custom).

- Added `scheduleEmbedding` on `beam.pendingExtractions` (mirroring
  `scheduleFactExtraction`) and wired it from `remember`, `rememberBatch`,
  `updateWorking`, and `consolidateToEpisodic`. Writes
  `INSERT OR REPLACE INTO memory_embeddings(memory_id, embedding_json, model)`
  with the active runtime-options model, captured before the AsyncLocalStorage
  scope exits and re-entered inside the task.
- Auto-derived `queryEmbedding` inside `recall()` via `embedQuery(query)` when
  the caller did not pass one. `queryEmbedding: null` is preserved as the
  explicit FTS-only opt-out; `undefined` triggers auto-derive.
- Propagated `queryEmbedding` through `Mnemopi`'s `toRecallOptions` so the
  facade no longer strips the override on the way to the beam layer.
- Made `Mnemopi.recall`/`recallEnhanced`/`search`/`query`, the module-level
  exports, `BeamMemory.recall`/`recallEnhanced`, the free `recall`/`recallEnhanced`,
  and `orchestrateRecall` async. MCP `handleToolCall`/`callToolJson`/`handleJsonRpc`
  follow suit so the recall handler can await.
- Fixed `withBeam`/`withSharedBeam` to defer `beam.close()` until the async
  handler resolves; otherwise the new async recall hit
  `RangeError: Cannot use a closed database`.
- Updated CLI, MCP entrypoints, coding-agent `MnemopiSessionState`, and every
  affected test to await the new shapes.

Verified with a new regression suite (`test/issue-1832-embedding-population.test.ts`)
exercising both ends of the bug: empty `memory_embeddings` and zero
`dense_score`.

Fixes #1832
This commit is contained in:
roboomp
2026-06-04 09:22:07 +00:00
parent f6fca1f5cd
commit 66b6597829
35 changed files with 565 additions and 233 deletions
@@ -761,7 +761,7 @@ describe("memory_edit.execute (Mnemopi backend)", () => {
await MemoryRetainTool.createIf(makeSession(settings))!.execute("call-memory-edit-store", {
items: [{ content }],
});
const id = registeredMnemopiState?.recallResultsScoped(query)[0]?.id;
const id = (await registeredMnemopiState?.recallResultsScoped(query))?.[0]?.id;
expect(id).toBeString();
return id!;
}
@@ -779,7 +779,7 @@ describe("memory_edit.execute (Mnemopi backend)", () => {
});
expect((result.content[0] as { text: string }).text).toContain("updated");
const recalled = registeredMnemopiState!.recallResultsScoped("accent color");
const recalled = await registeredMnemopiState!.recallResultsScoped("accent color");
expect(recalled.map(memory => memory.content)).toContain("editor accent color is green");
});
@@ -794,7 +794,7 @@ describe("memory_edit.execute (Mnemopi backend)", () => {
});
expect((result.content[0] as { text: string }).text).toContain("deleted");
const recalled = registeredMnemopiState!.recallResultsScoped("deployment note");
const recalled = await registeredMnemopiState!.recallResultsScoped("deployment note");
expect(recalled.map(memory => memory.content)).not.toContain("temporary deployment note can be deleted");
});
@@ -809,7 +809,7 @@ describe("memory_edit.execute (Mnemopi backend)", () => {
});
expect((result.content[0] as { text: string }).text).toContain("invalidated");
const recalled = registeredMnemopiState!.recallResultsScoped("api key rotation");
const recalled = await registeredMnemopiState!.recallResultsScoped("api key rotation");
expect(recalled.map(memory => memory.content)).not.toContain("stale api key rotation policy");
});