fix(cron): cancel timed-out runs before side effects (openclaw#22411) thanks @Takhoffman

Verified:
- pnpm check
- pnpm vitest run src/memory/qmd-manager.test.ts src/cron/service.issue-regressions.test.ts src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts --maxWorkers=1

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Tak Hoffman
2026-02-22 15:45:27 -06:00
committed by GitHub
parent 64b273a71c
commit 556af3f08b
5 changed files with 195 additions and 8 deletions

View File

@@ -731,6 +731,60 @@ describe("Cron issue regressions", () => {
expect(job?.state.lastError).toContain("timed out");
});
it("suppresses isolated follow-up side effects after timeout", async () => {
vi.useRealTimers();
const store = await makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
const enqueueSystemEvent = vi.fn();
const cronJob = createIsolatedRegressionJob({
id: "timeout-side-effects",
name: "timeout side effects",
scheduledAt,
schedule: { kind: "every", everyMs: 60_000, anchorMs: scheduledAt },
payload: { kind: "agentTurn", message: "work", timeoutSeconds: 0.01 },
state: { nextRunAtMs: scheduledAt },
});
await writeCronJobs(store.storePath, [cronJob]);
let now = scheduledAt;
const state = createCronServiceState({
cronEnabled: true,
storePath: store.storePath,
log: noopLogger,
nowMs: () => now,
enqueueSystemEvent,
requestHeartbeatNow: vi.fn(),
runIsolatedAgentJob: vi.fn(async (params) => {
const abortSignal = params.abortSignal;
await new Promise<void>((resolve, reject) => {
const onAbort = () => {
abortSignal?.removeEventListener("abort", onAbort);
now += 100;
reject(new Error("aborted"));
};
abortSignal?.addEventListener("abort", onAbort, { once: true });
});
return {
status: "ok" as const,
summary: "late-summary",
delivered: false,
error:
abortSignal?.aborted && typeof abortSignal.reason === "string"
? abortSignal.reason
: undefined,
};
}),
});
await onTimer(state);
const jobAfterTimeout = state.store?.jobs.find((j) => j.id === "timeout-side-effects");
expect(jobAfterTimeout?.state.lastStatus).toBe("error");
expect(jobAfterTimeout?.state.lastError).toContain("timed out");
expect(enqueueSystemEvent).not.toHaveBeenCalled();
});
it("applies timeoutSeconds to manual cron.run isolated executions", async () => {
vi.useRealTimers();
const store = await makeStorePath();