mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 00:33:44 +02:00
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:
@@ -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.
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user