From acdf2b1c8a31ffd0a1278e89f4fd42b2a79a5398 Mon Sep 17 00:00:00 2001 From: saram ali <140950904+SARAMALI15792@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:40:59 +0500 Subject: [PATCH] fix(memory-core): match daily notes stored in memory/ subdirectories (#64682) * fix(memory-core): match daily notes in memory/ subdirectories in isShortTermMemoryPath * fix(memory-core): exclude dream reports from short-term recall * fix(memory-core): widen short-term recall path matching * docs(changelog): note short-term recall fix --------- Co-authored-by: Vincent Koc --- CHANGELOG.md | 1 + .../src/short-term-promotion.test.ts | 140 ++++++++++++++++++ .../memory-core/src/short-term-promotion.ts | 6 +- 3 files changed, 146 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a45ec777544..837041af278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Dreaming: consume managed heartbeat events exactly once, stage light-sleep confidence from all recorded short-term signals, wake scheduled jobs immediately, raise dreaming-only promotion enough to cross the durable-memory gate, and stop dreaming from re-ingesting its own narrative transcripts. - Dreaming/narrative: harden transient narrative cleanup by retrying timed-out deletes, scrubbing stale dreaming session artifacts through the lock-aware session-store path, and isolating transient narrative session keys per workspace. (#65320, #61674) - Memory/wiki: preserve Unicode letters, digits, and combining marks in wiki slugs and contradiction clustering, and cap Unicode filename segments to safe byte lengths so non-ASCII titles stop collapsing or overflowing path limits. (#64742) Thanks @zhouhe-xydt. +- Memory/short-term recall: allow nested daily notes under `memory/**/YYYY-MM-DD.md` to feed short-term recall, while still excluding generated dream reports under `memory/dreaming/**` so dreaming does not promote its own output. (#64682) Thanks @SARAMALI15792. - UI/WebChat: hide synthetic transcript-repair tool results from chat history reloads so internal recovery markers do not leak into visible chat after reconnects. (#65247) Thanks @wangwllu. - WhatsApp/outbound: fall back to the first `mediaUrls` entry when `mediaUrl` is empty so gateway media sends stop silently dropping attachments that already have a resolved media list. (#64394) Thanks @eric-fr4 and @vincentkoc. - Doctor/Discord: stop `openclaw doctor --fix` from rewriting legacy Discord preview-streaming config into the nested modern shape, so downgrades can still recover without hand-editing `channels.discord.streaming`. (#65035) Thanks @vincentkoc. diff --git a/extensions/memory-core/src/short-term-promotion.test.ts b/extensions/memory-core/src/short-term-promotion.test.ts index 0fee7f1ad4a..85e384277be 100644 --- a/extensions/memory-core/src/short-term-promotion.test.ts +++ b/extensions/memory-core/src/short-term-promotion.test.ts @@ -54,6 +54,19 @@ describe("short-term promotion", () => { return notePath; } + async function writeDailyMemoryNoteInSubdir( + workspaceDir: string, + subdir: string, + date: string, + lines: string[], + ): Promise { + const dir = path.join(workspaceDir, "memory", subdir); + await fs.mkdir(dir, { recursive: true }); + const notePath = path.join(dir, `${date}.md`); + await fs.writeFile(notePath, `${lines.join("\n")}\n`, "utf-8"); + return notePath; + } + it("detects short-term daily memory paths", () => { expect(isShortTermMemoryPath("memory/2026-04-03.md")).toBe(true); expect(isShortTermMemoryPath("2026-04-03.md")).toBe(true); @@ -61,6 +74,133 @@ describe("short-term promotion", () => { expect(isShortTermMemoryPath("notes/2026-04-03.md")).toBe(false); expect(isShortTermMemoryPath("MEMORY.md")).toBe(false); expect(isShortTermMemoryPath("memory/network.md")).toBe(false); + expect(isShortTermMemoryPath("memory/daily/2026-04-03.md")).toBe(true); + expect(isShortTermMemoryPath("memory/daily notes/2026-04-03.md")).toBe(true); + expect(isShortTermMemoryPath("memory/日记/2026-04-03.md")).toBe(true); + expect(isShortTermMemoryPath("memory/notes/2026-04-03.md")).toBe(true); + expect(isShortTermMemoryPath("memory/nested/deep/2026-04-03.md")).toBe(true); + expect(isShortTermMemoryPath("memory/dreaming/2026-04-03.md")).toBe(false); + expect(isShortTermMemoryPath("memory/dreaming/deep/2026-04-03.md")).toBe(false); + expect(isShortTermMemoryPath("../../vault/memory/dreaming/deep/2026-04-03.md")).toBe(false); + expect(isShortTermMemoryPath("notes/daily/2026-04-03.md")).toBe(false); + }); + + it("records short-term recall for notes stored in a memory/ subdirectory", async () => { + await withTempWorkspace(async (workspaceDir) => { + const notePath = await writeDailyMemoryNoteInSubdir(workspaceDir, "daily", "2026-04-03", [ + "Subdirectory recall integration test note.", + ]); + const relativePath = path.relative(workspaceDir, notePath).replaceAll("\\", "/"); + await recordShortTermRecalls({ + workspaceDir, + query: "test query", + results: [ + { + path: relativePath, + source: "memory", + startLine: 1, + endLine: 1, + score: 0.9, + snippet: "Subdirectory recall integration test note.", + }, + ], + }); + const storePath = resolveShortTermRecallStorePath(workspaceDir); + const raw = await fs.readFile(storePath, "utf-8"); + const store = JSON.parse(raw) as Record; + expect(Object.keys(store).length).toBeGreaterThan(0); + }); + }); + + it("records short-term recall for notes stored in spaced and Unicode memory subdirectories", async () => { + await withTempWorkspace(async (workspaceDir) => { + const spacedPath = await writeDailyMemoryNoteInSubdir( + workspaceDir, + "daily notes", + "2026-04-03", + ["Spaced subdirectory recall integration test note."], + ); + const unicodePath = await writeDailyMemoryNoteInSubdir(workspaceDir, "日记", "2026-04-04", [ + "Unicode subdirectory recall integration test note.", + ]); + + await recordShortTermRecalls({ + workspaceDir, + query: "nested subdir query", + results: [ + { + path: path.relative(workspaceDir, spacedPath).replaceAll("\\", "/"), + source: "memory", + startLine: 1, + endLine: 1, + score: 0.9, + snippet: "Spaced subdirectory recall integration test note.", + }, + { + path: path.relative(workspaceDir, unicodePath).replaceAll("\\", "/"), + source: "memory", + startLine: 1, + endLine: 1, + score: 0.85, + snippet: "Unicode subdirectory recall integration test note.", + }, + ], + }); + + const raw = await fs.readFile(resolveShortTermRecallStorePath(workspaceDir), "utf-8"); + expect(raw).toContain("memory/daily notes/2026-04-03.md"); + expect(raw).toContain("memory/日记/2026-04-04.md"); + }); + }); + + it("ignores dream report paths when recording short-term recalls", async () => { + await withTempWorkspace(async (workspaceDir) => { + await recordShortTermRecalls({ + workspaceDir, + query: "dream recall", + results: [ + { + path: "memory/dreaming/deep/2026-04-03.md", + source: "memory", + startLine: 1, + endLine: 1, + score: 0.9, + snippet: "Auto-generated dream report should not seed promotions.", + }, + ], + }); + + await expect( + fs.readFile(resolveShortTermRecallStorePath(workspaceDir), "utf-8"), + ).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + }); + + it("ignores prefixed dream report paths when recording short-term recalls", async () => { + await withTempWorkspace(async (workspaceDir) => { + await recordShortTermRecalls({ + workspaceDir, + query: "prefixed dream recall", + results: [ + { + path: "../../vault/memory/dreaming/deep/2026-04-03.md", + source: "memory", + startLine: 1, + endLine: 1, + score: 0.9, + snippet: "External dream report should not seed promotions.", + }, + ], + }); + + await expect( + fs.readFile(resolveShortTermRecallStorePath(workspaceDir), "utf-8"), + ).rejects.toMatchObject({ + code: "ENOENT", + }); + }); }); it("records recalls and ranks candidates with weighted scores", async () => { diff --git a/extensions/memory-core/src/short-term-promotion.ts b/extensions/memory-core/src/short-term-promotion.ts index 0c7d438a5a9..e73a9b5816a 100644 --- a/extensions/memory-core/src/short-term-promotion.ts +++ b/extensions/memory-core/src/short-term-promotion.ts @@ -13,7 +13,8 @@ import { } from "./concept-vocabulary.js"; import { asRecord } from "./dreaming-shared.js"; -const SHORT_TERM_PATH_RE = /(?:^|\/)memory\/(\d{4})-(\d{2})-(\d{2})\.md$/; +const SHORT_TERM_PATH_RE = /(?:^|\/)memory\/(?:[^/]+\/)*(\d{4})-(\d{2})-(\d{2})\.md$/; +const DREAMING_MEMORY_PATH_RE = /(?:^|\/)memory\/dreaming\//; const SHORT_TERM_SESSION_CORPUS_RE = /(?:^|\/)memory\/\.dreams\/session-corpus\/(\d{4})-(\d{2})-(\d{2})\.(?:md|txt)$/; const SHORT_TERM_BASENAME_RE = /^(\d{4})-(\d{2})-(\d{2})\.md$/; @@ -799,6 +800,9 @@ async function writeStore(workspaceDir: string, store: ShortTermRecallStore): Pr export function isShortTermMemoryPath(filePath: string): boolean { const normalized = normalizeMemoryPath(filePath); + if (DREAMING_MEMORY_PATH_RE.test(normalized)) { + return false; + } if (SHORT_TERM_PATH_RE.test(normalized)) { return true; }