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

  1. Pass a positive integer milliseconds value, e.g. 30_000
  2. Convert with Math.round/ceil and validate with Number.isSafeInteger before calling
  3. Fix duration parsing so unit conversion yields whole milliseconds
  4. 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

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.

Related errors


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)