fix(memory-core): use startup config for dreaming cron reconciliation (#63873)

Merged via squash.

Prepared head SHA: 2ec22920cd
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-09 21:36:36 +02:00
committed by GitHub
parent 6af17b39e1
commit 2f130c418f
3 changed files with 82 additions and 2 deletions

View File

@@ -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.

View File

@@ -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<typeof registerInternalHook>[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();

View File

@@ -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<string, unknown>;
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) {