From e5787794f3c346e3058aff2382f1bd01cf060119 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 22:22:33 +0200 Subject: [PATCH] =?UTF-8?q?feat(sf):=20/sf=20memory=20search=20=E2=80=94?= =?UTF-8?q?=20embedding-ranked=20memory=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New subcommand: /sf memory search "". 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) --- .../extensions/sf/commands-memory.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/resources/extensions/sf/commands-memory.ts b/src/resources/extensions/sf/commands-memory.ts index 4d9dbc7a0..a54528b36 100644 --- a/src/resources/extensions/sf/commands-memory.ts +++ b/src/resources/extensions/sf/commands-memory.ts @@ -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 ", " list list recent active memories", + ' search "" embedding-ranked search (gateway-aware; static fallback)', " show print one memory", " forget 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 { + 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 "" (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 ", "warning");