diff --git a/extensions/memory-core/src/dreaming-phases.test.ts b/extensions/memory-core/src/dreaming-phases.test.ts index 9e587e1ec45..c5f10d5b1d2 100644 --- a/extensions/memory-core/src/dreaming-phases.test.ts +++ b/extensions/memory-core/src/dreaming-phases.test.ts @@ -720,6 +720,92 @@ describe("memory-core dreaming phases", () => { ]); }); + it("does not reread unchanged dreaming-generated transcripts after checkpointing skip state", async () => { + const workspaceDir = await createDreamingWorkspace(); + vi.stubEnv("OPENCLAW_TEST_FAST", "1"); + vi.stubEnv("OPENCLAW_STATE_DIR", path.join(workspaceDir, ".state")); + const sessionsDir = resolveSessionTranscriptsDirForAgent("main"); + await fs.mkdir(sessionsDir, { recursive: true }); + const transcriptPath = path.join(sessionsDir, "dreaming-narrative.jsonl"); + await fs.writeFile( + transcriptPath, + [ + JSON.stringify({ + type: "custom", + customType: "openclaw:bootstrap-context:full", + data: { + runId: "dreaming-narrative-light-1775894400455", + sessionId: "dream-session-1", + }, + }), + JSON.stringify({ + type: "message", + message: { + role: "user", + timestamp: "2026-04-05T18:01:00.000Z", + content: [ + { type: "text", text: "Write a dream diary entry from these memory fragments." }, + ], + }, + }), + ].join("\n") + "\n", + "utf-8", + ); + const mtime = new Date("2026-04-05T18:05:00.000Z"); + await fs.utimes(transcriptPath, mtime, mtime); + + const { beforeAgentReply } = createHarness( + { + agents: { + defaults: { + workspace: workspaceDir, + }, + list: [{ id: "main", workspace: workspaceDir }], + }, + plugins: { + entries: { + "memory-core": { + config: { + dreaming: { + enabled: true, + phases: { + light: { + enabled: true, + limit: 20, + lookbackDays: 7, + }, + }, + }, + }, + }, + }, + }, + }, + workspaceDir, + ); + + try { + await beforeAgentReply( + { cleanedBody: "__openclaw_memory_core_light_sleep__" }, + { trigger: "heartbeat", workspaceDir }, + ); + + const readFileSpy = vi.spyOn(fs, "readFile"); + await beforeAgentReply( + { cleanedBody: "__openclaw_memory_core_light_sleep__" }, + { trigger: "heartbeat", workspaceDir }, + ); + + expect(readFileSpy.mock.calls.some(([target]) => String(target) === transcriptPath)).toBe( + false, + ); + readFileSpy.mockRestore(); + } finally { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + } + }); + it("dedupes reset/deleted session archives instead of double-ingesting", async () => { const workspaceDir = await createDreamingWorkspace(); vi.stubEnv("OPENCLAW_TEST_FAST", "1"); diff --git a/extensions/memory-core/src/dreaming-phases.ts b/extensions/memory-core/src/dreaming-phases.ts index f20f262df54..29f0de9c1fc 100644 --- a/extensions/memory-core/src/dreaming-phases.ts +++ b/extensions/memory-core/src/dreaming-phases.ts @@ -739,10 +739,7 @@ async function collectSessionIngestionBatches(params: { mtimeMs: Math.floor(Math.max(0, stat.mtimeMs)), size: Math.floor(Math.max(0, stat.size)), }; - const cursorAtEnd = - previous !== undefined && - previous.lineCount > 0 && - previous.lastContentLine >= previous.lineCount; + const cursorAtEnd = previous !== undefined && previous.lastContentLine >= previous.lineCount; const unchanged = Boolean(previous) && previous.mtimeMs === fingerprint.mtimeMs && diff --git a/extensions/memory-core/src/short-term-promotion.ts b/extensions/memory-core/src/short-term-promotion.ts index faf41fab9b8..e159ef83ac7 100644 --- a/extensions/memory-core/src/short-term-promotion.ts +++ b/extensions/memory-core/src/short-term-promotion.ts @@ -39,6 +39,7 @@ const PHASE_SIGNAL_REM_BOOST_MAX = 0.09; const PHASE_SIGNAL_HALF_LIFE_DAYS = 14; const DREAMING_TRANSCRIPT_PROMPT_LINE_RE = /\[[^\]]*dreaming-narrative[^\]]*]\s*Write a dream diary entry from these memory fragments:?/i; +const DREAMING_DIFF_PREFIX_RE = /@@\s*-\d+(?:,\d+)?\s+[-*+]\s+/iy; const inProcessShortTermLocks = new Map>(); const ensuredShortTermDirs = new Map>(); @@ -240,10 +241,10 @@ function normalizeSnippet(raw: string): string { function consumeDreamingLeadPrefix(snippet: string): string { let index = 0; while (index < snippet.length) { - const remaining = snippet.slice(index); - const diffMatch = /^@@\s*-\d+(?:,\d+)?\s+[-*+]\s+/i.exec(remaining); + DREAMING_DIFF_PREFIX_RE.lastIndex = index; + const diffMatch = DREAMING_DIFF_PREFIX_RE.exec(snippet); if (diffMatch) { - index += diffMatch[0].length; + index = DREAMING_DIFF_PREFIX_RE.lastIndex; continue; } const char = snippet[index];