fix: harden qa memory dreaming sweep

This commit is contained in:
Peter Steinberger
2026-04-07 12:56:24 +01:00
parent ead634812e
commit 1cec37184c
18 changed files with 889 additions and 41 deletions

View File

@@ -0,0 +1,52 @@
import fs from "node:fs";
import { describe, expect, it } from "vitest";
import { validateJsonSchemaValue } from "../../../src/plugins/schema-validator.js";
const manifest = JSON.parse(
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
) as { configSchema: Record<string, unknown> };
describe("memory-core manifest config schema", () => {
it("accepts dreaming phase thresholds used by QA and runtime", () => {
const result = validateJsonSchemaValue({
schema: manifest.configSchema,
cacheKey: "memory-core.manifest.dreaming-phase-thresholds",
value: {
dreaming: {
enabled: true,
timezone: "Europe/London",
verboseLogging: true,
storage: {
mode: "inline",
separateReports: false,
},
phases: {
light: {
enabled: true,
lookbackDays: 2,
limit: 20,
dedupeSimilarity: 0.9,
},
deep: {
enabled: true,
limit: 10,
minScore: 0,
minRecallCount: 3,
minUniqueQueries: 3,
recencyHalfLifeDays: 14,
maxAgeDays: 30,
},
rem: {
enabled: true,
lookbackDays: 7,
limit: 10,
minPatternStrength: 0.75,
},
},
},
},
});
expect(result.ok).toBe(true);
});
});

View File

@@ -186,6 +186,44 @@ describe("memory-core dreaming phases", () => {
});
});
it("triggers light dreaming when the token is embedded in a reminder body", async () => {
const workspaceDir = await createDreamingWorkspace();
await withDreamingTestClock(async () => {
await writeDailyNote(workspaceDir, [
`# ${DREAMING_TEST_DAY}`,
"",
"- Move backups to S3 Glacier.",
"- Keep retention at 365 days.",
]);
const { beforeAgentReply } = createLightDreamingHarness(workspaceDir);
setDreamingTestTime(1);
await beforeAgentReply(
{
cleanedBody: [
"System: rotate logs",
"System: __openclaw_memory_core_light_sleep__",
"",
"A scheduled reminder has been triggered. The reminder content is:",
"",
"rotate logs",
"__openclaw_memory_core_light_sleep__",
"",
"Handle this reminder internally. Do not relay it to the user unless explicitly requested.",
].join("\n"),
},
{ trigger: "heartbeat", workspaceDir },
);
const dailyContent = await fs.readFile(
path.join(workspaceDir, "memory", `${DREAMING_TEST_DAY}.md`),
"utf-8",
);
expect(dailyContent).toContain("## Light Sleep");
expect(dailyContent).toContain("Move backups to S3 Glacier.");
});
});
it("stops stripping a malformed managed block at the next section boundary", async () => {
const workspaceDir = await createDreamingWorkspace();
await withDreamingTestClock(async () => {

View File

@@ -20,7 +20,12 @@ import {
} from "openclaw/plugin-sdk/memory-core-host-status";
import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js";
import { generateAndAppendDreamNarrative, type NarrativePhaseData } from "./dreaming-narrative.js";
import { asRecord, formatErrorMessage, normalizeTrimmedString } from "./dreaming-shared.js";
import {
asRecord,
formatErrorMessage,
includesSystemEventToken,
normalizeTrimmedString,
} from "./dreaming-shared.js";
import {
readShortTermRecallEntries,
recordDreamingPhaseSignals,
@@ -1521,7 +1526,10 @@ async function runPhaseIfTriggered(params: {
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
});
}): Promise<{ handled: true; reason: string } | undefined> {
if (params.trigger !== "heartbeat" || params.cleanedBody.trim() !== params.eventText) {
if (
params.trigger !== "heartbeat" ||
!includesSystemEventToken(params.cleanedBody, params.eventText)
) {
return undefined;
}
if (!params.config.enabled) {

View File

@@ -8,3 +8,15 @@ export function normalizeTrimmedString(value: unknown): string | undefined {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
export function includesSystemEventToken(cleanedBody: string, eventText: string): boolean {
const normalizedBody = normalizeTrimmedString(cleanedBody);
const normalizedEventText = normalizeTrimmedString(eventText);
if (!normalizedBody || !normalizedEventText) {
return false;
}
if (normalizedBody === normalizedEventText) {
return true;
}
return normalizedBody.split(/\r?\n/).some((line) => line.trim() === normalizedEventText);
}

View File

@@ -704,6 +704,58 @@ describe("short-term dreaming trigger", () => {
expect(memoryText).toContain("Move backups to S3 Glacier.");
});
it("applies promotions when the managed dreaming token is embedded in a reminder body", async () => {
const logger = createLogger();
const workspaceDir = await createTempWorkspace("memory-dreaming-composite-");
await writeDailyMemoryNote(workspaceDir, "2026-04-02", ["Move backups to S3 Glacier."]);
await recordShortTermRecalls({
workspaceDir,
query: "backup policy",
results: [
{
path: "memory/2026-04-02.md",
startLine: 1,
endLine: 1,
score: 0.9,
snippet: "Move backups to S3 Glacier.",
source: "memory",
},
],
});
const result = await runShortTermDreamingPromotionIfTriggered({
cleanedBody: [
"System: rotate logs",
"System: __openclaw_memory_core_short_term_promotion_dream__",
"",
"A scheduled reminder has been triggered. The reminder content is:",
"",
"rotate logs",
"__openclaw_memory_core_short_term_promotion_dream__",
"",
"Handle this reminder internally. Do not relay it to the user unless explicitly requested.",
].join("\n"),
trigger: "heartbeat",
workspaceDir,
config: {
enabled: true,
cron: constants.DEFAULT_DREAMING_CRON_EXPR,
limit: 10,
minScore: 0,
minRecallCount: 0,
minUniqueQueries: 0,
recencyHalfLifeDays: constants.DEFAULT_DREAMING_RECENCY_HALF_LIFE_DAYS,
verboseLogging: false,
},
logger,
});
expect(result?.handled).toBe(true);
const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8");
expect(memoryText).toContain("Move backups to S3 Glacier.");
});
it("keeps one-off recalls out of long-term memory under default thresholds", async () => {
const logger = createLogger();
const workspaceDir = await createTempWorkspace("memory-dreaming-strict-");

View File

@@ -13,7 +13,12 @@ import {
import { writeDeepDreamingReport } from "./dreaming-markdown.js";
import { generateAndAppendDreamNarrative, type NarrativePhaseData } from "./dreaming-narrative.js";
import { runDreamingSweepPhases } from "./dreaming-phases.js";
import { asRecord, formatErrorMessage, normalizeTrimmedString } from "./dreaming-shared.js";
import {
asRecord,
formatErrorMessage,
includesSystemEventToken,
normalizeTrimmedString,
} from "./dreaming-shared.js";
import {
applyShortTermPromotions,
repairShortTermPromotionArtifacts,
@@ -418,7 +423,7 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
if (params.trigger !== "heartbeat") {
return undefined;
}
if (params.cleanedBody.trim() !== DREAMING_SYSTEM_EVENT_TEXT) {
if (!includesSystemEventToken(params.cleanedBody, DREAMING_SYSTEM_EVENT_TEXT)) {
return undefined;
}
if (!params.config.enabled) {