fix(dreaming): use ingestion date for dayBucket instead of file date

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
This commit is contained in:
Bartok
2026-04-15 04:48:37 -04:00
committed by Josh Lehman
parent a177d8d454
commit 6300393acc
2 changed files with 95 additions and 3 deletions

View File

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

View File

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