mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(memory): keep skipped recall diagnostics opt-in
Follow-up to #92745 after maintainer autoreview found that the skipped recall event widened the shipped MemoryHostEvent union and changed limited legacy reads. Keep readMemoryHostEvents() source-compatible by filtering diagnostic records before applying limits, and expose skipped recall diagnostics through the opt-in MemoryHostEventRecord/readMemoryHostEventRecords path. Original skipped-recall behavior landed in #92745 by @mushuiyu886.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
// Memory Core tests cover memory events plugin behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readMemoryHostEvents } from "openclaw/plugin-sdk/memory-host-events";
|
||||
import {
|
||||
readMemoryHostEventRecords,
|
||||
readMemoryHostEvents,
|
||||
} from "openclaw/plugin-sdk/memory-host-events";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js";
|
||||
import {
|
||||
@@ -135,7 +138,7 @@ describe("memory host event journal integration", () => {
|
||||
minUniqueQueries: 0,
|
||||
nowMs: Date.UTC(2026, 5, 13, 9, 5, 0),
|
||||
});
|
||||
const events = await readMemoryHostEvents({ workspaceDir });
|
||||
const events = await readMemoryHostEventRecords({ workspaceDir });
|
||||
|
||||
expect(candidates).toEqual([]);
|
||||
expect(events.map((event) => event.type)).toEqual(["memory.recall.skipped"]);
|
||||
|
||||
@@ -68,10 +68,12 @@ export type MemoryHostDreamCompletedEvent = {
|
||||
/** Append-only memory host event schema stored as JSONL. */
|
||||
export type MemoryHostEvent =
|
||||
| MemoryHostRecallRecordedEvent
|
||||
| MemoryHostRecallSkippedEvent
|
||||
| MemoryHostPromotionAppliedEvent
|
||||
| MemoryHostDreamCompletedEvent;
|
||||
|
||||
/** Full event-log record schema, including opt-in diagnostic variants. */
|
||||
export type MemoryHostEventRecord = MemoryHostEvent | MemoryHostRecallSkippedEvent;
|
||||
|
||||
/** Resolve the event log path inside a workspace without touching the filesystem. */
|
||||
export function resolveMemoryHostEventLogPath(workspaceDir: string): string {
|
||||
return path.join(workspaceDir, MEMORY_HOST_EVENT_LOG_RELATIVE_PATH);
|
||||
@@ -80,7 +82,7 @@ export function resolveMemoryHostEventLogPath(workspaceDir: string): string {
|
||||
/** Append one memory host event, creating the dreams directory with symlink-safe writes. */
|
||||
export async function appendMemoryHostEvent(
|
||||
workspaceDir: string,
|
||||
event: MemoryHostEvent,
|
||||
event: MemoryHostEventRecord,
|
||||
): Promise<void> {
|
||||
const eventLogPath = resolveMemoryHostEventLogPath(workspaceDir);
|
||||
await fs.mkdir(path.dirname(eventLogPath), { recursive: true });
|
||||
@@ -91,11 +93,28 @@ export async function appendMemoryHostEvent(
|
||||
});
|
||||
}
|
||||
|
||||
/** Read recent memory host events, ignoring corrupt JSONL lines left by partial writes. */
|
||||
export async function readMemoryHostEvents(params: {
|
||||
function parseMemoryHostEventRecord(line: string): MemoryHostEventRecord | null {
|
||||
try {
|
||||
const record = JSON.parse(line) as MemoryHostEventRecord;
|
||||
if (
|
||||
record.type === "memory.recall.recorded" ||
|
||||
record.type === "memory.recall.skipped" ||
|
||||
record.type === "memory.promotion.applied" ||
|
||||
record.type === "memory.dream.completed"
|
||||
) {
|
||||
return record;
|
||||
}
|
||||
} catch {
|
||||
// The log is best-effort diagnostics; one malformed line must not hide
|
||||
// later valid events or break memory status rendering.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function readMemoryHostEventRecordsRaw(params: {
|
||||
workspaceDir: string;
|
||||
limit?: number;
|
||||
}): Promise<MemoryHostEvent[]> {
|
||||
}): Promise<MemoryHostEventRecord[]> {
|
||||
const eventLogPath = resolveMemoryHostEventLogPath(params.workspaceDir);
|
||||
const raw = await fs.readFile(eventLogPath, "utf8").catch((err: unknown) => {
|
||||
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
|
||||
@@ -111,13 +130,8 @@ export async function readMemoryHostEvents(params: {
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.flatMap((line) => {
|
||||
try {
|
||||
return [JSON.parse(line) as MemoryHostEvent];
|
||||
} catch {
|
||||
// The log is best-effort diagnostics; one malformed line must not hide
|
||||
// later valid events or break memory status rendering.
|
||||
return [];
|
||||
}
|
||||
const record = parseMemoryHostEventRecord(line);
|
||||
return record ? [record] : [];
|
||||
});
|
||||
if (!Number.isFinite(params.limit)) {
|
||||
return events;
|
||||
@@ -125,3 +139,31 @@ export async function readMemoryHostEvents(params: {
|
||||
const limit = Math.max(0, Math.floor(params.limit as number));
|
||||
return limit === 0 ? [] : events.slice(-limit);
|
||||
}
|
||||
|
||||
function applyMemoryHostEventLimit<T>(events: T[], limit: number | undefined): T[] {
|
||||
if (!Number.isFinite(limit)) {
|
||||
return events;
|
||||
}
|
||||
const normalizedLimit = Math.max(0, Math.floor(limit as number));
|
||||
return normalizedLimit === 0 ? [] : events.slice(-normalizedLimit);
|
||||
}
|
||||
|
||||
/** Read recent memory host events, ignoring corrupt JSONL lines left by partial writes. */
|
||||
export async function readMemoryHostEvents(params: {
|
||||
workspaceDir: string;
|
||||
limit?: number;
|
||||
}): Promise<MemoryHostEvent[]> {
|
||||
const events = await readMemoryHostEventRecordsRaw({ workspaceDir: params.workspaceDir });
|
||||
const legacyEvents = events.filter(
|
||||
(event): event is MemoryHostEvent => event.type !== "memory.recall.skipped",
|
||||
);
|
||||
return applyMemoryHostEventLimit(legacyEvents, params.limit);
|
||||
}
|
||||
|
||||
/** Read recent memory host event records, including opt-in diagnostic variants. */
|
||||
export async function readMemoryHostEventRecords(params: {
|
||||
workspaceDir: string;
|
||||
limit?: number;
|
||||
}): Promise<MemoryHostEventRecord[]> {
|
||||
return await readMemoryHostEventRecordsRaw(params);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
import { resetPluginStateStoreForTests } from "../plugin-state/plugin-state-store.js";
|
||||
import {
|
||||
appendMemoryHostEvent,
|
||||
readMemoryHostEventRecords,
|
||||
readMemoryHostEvents,
|
||||
resolveMemoryHostEventLogPath,
|
||||
} from "./memory-host-events.js";
|
||||
@@ -76,6 +77,72 @@ describe("memory host event journal helpers", () => {
|
||||
expect(tail).toHaveLength(1);
|
||||
expect(tail[0]?.type).toBe("memory.dream.completed");
|
||||
});
|
||||
|
||||
it("keeps legacy event readers stable when diagnostic records are present", async () => {
|
||||
const workspaceDir = await createTempDir("memory-host-events-diagnostics-");
|
||||
|
||||
await appendMemoryHostEvent(workspaceDir, {
|
||||
type: "memory.recall.skipped",
|
||||
timestamp: "2026-04-05T12:00:00.000Z",
|
||||
query: "durable memory",
|
||||
reason: "non-short-term-memory-path",
|
||||
eligibleResultCount: 0,
|
||||
skippedResultCount: 1,
|
||||
results: [
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 3,
|
||||
endLine: 3,
|
||||
score: 0.9,
|
||||
reason: "non-short-term-memory-path",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await appendMemoryHostEvent(workspaceDir, {
|
||||
type: "memory.recall.recorded",
|
||||
timestamp: "2026-04-05T12:05:00.000Z",
|
||||
query: "daily memory",
|
||||
resultCount: 1,
|
||||
results: [
|
||||
{
|
||||
path: "memory/2026-04-05.md",
|
||||
startLine: 1,
|
||||
endLine: 3,
|
||||
score: 0.95,
|
||||
},
|
||||
],
|
||||
});
|
||||
await appendMemoryHostEvent(workspaceDir, {
|
||||
type: "memory.recall.skipped",
|
||||
timestamp: "2026-04-05T12:10:00.000Z",
|
||||
query: "durable memory again",
|
||||
reason: "non-short-term-memory-path",
|
||||
eligibleResultCount: 1,
|
||||
skippedResultCount: 1,
|
||||
results: [
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 4,
|
||||
endLine: 4,
|
||||
score: 0.8,
|
||||
reason: "non-short-term-memory-path",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const legacyEvents = await readMemoryHostEvents({ workspaceDir });
|
||||
const legacyTail = await readMemoryHostEvents({ workspaceDir, limit: 1 });
|
||||
const records = await readMemoryHostEventRecords({ workspaceDir });
|
||||
|
||||
expect(legacyEvents.map((event) => event.type)).toEqual(["memory.recall.recorded"]);
|
||||
expect(legacyTail.map((event) => event.type)).toEqual(["memory.recall.recorded"]);
|
||||
expect(records.map((event) => event.type)).toEqual([
|
||||
"memory.recall.skipped",
|
||||
"memory.recall.recorded",
|
||||
"memory.recall.skipped",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPersistentDedupe", () => {
|
||||
|
||||
Reference in New Issue
Block a user