mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 09:20:22 +00:00
fix: harden qa memory dreaming sweep
This commit is contained in:
52
extensions/memory-core/src/config.test.ts
Normal file
52
extensions/memory-core/src/config.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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-");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user