Skip to content

Commit b20b72e

Browse files
committed
fix(memory-core): strip dreaming blocks from partial reads of daily files
Previously, memory_get calls with from/lines parameters on daily memory files (memory/YYYY-MM-DD.md) bypassed the dreaming block filter, leaking cross-session staging data from Light Sleep and REM Sleep phases. The fix redirects partial reads of daily files through the full-read path: read the entire file, strip dreaming managed blocks, then slice the requested line range from the cleaned content. This ensures line numbers are consistent with a full read and no staging data is ever exposed. Non-daily files continue to use the efficient streaming partial read path, since they never contain dreaming blocks. Daily memory files are small (a few KB) so the overhead of a full read is negligible. Resolves the TODO(#68367) left in the initial implementation. Addresses review feedback on #68408.
1 parent ed3bb01 commit b20b72e

2 files changed

Lines changed: 75 additions & 5 deletions

File tree

extensions/memory-core/src/memory/qmd-manager.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3604,6 +3604,59 @@ describe("QmdMemoryManager", () => {
36043604
readFileSpy.mockRestore();
36053605
});
36063606

3607+
it("strips dreaming managed blocks from partial reads of daily memory files", async () => {
3608+
const relPath = path.join("memory", "2026-04-18.md");
3609+
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
3610+
const lines = [
3611+
"# 2026-04-18 Daily Log",
3612+
"",
3613+
"## Morning notes",
3614+
"line-4",
3615+
"line-5",
3616+
"## Light Sleep",
3617+
"<!-- openclaw:dreaming:light:start -->",
3618+
"dreaming-candidate-1",
3619+
"dreaming-candidate-2",
3620+
"<!-- openclaw:dreaming:light:end -->",
3621+
"",
3622+
"## Afternoon notes",
3623+
"line-13",
3624+
"line-14",
3625+
];
3626+
await fs.writeFile(path.join(workspaceDir, relPath), lines.join("\n"), "utf-8");
3627+
3628+
const { manager } = await createManager();
3629+
3630+
// Partial read that would span the dreaming block in the raw file
3631+
const result = await manager.readFile({ relPath, from: 1, lines: 10 });
3632+
expect(result.text).not.toContain("dreaming-candidate");
3633+
expect(result.text).not.toContain("openclaw:dreaming");
3634+
expect(result.text).toContain("Morning notes");
3635+
expect(result.text).toContain("Afternoon notes");
3636+
3637+
await manager.close();
3638+
});
3639+
3640+
it("uses efficient streaming partial read for non-daily memory files", async () => {
3641+
const readFileSpy = vi.spyOn(fs, "readFile");
3642+
const relPath = path.join("memory", "window.md");
3643+
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
3644+
await fs.writeFile(
3645+
path.join(workspaceDir, relPath),
3646+
Array.from({ length: 20 }, (_, i) => `line-${i + 1}`).join("\n"),
3647+
"utf-8",
3648+
);
3649+
3650+
const { manager } = await createManager();
3651+
const result = await manager.readFile({ relPath, from: 5, lines: 3 });
3652+
expect(result.text).toContain("line-5");
3653+
// Non-daily files should use streaming partial read, not readFile
3654+
expect(readFileSpy).not.toHaveBeenCalled();
3655+
3656+
await manager.close();
3657+
readFileSpy.mockRestore();
3658+
});
3659+
36073660
it("returns a bounded default excerpt for qmd memory reads without explicit lines", async () => {
36083661
const relPath = path.join("memory", "default-window.md");
36093662
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });

extensions/memory-core/src/memory/qmd-manager.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,28 @@ export class QmdMemoryManager implements MemorySearchManager {
12081208
}
12091209
const contextLimits = resolveAgentContextLimits(this.cfg, this.agentId);
12101210
if (params.from !== undefined || params.lines !== undefined) {
1211+
// For daily memory files, read the full file and strip dreaming blocks
1212+
// before slicing so that line numbers are consistent with a full read
1213+
// and cross-session staging data is never exposed. Daily files are
1214+
// small (a few KB) so the overhead of a full read is negligible.
1215+
// See openclaw/openclaw#68367.
1216+
if (isDailyMemoryPath(relPath)) {
1217+
const full = await this.readFullText(absPath);
1218+
if (full.missing) {
1219+
return { text: "", path: relPath };
1220+
}
1221+
const stripped = stripDreamingManagedBlocks(full.text);
1222+
return buildMemoryReadResult({
1223+
content: stripped,
1224+
relPath,
1225+
from: params.from,
1226+
lines: params.lines,
1227+
defaultLines: contextLimits?.memoryGetDefaultLines ?? DEFAULT_MEMORY_READ_LINES,
1228+
maxChars: contextLimits?.memoryGetMaxChars,
1229+
suggestReadFallback: isDefaultMemoryPath(relPath),
1230+
});
1231+
}
1232+
// Non-daily files: use efficient streaming partial read.
12111233
const requestedCount = Math.max(
12121234
1,
12131235
params.lines ?? contextLimits?.memoryGetDefaultLines ?? DEFAULT_MEMORY_READ_LINES,
@@ -1216,11 +1238,6 @@ export class QmdMemoryManager implements MemorySearchManager {
12161238
if (partial.missing) {
12171239
return { text: "", path: relPath };
12181240
}
1219-
// TODO(#68367): partial reads of daily memory files still expose dreaming
1220-
// blocks. Stripping here would shift line numbers relative to the raw
1221-
// file, which breaks callers that paginate with from/lines offsets.
1222-
// A future fix could read-then-strip the full file and re-slice, or
1223-
// introduce virtual line mapping.
12241241
return buildMemoryReadResultFromSlice({
12251242
selectedLines: partial.selectedLines,
12261243
relPath,

0 commit comments

Comments
 (0)