From 6300393acc0d496ad0b6f163970a778894b0cf4b Mon Sep 17 00:00:00 2001 From: Bartok Date: Wed, 15 Apr 2026 04:48:37 -0400 Subject: [PATCH] fix(dreaming): use ingestion date for dayBucket instead of file date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When dreaming re-ingests the same daily memory file on a later day, dayBucket was set to batch.day (the file date, e.g. '2026-04-11') instead of the current ingestion date. Because dedupeByQueryPerDay checks whether todayBucket already appears in recallDays, using the file date caused all subsequent sweeps to hit the dedupe guard — the file date was already in recallDays from the first ingestion. This kept dailyCount stuck at 1, signalCount below the minRecallCount=3 promotion gate, and zero entries ever reached the scoring stage. Fix: pass formatMemoryDreamingDay(params.nowMs, params.timezone) as dayBucket in all three ingestion call sites (session transcripts, daily memory, and historical seed). The query string still uses the file date for identification; only the dedupe bucket changes to reflect when the sweep actually ran. Closes #67061 --- .../memory-core/src/dreaming-phases.test.ts | 90 +++++++++++++++++++ extensions/memory-core/src/dreaming-phases.ts | 8 +- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/extensions/memory-core/src/dreaming-phases.test.ts b/extensions/memory-core/src/dreaming-phases.test.ts index 4260686cb79..64f14f0777e 100644 --- a/extensions/memory-core/src/dreaming-phases.test.ts +++ b/extensions/memory-core/src/dreaming-phases.test.ts @@ -1648,4 +1648,94 @@ describe("memory-core dreaming phases", () => { "The traces braided themselves into a map.", ); }); + + it("increments dailyCount when the same daily file is re-ingested on a later day", async () => { + // Regression test for #67061: dayBucket used the file date instead of the + // ingestion date, so re-ingesting the same file on a different day was + // treated as a duplicate and dailyCount stayed at 1. + const workspaceDir = await createDreamingWorkspace(); + // Write a daily note dated 2026-04-03 (two days before the base test time). + await fs.writeFile( + path.join(workspaceDir, "memory", "2026-04-03.md"), + ["# 2026-04-03", "", "- Move backups to S3 Glacier.", "- Keep retention at 365 days."].join( + "\n", + ), + "utf-8", + ); + + const configForTest: OpenClawConfig = { + plugins: { + entries: { + "memory-core": { + config: { + dreaming: { + enabled: true, + phases: { + light: { + enabled: true, + limit: 20, + lookbackDays: 7, + }, + }, + }, + }, + }, + }, + }, + }; + + // First ingestion on 2026-04-05. + const day1Ms = Date.parse("2026-04-05T10:00:00.000Z"); + const { beforeAgentReply: reply1 } = createHarness(configForTest, workspaceDir); + await withDreamingTestClock(async () => { + vi.setSystemTime(new Date(day1Ms)); + await reply1( + { cleanedBody: "__openclaw_memory_core_light_sleep__" }, + { trigger: "heartbeat", workspaceDir }, + ); + }); + + const after1 = await rankShortTermPromotionCandidates({ + workspaceDir, + minScore: 0, + minRecallCount: 0, + minUniqueQueries: 0, + nowMs: day1Ms, + }); + expect(after1).toHaveLength(1); + expect(after1[0]?.dailyCount).toBe(1); + + // Clear the daily ingestion checkpoint so the file is re-read on the second + // sweep (simulating a new day where the same lookback window still covers + // this file). + const dailyStatePath = path.join(workspaceDir, "memory", ".dreams", "daily-ingestion.json"); + try { + await fs.unlink(dailyStatePath); + } catch { + // ignore if not created + } + + // Second ingestion on 2026-04-06 (next day). + const day2Ms = Date.parse("2026-04-06T10:00:00.000Z"); + const { beforeAgentReply: reply2 } = createHarness(configForTest, workspaceDir); + await withDreamingTestClock(async () => { + vi.setSystemTime(new Date(day2Ms)); + await reply2( + { cleanedBody: "__openclaw_memory_core_light_sleep__" }, + { trigger: "heartbeat", workspaceDir }, + ); + }); + + const after2 = await rankShortTermPromotionCandidates({ + workspaceDir, + minScore: 0, + minRecallCount: 0, + minUniqueQueries: 0, + nowMs: day2Ms, + }); + expect(after2).toHaveLength(1); + // With the fix, dailyCount should be 2 because the ingestion date changed. + // Before the fix, it stayed at 1 because dayBucket was the file date. + expect(after2[0]?.dailyCount).toBe(2); + }); }); diff --git a/extensions/memory-core/src/dreaming-phases.ts b/extensions/memory-core/src/dreaming-phases.ts index 29f0de9c1fc..d8f7f7295fd 100644 --- a/extensions/memory-core/src/dreaming-phases.ts +++ b/extensions/memory-core/src/dreaming-phases.ts @@ -954,6 +954,7 @@ async function ingestSessionTranscriptSignals(params: { timezone: params.timezone, state, }); + const ingestionDayBucket = formatMemoryDreamingDay(params.nowMs, params.timezone); for (const batch of collected.batches) { await recordShortTermRecalls({ workspaceDir: params.workspaceDir, @@ -961,7 +962,7 @@ async function ingestSessionTranscriptSignals(params: { results: batch.results, signalType: "daily", dedupeByQueryPerDay: true, - dayBucket: batch.day, + dayBucket: ingestionDayBucket, nowMs: params.nowMs, timezone: params.timezone, }); @@ -1113,6 +1114,7 @@ async function ingestDailyMemorySignals(params: { nowMs: params.nowMs, state, }); + const ingestionDayBucket = formatMemoryDreamingDay(params.nowMs, params.timezone); for (const batch of collected.batches) { await recordShortTermRecalls({ workspaceDir: params.workspaceDir, @@ -1120,7 +1122,7 @@ async function ingestDailyMemorySignals(params: { results: batch.results, signalType: "daily", dedupeByQueryPerDay: true, - dayBucket: batch.day, + dayBucket: ingestionDayBucket, nowMs: params.nowMs, timezone: params.timezone, }); @@ -1222,7 +1224,7 @@ export async function seedHistoricalDailyMemorySignals(params: { results, signalType: "daily", dedupeByQueryPerDay: true, - dayBucket: entry.day, + dayBucket: formatMemoryDreamingDay(params.nowMs, params.timezone), nowMs: params.nowMs, timezone: params.timezone, });