fix(memory): standardize DREAMS trail path

This commit is contained in:
Vincent Koc 2026-04-05 23:34:31 +01:00
parent 367f52f483
commit a9dbaa1124
9 changed files with 18 additions and 92 deletions

View File

@ -37,7 +37,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.
- 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

@ -93,14 +93,14 @@ Full options:
## Dreaming (experimental)
Dreaming is the background memory consolidation system with three cooperative
phases: **light** (organize into `dreams.md` in inline mode), **deep**
phases: **light** (organize into `DREAMS.md` in inline mode), **deep**
(promote into `MEMORY.md`), and **REM** (reflect and find patterns in
`dreams.md` in inline mode).
`DREAMS.md` in inline mode).
- Enable with `plugins.entries.memory-core.config.dreaming.enabled: true`.
- Toggle from chat with `/dreaming on|off` or `/dreaming enable|disable light|deep|rem`.
- Each phase runs on its own cron schedule, managed automatically by `memory-core`.
- Only the deep phase writes durable memory to `MEMORY.md`. With default inline storage, Light and REM write to `dreams.md`.
- Only the deep phase writes durable memory to `MEMORY.md`. With default inline storage, Light and REM write to `DREAMS.md`.
- Ranking uses weighted signals: recall frequency, retrieval relevance, query diversity, temporal recency, cross-day consolidation, and derived concept richness.
- Promotion re-reads the live daily note before writing to `MEMORY.md`, so edited or deleted short-term snippets do not get promoted from stale recall-store snapshots.
- Scheduled and manual `memory promote` runs share the same deep phase defaults unless you pass CLI threshold overrides.

View File

@ -22,7 +22,7 @@ a distinct job, writes to a distinct target, and runs on its own schedule.
Light dreaming sorts the recent mess. It scans recent memory traces, dedupes
them by Jaccard similarity, clusters related entries, and stages candidate
memories into the shared dreaming trail file (`dreams.md`) when inline storage
memories into the shared dreaming trail file (`DREAMS.md`) when inline storage
is enabled.
Light does **not** write anything into `MEMORY.md`. It only organizes and
@ -42,19 +42,19 @@ threshold). Think: "what is true enough to keep?"
REM dreaming looks for patterns and reflection. It examines recent material,
identifies recurring themes through concept tag clustering, and writes
higher-order notes and reflections into `dreams.md` when inline storage is
higher-order notes and reflections into `DREAMS.md` when inline storage is
enabled.
REM writes to `dreams.md` in inline mode, **not** `MEMORY.md`.
REM writes to `DREAMS.md` in inline mode, **not** `MEMORY.md`.
Its output is interpretive, not canonical. Think: "what pattern am I noticing?"
## Hard boundaries
| Phase | Job | Writes to | Does NOT write to |
| ----- | --------- | ------------------------- | ----------------- |
| Light | Organize | `dreams.md` (inline mode) | MEMORY.md |
| Light | Organize | `DREAMS.md` (inline mode) | MEMORY.md |
| Deep | Preserve | MEMORY.md | -- |
| REM | Interpret | `dreams.md` (inline mode) | MEMORY.md |
| REM | Interpret | `DREAMS.md` (inline mode) | MEMORY.md |
## Quick start
@ -112,7 +112,7 @@ for the full key list.
| `enabled` | `boolean` | `true` | Master switch for all phases |
| `timezone` | `string` | unset | Timezone for schedule evaluation and dreaming date bucketing |
| `verboseLogging` | `boolean` | `false` | Emit detailed per-run dreaming logs |
| `storage.mode` | `string` | `"inline"` | Inline `dreams.md`, separate reports, or both |
| `storage.mode` | `string` | `"inline"` | Inline `DREAMS.md`, separate reports, or both |
### Light phase config
@ -233,7 +233,7 @@ See [memory CLI](/cli/memory) for the full flag reference.
2. Filter entries within `lookbackDays` of the current time.
3. Deduplicate by Jaccard similarity (configurable threshold).
4. Sort by average recall score, take up to `limit` entries.
5. Write staged candidates into `dreams.md` under a `## Light Sleep` block when
5. Write staged candidates into `DREAMS.md` under a `## Light Sleep` block when
inline storage is enabled.
### Deep phase pipeline
@ -252,7 +252,7 @@ See [memory CLI](/cli/memory) for the full flag reference.
1. Read recent memory traces within `lookbackDays`.
2. Cluster concept tags by co-occurrence.
3. Filter patterns by `minPatternStrength`.
4. Write themes and reflections into `dreams.md` under a `## REM Sleep` block
4. Write themes and reflections into `DREAMS.md` under a `## REM Sleep` block
when inline storage is enabled.
## Scheduling

View File

