fix: re-ingest daily memory during dreaming (#76359) (thanks @neeravmakwana)

This commit is contained in:
Ayaan Zaidi
2026-05-05 21:13:47 +05:30
parent 91879ac442
commit 8faf91a2a8
3 changed files with 27 additions and 17 deletions

View File

@@ -326,6 +326,7 @@ Docs: https://docs.openclaw.ai
- Agents/auth-profiles: do not record request-shape (`format`) rejections as auth-profile health failures, so a single per-session transcript-shape error (such as a prefill-strict 400 "conversation must end with a user message") no longer triggers a profile-wide cooldown that blocks every other healthy session sharing the same auth profile. Refs #77228. (#77280) Thanks @openperf.
- CLI/update: stop dev-channel source updates immediately when `git fetch` fails, so tag conflicts cannot keep preflight, rebase, or build steps running against stale refs while the Gateway is still on the old runtime. (#77845) Thanks @obviyus.
- Config/recovery: chmod restored `openclaw.json` back to owner-only (`0600`) after suspicious-read backup recovery on POSIX hosts, so a previously world-readable config mode cannot persist into a freshly restored credential-bearing config. (#77488) Thanks @drobison00.
- Memory/dreaming: persist last dreaming-ingestion calendar day per daily note in `daily-ingestion.json` so unchanged notes are still re-ingested once per dreaming day for promotion signals toward deep thresholds. Fixes #76225. (#76359) Thanks @neeravmakwana.
## 2026.5.3-1

View File

@@ -2586,17 +2586,6 @@ describe("memory-core dreaming phases", () => {
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 () => {
@@ -2615,8 +2604,6 @@ describe("memory-core dreaming phases", () => {
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

@@ -72,6 +72,7 @@ type RunPhaseIfTriggeredParams = {
);
const LIGHT_SLEEP_EVENT_TEXT = "__openclaw_memory_core_light_sleep__";
const REM_SLEEP_EVENT_TEXT = "__openclaw_memory_core_rem_sleep__";
const MEMORY_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
const DAILY_MEMORY_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})\.md$/;
const DAILY_INGESTION_STATE_RELATIVE_PATH = path.join("memory", ".dreams", "daily-ingestion.json");
const DAILY_INGESTION_SCORE = 0.62;
@@ -386,6 +387,7 @@ type DailyIngestionBatch = {
type DailyIngestionFileState = {
mtimeMs: number;
size: number;
lastDreamingDayIngested?: string;
};
type DailyIngestionState = {
@@ -417,9 +419,11 @@ function normalizeDailyIngestionState(raw: unknown): DailyIngestionState {
if (!Number.isFinite(mtimeMs) || mtimeMs < 0 || !Number.isFinite(size) || size < 0) {
continue;
}
const lastDreamingDayIngested = normalizeMemoryDay(file.lastDreamingDayIngested);
files[key] = {
mtimeMs: Math.floor(mtimeMs),
size: Math.floor(size),
...(lastDreamingDayIngested ? { lastDreamingDayIngested } : {}),
};
}
return {
@@ -428,6 +432,14 @@ function normalizeDailyIngestionState(raw: unknown): DailyIngestionState {
};
}
function normalizeMemoryDay(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const day = value.trim();
return MEMORY_DAY_RE.test(day) ? day : undefined;
}
async function readDailyIngestionState(workspaceDir: string): Promise<DailyIngestionState> {
const statePath = resolveDailyIngestionStatePath(workspaceDir);
try {
@@ -1065,6 +1077,7 @@ async function collectDailyIngestionBatches(params: {
lookbackDays: number;
limit: number;
nowMs: number;
ingestionDreamingDay: string;
state: DailyIngestionState;
}): Promise<DailyIngestionCollectionResult> {
const memoryDir = path.join(params.workspaceDir, "memory");
@@ -1119,11 +1132,15 @@ async function collectDailyIngestionBatches(params: {
previous !== undefined &&
previous.mtimeMs === fingerprint.mtimeMs &&
previous.size === fingerprint.size;
if (!unchanged) {
changed = true;
} else {
const previousDreamingDay = normalizeMemoryDay(previous?.lastDreamingDayIngested);
if (unchanged && previousDreamingDay === params.ingestionDreamingDay) {
nextFiles[relativePath] = {
...fingerprint,
lastDreamingDayIngested: previousDreamingDay,
};
continue;
}
changed = true;
const raw = await fs.readFile(filePath, "utf-8").catch((err: unknown) => {
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
@@ -1155,6 +1172,10 @@ async function collectDailyIngestionBatches(params: {
}
batches.push({ day: file.day, results });
total += results.length;
nextFiles[relativePath] = {
...fingerprint,
lastDreamingDayIngested: params.ingestionDreamingDay,
};
if (total >= totalCap) {
break;
}
@@ -1189,14 +1210,15 @@ async function ingestDailyMemorySignals(params: {
timezone?: string;
}): Promise<void> {
const state = await readDailyIngestionState(params.workspaceDir);
const ingestionDayBucket = formatMemoryDreamingDay(params.nowMs, params.timezone);
const collected = await collectDailyIngestionBatches({
workspaceDir: params.workspaceDir,
lookbackDays: params.lookbackDays,
limit: params.limit,
nowMs: params.nowMs,
ingestionDreamingDay: ingestionDayBucket,
state,
});
const ingestionDayBucket = formatMemoryDreamingDay(params.nowMs, params.timezone);
for (const batch of collected.batches) {
await recordShortTermRecalls({
workspaceDir: params.workspaceDir,