fix(memory-core): suppress startup "cron service unavailable" warning (closes #69939)

memory-core registers a gateway:startup hook that runs reconcileManagedDreamingCron() before deps.cron is attached to the startup event (the startup hook is deferred via a 250ms setTimeout in server.impl).

Downgrade the first startup-time "cron service unavailable" warning to a debug log, and rely on the existing runtime reconciliation path to warn if the cron service truly stays unavailable after boot. The managed dreaming cron job itself runs correctly — this was a log-noise regression, not a functional failure.

Signed-off-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
This commit is contained in:
Sanjay Santhanam
2026-04-21 19:09:23 -07:00
committed by Peter Steinberger
parent f027d8faa7
commit a37321ad5f
2 changed files with 121 additions and 4 deletions

View File

@@ -42,6 +42,7 @@ type DreamingPluginApiTestDouble = {
function createLogger() {
return {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
@@ -1240,6 +1241,111 @@ describe("gateway startup reconciliation", () => {
clearInternalHooks();
}
});
it("does not emit the cron-unavailable warning on gateway:startup when deps.cron is missing (regression #69939)", async () => {
clearInternalHooks();
const logger = createLogger();
const api: DreamingPluginApiTestDouble = {
config: { plugins: { entries: {} } },
pluginConfig: {},
logger,
runtime: {},
registerHook: (event: string, handler: Parameters<typeof registerInternalHook>[1]) => {
registerInternalHook(event, handler);
},
on: vi.fn(),
};
try {
registerShortTermPromotionDreamingForTest(api);
// Simulate the startup race: gateway:startup fires before deps.cron is attached.
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: {},
}),
);
expect(logger.warn).not.toHaveBeenCalledWith(
expect.stringContaining("cron service unavailable"),
);
// The startup-path log should be demoted to debug instead.
expect(logger.debug).toHaveBeenCalledWith(
expect.stringContaining("cron service not yet available at gateway:startup"),
);
} finally {
clearInternalHooks();
}
});
it("still warns on runtime reconciliation when cron remains unavailable (preserves #69939 genuine-failure signal)", async () => {
clearInternalHooks();
const logger = createLogger();
const onMock = vi.fn();
const api: DreamingPluginApiTestDouble = {
config: {
plugins: {
entries: {
"memory-core": {
config: {
dreaming: {
enabled: true,
frequency: "15 4 * * *",
timezone: "UTC",
},
},
},
},
},
},
pluginConfig: {},
logger,
runtime: {},
registerHook: (event: string, handler: Parameters<typeof registerInternalHook>[1]) => {
registerInternalHook(event, handler);
},
on: onMock,
};
try {
registerShortTermPromotionDreamingForTest(api);
// Startup without cron — must stay silent on warn.
await triggerInternalHook(
createInternalHookEvent("gateway", "startup", "gateway:startup", {
cfg: api.config,
deps: {},
}),
);
expect(logger.warn).not.toHaveBeenCalled();
// Now a runtime heartbeat reconciliation happens and cron is still missing
// (e.g. the cron service genuinely failed to initialize). The warning must fire.
const beforeAgentReply = getBeforeAgentReplyHandler(onMock);
await beforeAgentReply(
{ cleanedBody: "" },
{ trigger: "heartbeat", workspaceDir: ".", sessionKey: "agent:main:main:heartbeat" },
);
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("cron service unavailable"));
} finally {
clearInternalHooks();
}
});
});
describe("short-term dreaming trigger", () => {

View File

@@ -718,10 +718,21 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
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).",
);
unavailableCronWarningEmitted = true;
// The gateway emits `gateway:startup` via a deferred setTimeout, and
// `deps.cron` may not be attached to the event context yet when memory-core's
// startup hook fires (see issue #69939). Avoid logging a confusing warning on
// the startup path — the runtime reconciliation path (heartbeat-driven) will
// still warn if the cron service remains unavailable after boot.
if (params.reason === "startup") {
api.logger.debug?.(
"memory-core: cron service not yet available at gateway:startup; deferring to runtime reconciliation.",
);
} else {
api.logger.warn(
"memory-core: managed dreaming cron could not be reconciled (cron service unavailable).",
);
unavailableCronWarningEmitted = true;
}
}
if (cron) {
unavailableCronWarningEmitted = false;