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:
Dave Morin 2026-04-05 12:19:31 -10:00 committed by GitHub
parent 48611ec40a
commit 2ed2dbba00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 115 additions and 13 deletions

View File

@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
- Providers/CLI: remove bundled CLI text-provider backends and the `agents.defaults.cliBackends` surface, while keeping ACP harness sessions and Gemini media understanding on the native bundled providers.
- Matrix/exec approvals: clarify unavailable-approval replies so Matrix no longer claims chat approvals are unsupported when native exec approvals are merely unconfigured. (#61424) Thanks @gumadeiras.
- Docs/IRC: replace public IRC hostname examples with `irc.example.com` and recommend private servers for bot coordination while listing common public networks for intentional use.
- Memory/dreaming: write dreaming trail content to top-level `dreams.md` instead of daily memory notes, update `/dreaming` help text to point there, and keep `dreams.md` available for explicit reads without pulling it into default recall. Thanks @davemorin.
### Fixes

View File

@ -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 () => {

View File

@ -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");
}

View 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");
});
});

View File

@ -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") {

View File

@ -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,

View File

@ -6,6 +6,7 @@ import {
buildMultimodalChunkForIndexing,
buildFileEntry,
chunkMarkdown,
isMemoryPath,
listMemoryFiles,
normalizeExtraMemoryPaths,
remapChunkLines,
@ -157,6 +158,12 @@ describe("listMemoryFiles", () => {
});
});
describe("isMemoryPath", () => {
it("allows explicit access to top-level dreams.md", () => {
expect(isMemoryPath("dreams.md")).toBe(true);
});
});
describe("buildFileEntry", () => {
const getTmpDir = setupTempDirLifecycle("memory-build-entry-");
const multimodal: MemoryMultimodalSettings = {

View File

@ -77,7 +77,7 @@ export function isMemoryPath(relPath: string): boolean {
if (!normalized) {
return false;
}
if (normalized === "MEMORY.md" || normalized === "memory.md") {
if (normalized === "MEMORY.md" || normalized === "memory.md" || normalized === "dreams.md") {
return true;
}
return normalized.startsWith("memory/");

View File

@ -6,6 +6,7 @@ import {
buildMultimodalChunkForIndexing,
buildFileEntry,
chunkMarkdown,
isMemoryPath,
listMemoryFiles,
normalizeExtraMemoryPaths,
remapChunkLines,
@ -157,6 +158,12 @@ describe("listMemoryFiles", () => {
});
});
describe("isMemoryPath", () => {
it("allows explicit access to top-level dreams.md", () => {
expect(isMemoryPath("dreams.md")).toBe(true);
});
});
describe("buildFileEntry", () => {
const getTmpDir = setupTempDirLifecycle("memory-build-entry-");
const multimodal: MemoryMultimodalSettings = {

View File

@ -77,7 +77,7 @@ export function isMemoryPath(relPath: string): boolean {
if (!normalized) {
return false;
}
if (normalized === "MEMORY.md" || normalized === "memory.md") {
if (normalized === "MEMORY.md" || normalized === "memory.md" || normalized === "dreams.md") {
return true;
}
return normalized.startsWith("memory/");