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 <vincentkoc@ieee.org>
This commit is contained in:
saram ali
2026-04-12 23:40:59 +05:00
committed by GitHub
parent 35a784c165
commit acdf2b1c8a
3 changed files with 146 additions and 1 deletions

View File

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

View File

@@ -54,6 +54,19 @@ describe("short-term promotion", () => {
return notePath;
}
async function writeDailyMemoryNoteInSubdir(
workspaceDir: string,
subdir: string,
date: string,
lines: string[],
): Promise<string> {
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<string, unknown>;
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 () => {

View File

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