mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 11:48:11 +00:00
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:
@@ -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-");
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user