diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dc4e5d2e0e..e54cdb95366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` 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. diff --git a/extensions/memory-core/src/dreaming.test.ts b/extensions/memory-core/src/dreaming.test.ts index 93aeaa77e3a..b90505f7680 100644 --- a/extensions/memory-core/src/dreaming.test.ts +++ b/extensions/memory-core/src/dreaming.test.ts @@ -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[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); diff --git a/extensions/memory-core/src/dreaming.ts b/extensions/memory-core/src/dreaming.ts index 9de000968e1..7718bf5279c 100644 --- a/extensions/memory-core/src/dreaming.ts +++ b/extensions/memory-core/src/dreaming.ts @@ -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", });