From 2ed2dbba00afadf1a012e4e9f46e923a955787f4 Mon Sep 17 00:00:00 2001 From: Dave Morin Date: Sun, 5 Apr 2026 12:19:31 -1000 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + .../memory-core/src/dreaming-command.test.ts | 6 +- .../memory-core/src/dreaming-command.ts | 4 +- .../memory-core/src/dreaming-markdown.test.ts | 88 +++++++++++++++++++ .../memory-core/src/dreaming-markdown.ts | 9 +- extensions/memory-core/src/flush-plan.ts | 2 +- .../memory-host-sdk/src/host/internal.test.ts | 7 ++ packages/memory-host-sdk/src/host/internal.ts | 2 +- src/memory-host-sdk/host/internal.test.ts | 7 ++ src/memory-host-sdk/host/internal.ts | 2 +- 10 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 extensions/memory-core/src/dreaming-markdown.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3b5611d3f..edf848fc24f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/extensions/memory-core/src/dreaming-command.test.ts b/extensions/memory-core/src/dreaming-command.test.ts index 3b5fb403c04..e9f09a90cfd 100644 --- a/extensions/memory-core/src/dreaming-command.test.ts +++ b/extensions/memory-core/src/dreaming-command.test.ts @@ -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 () => { diff --git a/extensions/memory-core/src/dreaming-command.ts b/extensions/memory-core/src/dreaming-command.ts index 81aa4f565dc..c86a08bbac6 100644 --- a/extensions/memory-core/src/dreaming-command.ts +++ b/extensions/memory-core/src/dreaming-command.ts @@ -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"); } diff --git a/extensions/memory-core/src/dreaming-markdown.test.ts b/extensions/memory-core/src/dreaming-markdown.test.ts new file mode 100644 index 00000000000..767c445499f --- /dev/null +++ b/extensions/memory-core/src/dreaming-markdown.test.ts @@ -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 { + 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"); + }); +}); diff --git a/extensions/memory-core/src/dreaming-markdown.ts b/extensions/memory-core/src/dreaming-markdown.ts index 0e9ecfe44f9..833f5285135 100644 --- a/extensions/memory-core/src/dreaming-markdown.ts +++ b/extensions/memory-core/src/dreaming-markdown.ts @@ -16,6 +16,8 @@ const DAILY_PHASE_LABELS: Record, strin rem: "rem", }; +const DREAMS_FILENAME = "dreams.md"; + function resolvePhaseMarkers(phase: Exclude): { 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") { diff --git a/extensions/memory-core/src/flush-plan.ts b/extensions/memory-core/src/flush-plan.ts index 24e53ce9d34..ace00f3a596 100644 --- a/extensions/memory-core/src/flush-plan.ts +++ b/extensions/memory-core/src/flush-plan.ts @@ -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, diff --git a/packages/memory-host-sdk/src/host/internal.test.ts b/packages/memory-host-sdk/src/host/internal.test.ts index 8e7a748d740..465a63abc8a 100644 --- a/packages/memory-host-sdk/src/host/internal.test.ts +++ b/packages/memory-host-sdk/src/host/internal.test.ts @@ -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 = { diff --git a/packages/memory-host-sdk/src/host/internal.ts b/packages/memory-host-sdk/src/host/internal.ts index 45d4ceacb65..bb64bd1c2c0 100644 --- a/packages/memory-host-sdk/src/host/internal.ts +++ b/packages/memory-host-sdk/src/host/internal.ts @@ -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/"); diff --git a/src/memory-host-sdk/host/internal.test.ts b/src/memory-host-sdk/host/internal.test.ts index 8e7a748d740..465a63abc8a 100644 --- a/src/memory-host-sdk/host/internal.test.ts +++ b/src/memory-host-sdk/host/internal.test.ts @@ -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 = { diff --git a/src/memory-host-sdk/host/internal.ts b/src/memory-host-sdk/host/internal.ts index 557e1a49c31..aa7e85facb6 100644 --- a/src/memory-host-sdk/host/internal.ts +++ b/src/memory-host-sdk/host/internal.ts @@ -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/");