diff --git a/extensions/memory-core/src/memory-events.test.ts b/extensions/memory-core/src/memory-events.test.ts index 4bbc03d3df1..3dc8305721e 100644 --- a/extensions/memory-core/src/memory-events.test.ts +++ b/extensions/memory-core/src/memory-events.test.ts @@ -76,6 +76,87 @@ describe("memory host event journal integration", () => { expect(promotionEvent.applied).toBe(1); }); + it("records skipped recall events for durable memory hits excluded from short-term promotion", async () => { + const workspaceDir = await createTempWorkspace("memory-core-skipped-recall-events-"); + await fs.mkdir(path.join(workspaceDir, "memory", "decisoes"), { recursive: true }); + await fs.mkdir(path.join(workspaceDir, "memory", "idiomas"), { recursive: true }); + await fs.writeFile( + path.join(workspaceDir, "MEMORY.md"), + "# Memory\n\nAlpha durable note.\n", + "utf8", + ); + await fs.writeFile( + path.join(workspaceDir, "memory", "decisoes", "2026-06.md"), + "# Decisoes\n\nAlpha monthly decision.\n", + "utf8", + ); + await fs.writeFile( + path.join(workspaceDir, "memory", "idiomas", "PLANO.md"), + "# Plano\n\nAlpha language plan.\n", + "utf8", + ); + + await recordShortTermRecalls({ + workspaceDir, + query: "alpha durable memory", + results: [ + { + path: "MEMORY.md", + startLine: 3, + endLine: 3, + score: 0.91, + snippet: "Alpha durable note.", + source: "memory", + }, + { + path: "memory/decisoes/2026-06.md", + startLine: 3, + endLine: 3, + score: 0.88, + snippet: "Alpha monthly decision.", + source: "memory", + }, + { + path: "memory/idiomas/PLANO.md", + startLine: 3, + endLine: 3, + score: 0.83, + snippet: "Alpha language plan.", + source: "memory", + }, + ], + nowMs: Date.UTC(2026, 5, 13, 9, 0, 0), + }); + + const candidates = await rankShortTermPromotionCandidates({ + workspaceDir, + minScore: 0, + minRecallCount: 0, + minUniqueQueries: 0, + nowMs: Date.UTC(2026, 5, 13, 9, 5, 0), + }); + const events = await readMemoryHostEvents({ workspaceDir }); + + expect(candidates).toEqual([]); + expect(events.map((event) => event.type)).toEqual(["memory.recall.skipped"]); + const skippedEvent = events[0]; + if (skippedEvent?.type !== "memory.recall.skipped") { + throw new Error("expected skipped recall event"); + } + expect(skippedEvent.query).toBe("alpha durable memory"); + expect(skippedEvent.reason).toBe("non-short-term-memory-path"); + expect(skippedEvent.eligibleResultCount).toBe(0); + expect(skippedEvent.skippedResultCount).toBe(3); + expect(skippedEvent.results.map((result) => result.path)).toEqual([ + "MEMORY.md", + "memory/decisoes/2026-06.md", + "memory/idiomas/PLANO.md", + ]); + expect( + skippedEvent.results.every((result) => result.reason === "non-short-term-memory-path"), + ).toBe(true); + }); + it("records dreaming completion events when phase artifacts are written", async () => { const workspaceDir = await createTempWorkspace("memory-core-dream-events-"); diff --git a/extensions/memory-core/src/short-term-promotion.ts b/extensions/memory-core/src/short-term-promotion.ts index 734c0495515..73b4ca1cca2 100644 --- a/extensions/memory-core/src/short-term-promotion.ts +++ b/extensions/memory-core/src/short-term-promotion.ts @@ -1355,6 +1355,29 @@ export async function filterLiveShortTermRecallEntries(params: { return results.filter((result) => result.exists).map((result) => result.entry); } +function buildMemoryRecallSkippedEvent(params: { + timestamp: string; + query: string; + eligibleResultCount: number; + skipped: MemorySearchResult[]; +}) { + return { + type: "memory.recall.skipped" as const, + timestamp: params.timestamp, + query: params.query, + reason: "non-short-term-memory-path" as const, + eligibleResultCount: params.eligibleResultCount, + skippedResultCount: params.skipped.length, + results: params.skipped.map((result) => ({ + path: normalizeMemoryPath(result.path), + startLine: Math.max(1, Math.floor(result.startLine)), + endLine: Math.max(1, Math.floor(result.endLine)), + score: clampScore(result.score), + reason: "non-short-term-memory-path" as const, + })), + }; +} + export async function recordShortTermRecalls(params: { workspaceDir?: string; query: string; @@ -1373,15 +1396,27 @@ export async function recordShortTermRecalls(params: { if (!query) { return; } - const relevant = params.results.filter( - (result) => result.source === "memory" && isShortTermMemoryPath(result.path), - ); - if (relevant.length === 0) { + const memoryResults = params.results.filter((result) => result.source === "memory"); + const relevant = memoryResults.filter((result) => isShortTermMemoryPath(result.path)); + const skipped = memoryResults.filter((result) => !isShortTermMemoryPath(result.path)); + if (relevant.length === 0 && skipped.length === 0) { return; } const nowMs = resolveMemoryCoreNowMs(params.nowMs); const nowIso = resolveMemoryCoreTimestamp(nowMs); + if (relevant.length === 0) { + await appendMemoryHostEvent( + workspaceDir, + buildMemoryRecallSkippedEvent({ + timestamp: nowIso, + query, + eligibleResultCount: relevant.length, + skipped, + }), + ); + return; + } const signalType = params.signalType ?? "recall"; const queryHash = hashQuery(query); const todayBucket = @@ -1466,6 +1501,17 @@ export async function recordShortTermRecalls(params: { score: clampScore(result.score), })), }); + if (skipped.length > 0) { + await appendMemoryHostEvent( + workspaceDir, + buildMemoryRecallSkippedEvent({ + timestamp: nowIso, + query, + eligibleResultCount: relevant.length, + skipped, + }), + ); + } }); } diff --git a/src/memory-host-sdk/events.ts b/src/memory-host-sdk/events.ts index 50424ea5816..cb04eb4202a 100644 --- a/src/memory-host-sdk/events.ts +++ b/src/memory-host-sdk/events.ts @@ -21,6 +21,23 @@ export type MemoryHostRecallRecordedEvent = { }>; }; +/** Event emitted when recall hits are visible but excluded from short-term promotion. */ +export type MemoryHostRecallSkippedEvent = { + type: "memory.recall.skipped"; + timestamp: string; + query: string; + reason: "non-short-term-memory-path"; + eligibleResultCount: number; + skippedResultCount: number; + results: Array<{ + path: string; + startLine: number; + endLine: number; + score: number; + reason: "non-short-term-memory-path"; + }>; +}; + /** Event emitted when deep-dream candidates are promoted into durable memory. */ export type MemoryHostPromotionAppliedEvent = { type: "memory.promotion.applied"; @@ -51,6 +68,7 @@ export type MemoryHostDreamCompletedEvent = { /** Append-only memory host event schema stored as JSONL. */ export type MemoryHostEvent = | MemoryHostRecallRecordedEvent + | MemoryHostRecallSkippedEvent | MemoryHostPromotionAppliedEvent | MemoryHostDreamCompletedEvent;