mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 13:11:19 +00:00
fix(cron): run explicit wakes outside heartbeat hours (#105830)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -41,6 +41,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
storePath: string;
|
||||
target?: "telegram" | "none";
|
||||
isolatedSession?: boolean;
|
||||
activeHours?: boolean;
|
||||
}): Promise<{ cfg: OpenClawConfig; sessionKey: string }> => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
@@ -50,6 +51,9 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
every: "5m",
|
||||
target: params.target ?? "telegram",
|
||||
...(params.isolatedSession === true ? { isolatedSession: true } : {}),
|
||||
...(params.activeHours === true
|
||||
? { activeHours: { start: "08:00", end: "24:00", timezone: "user" as const } }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -196,6 +200,8 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
owningCronLaneTaskMarker?: CommandLaneTaskMarker;
|
||||
cronLaneDepth?: number;
|
||||
cronNestedLaneDepth?: number;
|
||||
activeHours?: boolean;
|
||||
nowMs?: number;
|
||||
}): Promise<{
|
||||
result: Awaited<ReturnType<typeof runHeartbeatOnce>>;
|
||||
sendTelegram: ReturnType<typeof vi.fn>;
|
||||
@@ -215,6 +221,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
storePath,
|
||||
target: params.target,
|
||||
isolatedSession: params.isolatedSession,
|
||||
activeHours: params.activeHours,
|
||||
});
|
||||
params.enqueue(sessionKey);
|
||||
const owningCronJobMarker = params.owningCronJobId
|
||||
@@ -244,6 +251,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
deps: {
|
||||
getReplyFromConfig: getReplySpy,
|
||||
telegram: sendTelegram,
|
||||
nowMs: () => params.nowMs ?? Date.now(),
|
||||
...(params.cronLaneDepth === undefined && params.cronNestedLaneDepth === undefined
|
||||
? {}
|
||||
: {
|
||||
@@ -310,6 +318,30 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
expect(sendTelegram).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs the tagged cron payload outside heartbeat active hours", async () => {
|
||||
const reminderText = "Reminder: Send the overnight report";
|
||||
const { result, sendTelegram, calledCtx, replyCallCount } = await runHeartbeatCase({
|
||||
tmpPrefix: "openclaw-cron-quiet-hours-",
|
||||
replyText: "Overnight report sent",
|
||||
reason: "cron:overnight-report",
|
||||
source: "cron",
|
||||
intent: "immediate",
|
||||
activeHours: true,
|
||||
nowMs: Date.UTC(2025, 0, 1, 7, 0, 0),
|
||||
enqueue: (sessionKey) => {
|
||||
enqueueSystemEvent(reminderText, {
|
||||
sessionKey,
|
||||
contextKey: "cron:overnight-report",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.status).toBe("ran");
|
||||
expect(replyCallCount).toBe(1);
|
||||
expectCronEventPrompt(calledCtx, reminderText);
|
||||
expect(sendTelegram).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses CRON_EVENT_PROMPT when cron events are mixed with heartbeat noise", async () => {
|
||||
const { result, sendTelegram, calledCtx } = await runCronReminderCase(
|
||||
"openclaw-cron-mixed-",
|
||||
|
||||
@@ -1324,12 +1324,13 @@ export async function runHeartbeatOnce(opts: {
|
||||
const agentId = normalizeAgentId(
|
||||
explicitAgentId || forcedSessionAgentId || resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const wakeSource = opts.source ?? inferHeartbeatWakeSourceFromReason(opts.reason);
|
||||
const heartbeat = resolveHeartbeatForWake({
|
||||
cfg,
|
||||
agentId,
|
||||
requestedHeartbeat: opts.heartbeat,
|
||||
source: opts.source,
|
||||
mergeRequestedHeartbeat: opts.source === "cron",
|
||||
source: wakeSource,
|
||||
mergeRequestedHeartbeat: wakeSource === "cron",
|
||||
});
|
||||
const runScope = opts.runScope ?? "global";
|
||||
const allowsUnscheduledTarget =
|
||||
@@ -1345,7 +1346,12 @@ export async function runHeartbeatOnce(opts: {
|
||||
}
|
||||
|
||||
const startedAt = opts.deps?.nowMs?.() ?? Date.now();
|
||||
if (!allowsUnscheduledTarget && !isWithinActiveHours(cfg, heartbeat, startedAt)) {
|
||||
// Cron uses the heartbeat runner as execution transport; heartbeat scheduling windows do not own it.
|
||||
if (
|
||||
!allowsUnscheduledTarget &&
|
||||
wakeSource !== "cron" &&
|
||||
!isWithinActiveHours(cfg, heartbeat, startedAt)
|
||||
) {
|
||||
return { status: "skipped", reason: "quiet-hours" };
|
||||
}
|
||||
|
||||
@@ -1445,7 +1451,7 @@ export async function runHeartbeatOnce(opts: {
|
||||
heartbeat,
|
||||
runScope,
|
||||
forcedSessionKey: opts.sessionKey,
|
||||
source: opts.source,
|
||||
source: wakeSource,
|
||||
reason: opts.reason,
|
||||
nowMs: startedAt,
|
||||
});
|
||||
@@ -1965,7 +1971,7 @@ export async function runHeartbeatOnce(opts: {
|
||||
runSessionKey,
|
||||
response: heartbeatToolResponse,
|
||||
taskNames: dueHeartbeatTasks.map((task) => task.name),
|
||||
wakeSource: opts.source,
|
||||
wakeSource,
|
||||
wakeReason: opts.reason,
|
||||
occurredAt: startedAt,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user