@ -387,12 +387,12 @@ conceptual details and chat commands, see [Dreaming](/concepts/dreaming).
| `enabled` | `boolean` | `true` | Master switch for all phases |
| `timezone` | `string` | unset | Timezone for schedule evaluation and dreaming date bucketing |
| `verboseLogging` | `boolean` | `false` | Emit detailed per-run dreaming logs |
| `storage.mode` | `string` | `"inline"` | Inline `dreams.md`, separate reports, or both |
| `storage.mode` | `string` | `"inline"` | Inline `DREAMS.md`, separate reports, or both |
| `storage.separateReports` | `boolean` | `false` | Write separate report files per phase |
### Light phase (`phases.light`)
Scans recent traces, dedupes, and stages candidates into `dreams.md` when
Scans recent traces, dedupes, and stages candidates into `DREAMS.md` when
inline storage is enabled.
Does **not** write to `MEMORY.md`.
@ -435,7 +435,7 @@ writes durable facts. Also owns recovery when memory is thin.
### REM phase (`phases.rem`)
Writes themes, reflections, and pattern notes into `dreams.md` when inline
Writes themes, reflections, and pattern notes into `DREAMS.md` when inline
storage is enabled.
Does **not** write to `MEMORY.md`.

View File

@ -15,7 +15,7 @@
"dreaming.storage.mode": {
"label": "Dreaming Storage Mode",
"placeholder": "inline",
"help": "Write inline to dreams.md, to separate reports, or both."
"help": "Write inline to DREAMS.md, to separate reports, or both."
},
"dreaming.phases.light.cron": {
"label": "Light Sleep Cron",

View File

@ -138,32 +138,6 @@ describe("dreaming markdown storage", () => {
expect(content.match(/## 2026-04-05 - REM Sleep/g)).toHaveLength(1);
});
it("migrates legacy dreams.md to canonical DREAMS.md on the next inline write", async () => {
const workspaceDir = await createTempWorkspace();
const legacyPath = path.join(workspaceDir, "dreams.md");
await fs.writeFile(legacyPath, "## 2026-04-04 - Light Sleep\nlegacy\n", "utf-8");
await writeDailyDreamingPhaseBlock({
workspaceDir,
phase: "light",
bodyLines: ["- Candidate: migrated forward"],
nowMs: Date.parse("2026-04-05T10:00:00Z"),
timezone: "UTC",
storage: {
mode: "inline",
separateReports: false,
},
});
const canonicalPath = path.join(workspaceDir, "DREAMS.md");
const content = await fs.readFile(canonicalPath, "utf-8");
expect(content).toContain("## 2026-04-04 - Light Sleep");
expect(content).toContain("## 2026-04-05 - Light Sleep");
const workspaceEntries = await fs.readdir(workspaceDir);
expect(workspaceEntries).toContain("DREAMS.md");
expect(workspaceEntries).not.toContain("dreams.md");
});
it("still writes deep reports to the per-phase report directory", async () => {
const workspaceDir = await createTempWorkspace();

View File

@ -12,7 +12,6 @@ const DAILY_PHASE_LABELS: Record<Exclude<MemoryDreamingPhaseName, "deep">, strin
};
const DREAMS_FILENAME = "DREAMS.md";
const LEGACY_DREAMS_FILENAME = "dreams.md";
function resolvePhaseMarkers(
phase: Exclude<MemoryDreamingPhaseName, "deep">,
@ -62,44 +61,6 @@ function resolveDreamsPath(workspaceDir: string): string {
return path.join(workspaceDir, DREAMS_FILENAME);
}
function resolveLegacyDreamsPath(workspaceDir: string): string {
return path.join(workspaceDir, LEGACY_DREAMS_FILENAME);
}
async function pathExists(targetPath: string): Promise<boolean> {
try {
await fs.access(targetPath);
return true;
} catch {
return false;
}
}
async function listWorkspaceEntries(workspaceDir: string): Promise<Set<string>> {
try {
return new Set(await fs.readdir(workspaceDir));
} catch {
return new Set<string>();
}
}
async function prepareDreamsPathForWrite(workspaceDir: string): Promise<string> {
const dreamsPath = resolveDreamsPath(workspaceDir);
const workspaceEntries = await listWorkspaceEntries(workspaceDir);
if (workspaceEntries.has(DREAMS_FILENAME)) {
return dreamsPath;
}
const legacyDreamsPath = resolveLegacyDreamsPath(workspaceDir);
if (dreamsPath !== legacyDreamsPath && workspaceEntries.has(LEGACY_DREAMS_FILENAME)) {
await fs.rename(legacyDreamsPath, dreamsPath);
return dreamsPath;
}
if (await pathExists(dreamsPath)) {
return dreamsPath;
}
return dreamsPath;
}
function resolveDreamsBlockHeading(
phase: Exclude<MemoryDreamingPhaseName, "deep">,
isoDay: string,
@ -140,7 +101,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
let reportPath: string | undefined;
if (shouldWriteInline(params.storage)) {
inlinePath = await prepareDreamsPathForWrite(params.workspaceDir);
inlinePath = resolveDreamsPath(params.workspaceDir);
const original = await fs.readFile(inlinePath, "utf-8").catch((err: unknown) => {
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
return "";

View File

@ -162,10 +162,6 @@ describe("isMemoryPath", () => {
it("allows explicit access to top-level DREAMS.md", () => {
expect(isMemoryPath("DREAMS.md")).toBe(true);
});
it("keeps explicit access compatible with legacy dreams.md", () => {
expect(isMemoryPath("dreams.md")).toBe(true);
});
});
describe("buildFileEntry", () => {

View File

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