JuliusBrussee/caveman · error
caveman build: runner timeout must be a positive integer
Error message
caveman build: runner timeout must be a positive integer
What it means
withDeadline wraps runner invocations with an AbortSignal timeout and first validates timeoutMs. This error means the timeout was not a safe positive integer (it must be a whole number of milliseconds, > 0, within Number.MAX_SAFE_INTEGER). The library throws before starting any runner work.
Solutions
- Pass a positive integer milliseconds value, e.g. 30_000
- Convert with Math.round/ceil and validate with Number.isSafeInteger before calling
- Fix duration parsing so unit conversion yields whole milliseconds
- Replace unset/0 defaults with an explicit sane timeout
Example fix
// before await withDeadline(invoke, config.timeoutSeconds * 0.001); // after await withDeadline(invoke, Math.ceil(config.timeoutSeconds * 1000));
Defensive patterns
Strategy: validation
Validate before calling
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
throw new Error(`runner timeout must be a positive integer ms, got ${timeoutMs}`);
} Type guard
function isValidTimeoutMs(v: unknown): v is number {
return typeof v === "number" && Number.isSafeInteger(v) && v > 0;
} Try / catch
try {
await withDeadline(invoke, timeoutMs);
} catch (e) {
if (e instanceof Error && e.message === "caveman build: runner timeout must be a positive integer") {
// fix the timeout source (unit conversion / unset default)
} else throw e;
} Prevention
- Keep all timeouts in whole milliseconds end to end
- Convert seconds to ms with Math.ceil before passing
- Guard parsed duration strings for NaN/fractions
When it happens
Trigger: Calling withDeadline (directly or via compile's runner path) with timeoutMs = 0, negative, fractional (e.g. 1.5), NaN, or Infinity.
Common situations: A config timeout expressed in seconds (2) misread as milliseconds then divided (0.002); an unset numeric field defaulting to 0; parsing a duration string with parseFloat yielding a fraction or NaN.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- cave_breaker_threshold_invalid
- cave_${field}_invalid
- cave_run_deadline_invalid
- caveman agent: tool timeoutMs must be a positive integer
- timeoutMs must be a positive integer
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/66e2d3db659eb028.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/build.ts:438
...lockWithoutBuild,
build_sha256: buildSha256,
};
return {
...base,
status: "locked",
best_observed_plan_id: selected.candidate.plan.plan_id,
baseline_catalog_cost_usd_per_task: baselineCompleted.costPerTask,
selected_catalog_cost_usd_per_task: selected.costPerTask,
lock,
};
}
async function withDeadline<T>(
invoke: (signal: AbortSignal) => Promise<T>,
timeoutMs: number,
): Promise<T> {
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
throw new Error("caveman build: runner timeout must be a positive integer");
}
const controller = new AbortController();
let timer: NodeJS.Timeout | undefined;
const deadline = new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
const error = new Error("cave_build_runner_timeout");
controller.abort(error);
reject(error);
}, timeoutMs);
});
try {
// Abort asks cooperative runners to stop; the independent race prevents an
// adapter that ignores AbortSignal from pinning the compiler forever. A late
// runner can never mint a lock or known actual cost after this branch returns.
return await Promise.race([Promise.resolve().then(() => invoke(controller.signal)), deadline]);
} finally {
if (timer !== undefined) clearTimeout(timer);
}View on GitHub (pinned to 3ee70a1026)