fix(memory-core): limit runtime dreaming cron reconcile to heartbeats (#63938)

Merged via squash.

Prepared head SHA: 845c1e2763
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-04-10 00:34:49 +02:00
committed by GitHub
parent 4eb7160622
commit 8b4883d990
3 changed files with 77 additions and 7 deletions

View File

@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
- Gateway/session reset: emit the typed `before_reset` hook for gateway `/new` and `/reset`, preserving reset-hook behavior even when the previous transcript has already been archived. (#53872) thanks @VACInc
- Plugins/commands: pass the active host `sessionKey` into plugin command contexts, and include `sessionId` when it is already available from the active session entry, so bundled and third-party commands can resolve the current conversation reliably. (#59044) Thanks @jalehman.
- Agents/auth: honor `models.providers.*.authHeader` for pi embedded runner model requests by injecting `Authorization: Bearer <apiKey>` when requested. (#54390) Thanks @lndyzwdxhs.
- Dreaming/cron: stop runtime cron reconciliation on ordinary user turns and only recover managed dreaming cron state during heartbeat-triggered dreaming checks, so unrelated chat traffic does not silently recreate removed jobs. (#63938) Thanks @mbelinky.
- UI/compaction: keep the compaction indicator in a retry-pending state until the run actually finishes, so the UI does not show `Context compacted` before compaction actually finishes. (#55132) Thanks @mpz4life.
- Cron/tool schemas: keep cron tool schemas strict-model-friendly while still preserving `failureAlert=false`, nullable `agentId`/`sessionKey`, and flattened add/update recovery for the newly exposed cron job fields. (#55043) Thanks @brunolorente.
- BlueBubbles/config: accept `enrichGroupParticipantsFromContacts` in the core strict config schema so gateways no longer fail validation or startup when the BlueBubbles plugin writes that field. (#56889) Thanks @zqchris.

View File

@@ -810,7 +810,10 @@ describe("gateway startup reconciliation", () => {
} as OpenClawConfig;
const beforeAgentReply = getBeforeAgentReplyHandler(onMock);
await beforeAgentReply({ cleanedBody: "hello" }, { trigger: "user", workspaceDir: "." });
await beforeAgentReply(
{ cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT },
{ trigger: "heartbeat", workspaceDir: "." },
);
expect(harness.addCalls).toHaveLength(1);
expect(harness.addCalls[0]?.schedule).toMatchObject({
@@ -898,7 +901,10 @@ describe("gateway startup reconciliation", () => {
} as OpenClawConfig;
const beforeAgentReply = getBeforeAgentReplyHandler(onMock);
await beforeAgentReply({ cleanedBody: "hello" }, { trigger: "user", workspaceDir: "." });
await beforeAgentReply(
{ cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT },
{ trigger: "heartbeat", workspaceDir: "." },
);
expect(startupHarness.updateCalls).toHaveLength(0);
expect(reloadedHarness.updateCalls).toHaveLength(1);
@@ -962,7 +968,10 @@ describe("gateway startup reconciliation", () => {
expect(harness.jobs).toHaveLength(0);
const beforeAgentReply = getBeforeAgentReplyHandler(onMock);
await beforeAgentReply({ cleanedBody: "hello" }, { trigger: "user", workspaceDir: "." });
await beforeAgentReply(
{ cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT },
{ trigger: "heartbeat", workspaceDir: "." },
);
expect(harness.addCalls).toHaveLength(2);
expect(harness.addCalls[1]?.schedule).toMatchObject({
@@ -975,7 +984,61 @@ describe("gateway startup reconciliation", () => {
}
});
it("does not reconcile managed cron on every repeated runtime reply", async () => {
it("does not reconcile managed cron on non-heartbeat runtime replies", async () => {
clearInternalHooks();
const logger = createLogger();
const harness = createCronHarness();
const onMock = vi.fn();
const api = {
config: {
plugins: {
entries: {
"memory-core": {
config: {
dreaming: {
enabled: true,
frequency: "0 2 * * *",
timezone: "UTC",
},
},
},
},
},
},
pluginConfig: {},
logger,
runtime: {},
registerHook: (event: string, handler: Parameters<typeof registerInternalHook>[1]) => {
registerInternalHook(event, handler);
},
on: onMock,
} as never;
try {
registerShortTermPromotionDreaming(api);
await triggerInternalHook(
createInternalHookEvent("gateway", "startup", "gateway:startup", {
cfg: api.config,
deps: { cron: harness.cron },
}),
);
expect(harness.listCalls).toBe(1);
const beforeAgentReply = getBeforeAgentReplyHandler(onMock);
await beforeAgentReply({ cleanedBody: "hello" }, { trigger: "user", workspaceDir: "." });
await beforeAgentReply(
{ cleanedBody: "hello again" },
{ trigger: "user", workspaceDir: "." },
);
expect(harness.listCalls).toBe(1);
} finally {
clearInternalHooks();
}
});
it("does not reconcile managed cron on every repeated runtime heartbeat", async () => {
clearInternalHooks();
const logger = createLogger();
const harness = createCronHarness();
@@ -1019,10 +1082,13 @@ describe("gateway startup reconciliation", () => {
expect(harness.listCalls).toBe(1);
const beforeAgentReply = getBeforeAgentReplyHandler(onMock);
await beforeAgentReply({ cleanedBody: "hello" }, { trigger: "user", workspaceDir: "." });
await beforeAgentReply(
{ cleanedBody: "hello again" },
{ trigger: "user", workspaceDir: "." },
{ cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT },
{ trigger: "heartbeat", workspaceDir: "." },
);
await beforeAgentReply(
{ cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT },
{ trigger: "heartbeat", workspaceDir: "." },
);
expect(harness.listCalls).toBe(2);

View File

@@ -710,6 +710,9 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
api.on("before_agent_reply", async (event, ctx) => {
try {
if (ctx.trigger !== "heartbeat") {
return undefined;
}
const config = await reconcileManagedDreamingCron({
reason: "runtime",
});