mirror of https://github.com/openclaw/openclaw.git
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:
parent
48611ec40a
commit
2ed2dbba00
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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/");
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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/");
|
||||
|
|
|
|||
Loading…
Reference in New Issue