From 2f130c418f862bd8f2dbbbc1dab373fcabe2b305 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 9 Apr 2026 21:36:36 +0200 Subject: [PATCH] fix(memory-core): use startup config for dreaming cron reconciliation (#63873) Merged via squash. Prepared head SHA: 2ec22920cdff1f4bf88ad6d665a17961eb73f247 Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Reviewed-by: @mbelinky --- CHANGELOG.md | 1 + extensions/memory-core/src/dreaming.test.ts | 64 +++++++++++++++++++++ extensions/memory-core/src/dreaming.ts | 19 +++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a26f603bab7..c842e4f24ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai - Fireworks/FirePass: disable Kimi K2.5 Turbo reasoning output by forcing thinking off on the FirePass path and hardening the provider wrapper so hidden reasoning no longer leaks into visible replies. (#63607) Thanks @frankekn. - Sessions/model selection: preserve catalog-backed session model labels and keep already-qualified session model refs stable when catalog metadata is unavailable, so Control UI model selection survives reloads without bogus provider-prefixed values. (#61382) Thanks @Mule-ME. - Gateway/startup: keep WebSocket RPC available while channels and plugin sidecars start, hold `chat.history` unavailable until startup sidecars finish so synchronous history reads cannot stall startup (reported in #63450), refresh advertised gateway methods after deferred plugin reloads, and enforce the pre-auth WebSocket upgrade budget before the no-handler 503 path so upgrade floods cannot bypass connection limits during that window. (#63480) Thanks @neeravmakwana. +- Dreaming/cron: reconcile managed dreaming cron from the resolved gateway startup config so boot-time schedule recovery respects the configured cadence and timezone. (#63873) Thanks @mbelinky. - Gateway/tailscale: start Tailscale exposure and the gateway update check before awaiting channel and plugin sidecar startup so remote operators are not locked out when startup sidecars stall. - QQBot/streaming: make block streaming configurable per QQ bot account via `streaming.mode` (`"partial"` | `"off"`, default `"partial"`) instead of hardcoding it off, so responses can be delivered incrementally. (#63746) - Dreaming/gateway: require `operator.admin` for persistent `/dreaming on|off` changes and treat missing gateway client scopes as unprivileged instead of silently allowing config writes. (#63872) Thanks @mbelinky. diff --git a/extensions/memory-core/src/dreaming.test.ts b/extensions/memory-core/src/dreaming.test.ts index 437e6403a74..49773d5440f 100644 --- a/extensions/memory-core/src/dreaming.test.ts +++ b/extensions/memory-core/src/dreaming.test.ts @@ -2,9 +2,16 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core"; import { describe, expect, it, vi } from "vitest"; +import { + clearInternalHooks, + createInternalHookEvent, + registerInternalHook, + triggerInternalHook, +} from "../../../src/hooks/internal-hooks.js"; import { __testing, reconcileShortTermDreamingCronJob, + registerShortTermPromotionDreaming, resolveShortTermPromotionDreamingConfig, runShortTermDreamingPromotionIfTriggered, } from "./dreaming.js"; @@ -661,6 +668,63 @@ describe("short-term dreaming cron reconciliation", () => { }); }); +describe("gateway startup reconciliation", () => { + it("uses the startup cfg when reconciling the managed dreaming cron job", async () => { + clearInternalHooks(); + const logger = createLogger(); + const harness = createCronHarness(); + const api = { + config: { plugins: { entries: {} } }, + pluginConfig: {}, + logger, + runtime: {}, + registerHook: (event: string, handler: Parameters[1]) => { + registerInternalHook(event, handler); + }, + on: vi.fn(), + } as never; + + try { + registerShortTermPromotionDreaming(api); + await triggerInternalHook( + createInternalHookEvent("gateway", "startup", "gateway:startup", { + cfg: { + hooks: { internal: { enabled: true } }, + plugins: { + entries: { + "memory-core": { + config: { + dreaming: { + enabled: true, + frequency: "15 4 * * *", + timezone: "UTC", + }, + }, + }, + }, + }, + } as OpenClawConfig, + deps: { cron: harness.cron }, + }), + ); + + expect(harness.addCalls).toHaveLength(1); + expect(harness.addCalls[0]).toMatchObject({ + schedule: { + kind: "cron", + expr: "15 4 * * *", + tz: "UTC", + }, + }); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining("created managed dreaming cron job"), + ); + } finally { + clearInternalHooks(); + } + }); +}); + describe("short-term dreaming trigger", () => { it("applies promotions when the managed dreaming heartbeat event fires", async () => { const logger = createLogger(); diff --git a/extensions/memory-core/src/dreaming.ts b/extensions/memory-core/src/dreaming.ts index c44f45c0bb6..ea0a0b47b6e 100644 --- a/extensions/memory-core/src/dreaming.ts +++ b/extensions/memory-core/src/dreaming.ts @@ -307,6 +307,16 @@ function resolveCronServiceFromStartupEvent(event: unknown): CronServiceLike | n return cron as CronServiceLike; } +function resolveStartupConfigFromEvent(event: unknown, fallback: OpenClawConfig): OpenClawConfig { + const startupEvent = asRecord(event); + const startupContext = asRecord(startupEvent?.context); + const startupCfg = asRecord(startupContext?.cfg); + if (!startupCfg) { + return fallback; + } + return startupCfg as OpenClawConfig; +} + export function resolveShortTermPromotionDreamingConfig(params: { pluginConfig?: Record; cfg?: OpenClawConfig; @@ -584,9 +594,14 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void "gateway:startup", async (event: unknown) => { try { + // Use the resolved startup snapshot so cron reconciliation matches the boot config. + const startupCfg = resolveStartupConfigFromEvent(event, api.config); const config = resolveShortTermPromotionDreamingConfig({ - pluginConfig: resolveMemoryCorePluginConfig(api.config) ?? api.pluginConfig, - cfg: api.config, + pluginConfig: + resolveMemoryCorePluginConfig(startupCfg) ?? + resolveMemoryCorePluginConfig(api.config) ?? + api.pluginConfig, + cfg: startupCfg, }); const cron = resolveCronServiceFromStartupEvent(event); if (!cron && config.enabled) {