mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
Dreaming: throttle cron lifecycle reconciliation
This commit is contained in:
@@ -30,7 +30,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Dreaming/cron: keep managed dreaming cron reconciled after startup by rechecking lifecycle state during runtime config/plugin changes, recovering missing managed jobs, and applying cadence/timezone updates idempotently.
|
||||
- Dreaming/cron: keep managed dreaming cron reconciled after startup by rechecking lifecycle state during runtime config/plugin changes, recovering missing managed jobs, and applying cadence/timezone updates idempotently. (#63929) 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.
|
||||
|
||||
@@ -49,12 +49,14 @@ function createCronHarness(
|
||||
opts?: { removeResult?: "boolean" | "unknown"; removeThrowsForIds?: string[] },
|
||||
) {
|
||||
const jobs: CronJobLike[] = [...initialJobs];
|
||||
let listCalls = 0;
|
||||
const addCalls: CronAddInput[] = [];
|
||||
const updateCalls: Array<{ id: string; patch: CronPatch }> = [];
|
||||
const removeCalls: string[] = [];
|
||||
|
||||
const cron: CronParam = {
|
||||
async list() {
|
||||
listCalls += 1;
|
||||
return jobs.map((job) => ({
|
||||
...job,
|
||||
...(job.schedule ? { schedule: { ...job.schedule } } : {}),
|
||||
@@ -111,7 +113,16 @@ function createCronHarness(
|
||||
},
|
||||
};
|
||||
|
||||
return { cron, jobs, addCalls, updateCalls, removeCalls };
|
||||
return {
|
||||
cron,
|
||||
jobs,
|
||||
addCalls,
|
||||
updateCalls,
|
||||
removeCalls,
|
||||
get listCalls() {
|
||||
return listCalls;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getBeforeAgentReplyHandler(
|
||||
@@ -963,6 +974,63 @@ describe("gateway startup reconciliation", () => {
|
||||
clearInternalHooks();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not reconcile managed cron on every repeated runtime reply", async () => {
|
||||
clearInternalHooks();
|
||||
const logger = createLogger();
|
||||
const harness = createCronHarness();
|
||||
const onMock = vi.fn();
|
||||
const now = Date.parse("2026-04-10T12:00:00Z");
|
||||
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(now);
|
||||
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(2);
|
||||
} finally {
|
||||
nowSpy.mockRestore();
|
||||
clearInternalHooks();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("short-term dreaming trigger", () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ const LEGACY_LIGHT_SLEEP_EVENT_TEXT = "__openclaw_memory_core_light_sleep__";
|
||||
const LEGACY_REM_SLEEP_CRON_NAME = "Memory REM Dreaming";
|
||||
const LEGACY_REM_SLEEP_CRON_TAG = "[managed-by=memory-core.dreaming.rem]";
|
||||
const LEGACY_REM_SLEEP_EVENT_TEXT = "__openclaw_memory_core_rem_sleep__";
|
||||
const RUNTIME_CRON_RECONCILE_INTERVAL_MS = 60_000;
|
||||
|
||||
type Logger = Pick<OpenClawPluginApi["logger"], "info" | "warn" | "error">;
|
||||
|
||||
@@ -618,6 +619,25 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
|
||||
export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void {
|
||||
let startupCronSource: StartupCronSourceRefs | null = null;
|
||||
let unavailableCronWarningEmitted = false;
|
||||
let lastRuntimeReconcileAtMs = 0;
|
||||
let lastRuntimeConfigKey: string | null = null;
|
||||
let lastRuntimeCronRef: CronServiceLike | null = null;
|
||||
|
||||
const runtimeConfigKey = (config: ShortTermPromotionDreamingConfig): string =>
|
||||
[
|
||||
config.enabled ? "enabled" : "disabled",
|
||||
config.cron,
|
||||
config.timezone ?? "",
|
||||
String(config.limit),
|
||||
String(config.minScore),
|
||||
String(config.minRecallCount),
|
||||
String(config.minUniqueQueries),
|
||||
String(config.recencyHalfLifeDays ?? ""),
|
||||
String(config.maxAgeDays ?? ""),
|
||||
config.verboseLogging ? "verbose" : "quiet",
|
||||
config.storage?.mode ?? "",
|
||||
config.storage?.separateReports ? "separate" : "inline",
|
||||
].join("|");
|
||||
|
||||
const reconcileManagedDreamingCron = async (params: {
|
||||
reason: "startup" | "runtime";
|
||||
@@ -638,6 +658,7 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
|
||||
startupCronSource = resolveStartupCronSourceFromEvent(params.startupEvent);
|
||||
}
|
||||
const cron = resolveCronServiceFromStartupSource(startupCronSource);
|
||||
const configKey = runtimeConfigKey(config);
|
||||
if (!cron && config.enabled && !unavailableCronWarningEmitted) {
|
||||
api.logger.warn(
|
||||
"memory-core: managed dreaming cron could not be reconciled (cron service unavailable).",
|
||||
@@ -647,6 +668,21 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
|
||||
if (cron) {
|
||||
unavailableCronWarningEmitted = false;
|
||||
}
|
||||
if (params.reason === "runtime") {
|
||||
const now = Date.now();
|
||||
const withinThrottleWindow =
|
||||
now - lastRuntimeReconcileAtMs < RUNTIME_CRON_RECONCILE_INTERVAL_MS;
|
||||
if (
|
||||
withinThrottleWindow &&
|
||||
lastRuntimeConfigKey === configKey &&
|
||||
lastRuntimeCronRef === cron
|
||||
) {
|
||||
return config;
|
||||
}
|
||||
lastRuntimeReconcileAtMs = now;
|
||||
lastRuntimeConfigKey = configKey;
|
||||
lastRuntimeCronRef = cron;
|
||||
}
|
||||
await reconcileShortTermDreamingCronJob({
|
||||
cron,
|
||||
config,
|
||||
|
||||
Reference in New Issue
Block a user