Cron: respect aborts in main wake-now retries (#23967)

* Cron: respect aborts in main wake-now retries

* Changelog: add main-session cron abort retry fix note

* Cron tests: format post-rebase conflict resolution
This commit is contained in:
Tak Hoffman
2026-02-22 17:19:27 -06:00
committed by GitHub
parent 9bc265f379
commit 3efe63d1ad
3 changed files with 91 additions and 6 deletions

View File

@@ -607,8 +607,34 @@ export async function executeJobCore(
job: CronJob,
abortSignal?: AbortSignal,
): Promise<CronRunOutcome & CronRunTelemetry & { delivered?: boolean }> {
const resolveAbortError = () => ({
status: "error" as const,
error: timeoutErrorMessage(),
});
const waitWithAbort = async (ms: number) => {
if (!abortSignal) {
await new Promise<void>((resolve) => setTimeout(resolve, ms));
return;
}
if (abortSignal.aborted) {
return;
}
await new Promise<void>((resolve) => {
const timer = setTimeout(() => {
abortSignal.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
abortSignal.removeEventListener("abort", onAbort);
resolve();
};
abortSignal.addEventListener("abort", onAbort, { once: true });
});
};
if (abortSignal?.aborted) {
return { status: "error", error: timeoutErrorMessage() };
return resolveAbortError();
}
if (job.sessionTarget === "main") {
const text = resolveJobPayloadTextForMain(job);
@@ -629,7 +655,6 @@ export async function executeJobCore(
});
if (job.wakeMode === "now" && state.deps.runHeartbeatOnce) {
const reason = `cron:${job.id}`;
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
const maxWaitMs = state.deps.wakeNowHeartbeatBusyMaxWaitMs ?? 2 * 60_000;
const retryDelayMs = state.deps.wakeNowHeartbeatBusyRetryDelayMs ?? 250;
const waitStartedAt = state.deps.nowMs();
@@ -637,7 +662,7 @@ export async function executeJobCore(
let heartbeatResult: HeartbeatRunResult;
for (;;) {
if (abortSignal?.aborted) {
return { status: "error", error: timeoutErrorMessage() };
return resolveAbortError();
}
heartbeatResult = await state.deps.runHeartbeatOnce({
reason,
@@ -650,7 +675,13 @@ export async function executeJobCore(
) {
break;
}
if (abortSignal?.aborted) {
return resolveAbortError();
}
if (state.deps.nowMs() - waitStartedAt > maxWaitMs) {
if (abortSignal?.aborted) {
return resolveAbortError();
}
state.deps.requestHeartbeatNow({
reason,
agentId: job.agentId,
@@ -658,7 +689,7 @@ export async function executeJobCore(
});
return { status: "ok", summary: text };
}
await delay(retryDelayMs);
await waitWithAbort(retryDelayMs);
}
if (heartbeatResult.status === "ran") {
@@ -669,6 +700,9 @@ export async function executeJobCore(
return { status: "error", error: heartbeatResult.reason, summary: text };
}
} else {
if (abortSignal?.aborted) {
return resolveAbortError();
}
state.deps.requestHeartbeatNow({
reason: `cron:${job.id}`,
agentId: job.agentId,
@@ -682,7 +716,7 @@ export async function executeJobCore(
return { status: "skipped", error: "isolated job requires payload.kind=agentTurn" };
}
if (abortSignal?.aborted) {
return { status: "error", error: timeoutErrorMessage() };
return resolveAbortError();
}
const res = await state.deps.runIsolatedAgentJob({