fix(memory): surface skipped short-term recall hits (#92745)

Record diagnostic events when memory_search returns durable memory hits that are intentionally excluded from short-term promotion, so users can distinguish eligibility decisions from recall tracking failures.
This commit is contained in:
mushuiyu_xydt
2026-06-14 08:05:52 +08:00
committed by GitHub
parent d626e99c31
commit 99e7dad0e4
3 changed files with 149 additions and 4 deletions

View File

@@ -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-");

View File

@@ -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,
}),
);
}
});
}

View File

@@ -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;