feat(sf): /sf memory search — embedding-ranked memory query

New subcommand: /sf memory search "<query>". Routes through
getRelevantMemoriesRanked, so when SF_LLM_GATEWAY_KEY is set the gateway
embeds the query and ranks memories by cosine + static blend; without
the key, gracefully degrades to static ranking. Header text indicates
which path was taken so users know whether embeddings are live.

This makes the embedding pipeline operator-discoverable — previously the
only consumer was the silent execute-task injection path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-02 22:22:33 +02:00
parent eb5f7ef7b6
commit e5787794f3

View file

@ -26,6 +26,7 @@ import {
enforceMemoryCap,
getActiveMemories,
getActiveMemoriesRanked,
getRelevantMemoriesRanked,
supersedeMemory,
} from "./memory-store.js";
import { _getAdapter, isDbAvailable } from "./sf-db.js";
@ -117,6 +118,9 @@ export async function handleMemory(
case "list":
handleList(ctx);
return;
case "search":
await handleSearch(ctx, parsed);
return;
case "show":
handleShow(ctx, parsed.positional[0]);
return;
@ -160,6 +164,7 @@ function usage(): string {
return [
"Usage: /sf memory <subcommand>",
" list list recent active memories",
' search "<query>" embedding-ranked search (gateway-aware; static fallback)',
" show <MEM###> print one memory",
" forget <MEM###> supersede a memory",
" stats counts by category / sources / edges",
@ -199,6 +204,40 @@ function handleList(ctx: ExtensionCommandContext): void {
ctx.ui.notify(lines.join("\n"), "info");
}
async function handleSearch(
ctx: ExtensionCommandContext,
parsed: MemoryCmdArgs,
): Promise<void> {
if (!isDbAvailable()) {
ctx.ui.notify("No SF database available.", "warning");
return;
}
const query = parsed.positional.join(" ").trim();
if (!query) {
ctx.ui.notify(
'Usage: /sf memory search "<query>" (uses embeddings when SF_LLM_GATEWAY_KEY is set; static fallback otherwise)',
"warning",
);
return;
}
const memories = await getRelevantMemoriesRanked(query, 10);
if (memories.length === 0) {
ctx.ui.notify("No matches.", "info");
return;
}
const usingEmbeddings = !!process.env.SF_LLM_GATEWAY_KEY;
const header = usingEmbeddings
? `Top ${memories.length} memories for "${truncate(query, 60)}" (embedding-ranked):`
: `Top ${memories.length} memories for "${truncate(query, 60)}" (static rank — set SF_LLM_GATEWAY_KEY for embeddings):`;
const lines = [header];
for (const m of memories) {
lines.push(
` [${m.id}] (${m.category}, conf ${m.confidence.toFixed(2)}) ${truncate(m.content, 100)}`,
);
}
ctx.ui.notify(lines.join("\n"), "info");
}
function handleShow(ctx: ExtensionCommandContext, id: string | undefined): void {
if (!id) {
ctx.ui.notify("Usage: /sf memory show <MEM###>", "warning");