refactor(reply): split abort cutoff and timeout policy modules

This commit is contained in:
Peter Steinberger
2026-02-26 14:00:31 +01:00
parent f53e4e9ffb
commit b402770f63
9 changed files with 476 additions and 309 deletions

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import type { CronJob } from "../types.js";
import {
AGENT_TURN_SAFETY_TIMEOUT_MS,
DEFAULT_JOB_TIMEOUT_MS,
resolveCronJobTimeoutMs,
} from "./timeout-policy.js";
function makeJob(payload: CronJob["payload"]): CronJob {
const sessionTarget = payload.kind === "agentTurn" ? "isolated" : "main";
return {
id: "job-1",
name: "job",
createdAtMs: 0,
updatedAtMs: 0,
enabled: true,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget,
wakeMode: "next-heartbeat",
payload,
state: {},
};
}
describe("timeout-policy", () => {
it("uses default timeout for non-agent jobs", () => {
const timeout = resolveCronJobTimeoutMs(makeJob({ kind: "systemEvent", text: "hello" }));
expect(timeout).toBe(DEFAULT_JOB_TIMEOUT_MS);
});
it("uses expanded safety timeout for agentTurn jobs without explicit timeout", () => {
const timeout = resolveCronJobTimeoutMs(makeJob({ kind: "agentTurn", message: "hi" }));
expect(timeout).toBe(AGENT_TURN_SAFETY_TIMEOUT_MS);
});
it("disables timeout when timeoutSeconds <= 0", () => {
const timeout = resolveCronJobTimeoutMs(
makeJob({ kind: "agentTurn", message: "hi", timeoutSeconds: 0 }),
);
expect(timeout).toBeUndefined();
});
it("applies explicit timeoutSeconds when positive", () => {
const timeout = resolveCronJobTimeoutMs(
makeJob({ kind: "agentTurn", message: "hi", timeoutSeconds: 1.9 }),
);
expect(timeout).toBe(1_900);
});
});

View File

@@ -0,0 +1,25 @@
import type { CronJob } from "../types.js";
/**
* Maximum wall-clock time for a single job execution. Acts as a safety net
* on top of per-provider/per-agent timeouts to prevent one stuck job from
* wedging the entire cron lane.
*/
export const DEFAULT_JOB_TIMEOUT_MS = 10 * 60_000; // 10 minutes
/**
* Agent turns can legitimately run much longer than generic cron jobs.
* Use a larger safety ceiling when no explicit timeout is set.
*/
export const AGENT_TURN_SAFETY_TIMEOUT_MS = 60 * 60_000; // 60 minutes
export function resolveCronJobTimeoutMs(job: CronJob): number | undefined {
const configuredTimeoutMs =
job.payload.kind === "agentTurn" && typeof job.payload.timeoutSeconds === "number"
? Math.floor(job.payload.timeoutSeconds * 1_000)
: undefined;
if (configuredTimeoutMs === undefined) {
return job.payload.kind === "agentTurn" ? AGENT_TURN_SAFETY_TIMEOUT_MS : DEFAULT_JOB_TIMEOUT_MS;
}
return configuredTimeoutMs <= 0 ? undefined : configuredTimeoutMs;
}

View File

@@ -18,6 +18,9 @@ import {
import { locked } from "./locked.js";
import type { CronEvent, CronServiceState } from "./state.js";
import { ensureLoaded, persist } from "./store.js";
import { DEFAULT_JOB_TIMEOUT_MS, resolveCronJobTimeoutMs } from "./timeout-policy.js";
export { DEFAULT_JOB_TIMEOUT_MS } from "./timeout-policy.js";
const MAX_TIMER_DELAY_MS = 60_000;
@@ -30,14 +33,6 @@ const MAX_TIMER_DELAY_MS = 60_000;
*/
const MIN_REFIRE_GAP_MS = 2_000;
/**
* Maximum wall-clock time for a single job execution. Acts as a safety net
* on top of the per-provider / per-agent timeouts to prevent one stuck job
* from wedging the entire cron lane.
*/
export const DEFAULT_JOB_TIMEOUT_MS = 10 * 60_000; // 10 minutes
const AGENT_TURN_SAFETY_TIMEOUT_MS = 60 * 60_000; // 60 minutes
type TimedCronRunOutcome = CronRunOutcome &
CronRunTelemetry & {
jobId: string;
@@ -47,17 +42,6 @@ type TimedCronRunOutcome = CronRunOutcome &
endedAt: number;
};
function resolveCronJobTimeoutMs(job: CronJob): number | undefined {
const configuredTimeoutMs =
job.payload.kind === "agentTurn" && typeof job.payload.timeoutSeconds === "number"
? Math.floor(job.payload.timeoutSeconds * 1_000)
: undefined;
if (configuredTimeoutMs === undefined) {
return job.payload.kind === "agentTurn" ? AGENT_TURN_SAFETY_TIMEOUT_MS : DEFAULT_JOB_TIMEOUT_MS;
}
return configuredTimeoutMs <= 0 ? undefined : configuredTimeoutMs;
}
export async function executeJobCoreWithTimeout(
state: CronServiceState,
job: CronJob,