mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 11:50:23 +00:00
Memory: move dreaming trail to dreams.md (#61537)
* Memory: move dreaming trail to dreams.md * docs(changelog): add dreams.md entry --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -79,13 +79,11 @@ describe("memory-core /dreaming command", () => {
|
||||
|
||||
expect(result.text).toContain("Usage: /dreaming status");
|
||||
expect(result.text).toContain("Dreaming status:");
|
||||
expect(result.text).toContain("- light: sorts recent memory traces into the daily note.");
|
||||
expect(result.text).toContain("- light: sorts recent memory traces into dreams.md.");
|
||||
expect(result.text).toContain(
|
||||
"- deep: promotes durable memories into MEMORY.md and handles recovery when memory is thin.",
|
||||
);
|
||||
expect(result.text).toContain(
|
||||
"- rem: writes reflection and pattern notes into the daily note.",
|
||||
);
|
||||
expect(result.text).toContain("- rem: writes reflection and pattern notes into dreams.md.");
|
||||
});
|
||||
|
||||
it("persists global enablement under plugins.entries.memory-core.config.dreaming.enabled", async () => {
|
||||
|
||||
@@ -99,9 +99,9 @@ function formatEnabled(value: boolean): string {
|
||||
|
||||
function formatPhaseGuide(): string {
|
||||
return [
|
||||
"- light: sorts recent memory traces into the daily note.",
|
||||
"- light: sorts recent memory traces into dreams.md.",
|
||||
"- deep: promotes durable memories into MEMORY.md and handles recovery when memory is thin.",
|
||||
"- rem: writes reflection and pattern notes into the daily note.",
|
||||
"- rem: writes reflection and pattern notes into dreams.md.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
|
||||
88
extensions/memory-core/src/dreaming-markdown.test.ts
Normal file
88
extensions/memory-core/src/dreaming-markdown.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { writeDailyDreamingPhaseBlock, writeDeepDreamingReport } from "./dreaming-markdown.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempWorkspace(): Promise<string> {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-dreaming-markdown-"));
|
||||
tempDirs.push(workspaceDir);
|
||||
return workspaceDir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("dreaming markdown storage", () => {
|
||||
it("writes inline light dreaming output into top-level dreams.md", async () => {
|
||||
const workspaceDir = await createTempWorkspace();
|
||||
|
||||
const result = await writeDailyDreamingPhaseBlock({
|
||||
workspaceDir,
|
||||
phase: "light",
|
||||
bodyLines: ["- Candidate: remember the API key is fake"],
|
||||
storage: {
|
||||
mode: "inline",
|
||||
separateReports: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.inlinePath).toBe(path.join(workspaceDir, "dreams.md"));
|
||||
const content = await fs.readFile(result.inlinePath!, "utf-8");
|
||||
expect(content).toContain("## Light Sleep");
|
||||
expect(content).toContain("- Candidate: remember the API key is fake");
|
||||
});
|
||||
|
||||
it("keeps multiple inline phases in the shared top-level dreams.md file", async () => {
|
||||
const workspaceDir = await createTempWorkspace();
|
||||
|
||||
await writeDailyDreamingPhaseBlock({
|
||||
workspaceDir,
|
||||
phase: "light",
|
||||
bodyLines: ["- Candidate: first block"],
|
||||
storage: {
|
||||
mode: "inline",
|
||||
separateReports: false,
|
||||
},
|
||||
});
|
||||
await writeDailyDreamingPhaseBlock({
|
||||
workspaceDir,
|
||||
phase: "rem",
|
||||
bodyLines: ["- Theme: `focus` kept surfacing."],
|
||||
storage: {
|
||||
mode: "inline",
|
||||
separateReports: false,
|
||||
},
|
||||
});
|
||||
|
||||
const dreamsPath = path.join(workspaceDir, "dreams.md");
|
||||
const content = await fs.readFile(dreamsPath, "utf-8");
|
||||
expect(content).toContain("## Light Sleep");
|
||||
expect(content).toContain("## REM Sleep");
|
||||
expect(content).toContain("- Candidate: first block");
|
||||
expect(content).toContain("- Theme: `focus` kept surfacing.");
|
||||
});
|
||||
|
||||
it("still writes deep reports to the per-phase report directory", async () => {
|
||||
const workspaceDir = await createTempWorkspace();
|
||||
|
||||
const reportPath = await writeDeepDreamingReport({
|
||||
workspaceDir,
|
||||
bodyLines: ["- Promoted: durable preference"],
|
||||
storage: {
|
||||
mode: "separate",
|
||||
separateReports: false,
|
||||
},
|
||||
nowMs: Date.parse("2026-04-05T10:00:00Z"),
|
||||
timezone: "UTC",
|
||||
});
|
||||
|
||||
expect(reportPath).toBe(path.join(workspaceDir, "memory", "dreaming", "deep", "2026-04-05.md"));
|
||||
const content = await fs.readFile(reportPath!, "utf-8");
|
||||
expect(content).toContain("# Deep Sleep");
|
||||
expect(content).toContain("- Promoted: durable preference");
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,8 @@ const DAILY_PHASE_LABELS: Record<Exclude<MemoryDreamingPhaseName, "deep">, strin
|
||||
rem: "rem",
|
||||
};
|
||||
|
||||
const DREAMS_FILENAME = "dreams.md";
|
||||
|
||||
function resolvePhaseMarkers(phase: Exclude<MemoryDreamingPhaseName, "deep">): {
|
||||
start: string;
|
||||
end: string;
|
||||
@@ -57,9 +59,8 @@ function escapeRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function resolveDailyMemoryPath(workspaceDir: string, epochMs: number, timezone?: string): string {
|
||||
const isoDay = formatMemoryDreamingDay(epochMs, timezone);
|
||||
return path.join(workspaceDir, "memory", `${isoDay}.md`);
|
||||
function resolveDreamsPath(workspaceDir: string): string {
|
||||
return path.join(workspaceDir, DREAMS_FILENAME);
|
||||
}
|
||||
|
||||
function resolveSeparateReportPath(
|
||||
@@ -94,7 +95,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
|
||||
let reportPath: string | undefined;
|
||||
|
||||
if (shouldWriteInline(params.storage)) {
|
||||
inlinePath = resolveDailyMemoryPath(params.workspaceDir, nowMs, params.timezone);
|
||||
inlinePath = resolveDreamsPath(params.workspaceDir);
|
||||
await fs.mkdir(path.dirname(inlinePath), { recursive: true });
|
||||
const original = await fs.readFile(inlinePath, "utf-8").catch((err: unknown) => {
|
||||
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
|
||||
|
||||
@@ -15,7 +15,7 @@ const MEMORY_FLUSH_TARGET_HINT =
|
||||
const MEMORY_FLUSH_APPEND_ONLY_HINT =
|
||||
"If memory/YYYY-MM-DD.md already exists, APPEND new content only and do not overwrite existing entries.";
|
||||
const MEMORY_FLUSH_READ_ONLY_HINT =
|
||||
"Treat workspace bootstrap/reference files such as MEMORY.md, SOUL.md, TOOLS.md, and AGENTS.md as read-only during this flush; never overwrite, replace, or edit them.";
|
||||
"Treat workspace bootstrap/reference files such as MEMORY.md, dreams.md, SOUL.md, TOOLS.md, and AGENTS.md as read-only during this flush; never overwrite, replace, or edit them.";
|
||||
const MEMORY_FLUSH_REQUIRED_HINTS = [
|
||||
MEMORY_FLUSH_TARGET_HINT,
|
||||
MEMORY_FLUSH_APPEND_ONLY_HINT,
|
||||
|
||||
Reference in New Issue
Block a user