JuliusBrussee/caveman · error · Error

cave_breaker_threshold_invalid

cave_breaker_threshold_invalid

Error message

cave_breaker_threshold_invalid

What it means

`normalizeRunBreakers` validates the four breaker thresholds — `repeatedToolCalls`, `repeatedToolCallWindowTurns`, `noProgressTurns`, `maxToolCallsPerTurn` — after applying defaults. Each must be a safe integer greater than zero; anything else (zero, negative, fractional, `Infinity`, `NaN`, or a value beyond `Number.MAX_SAFE_INTEGER`) throws `cave_breaker_threshold_invalid`. Defaults are valid, so this error always traces to explicitly supplied `RunOptions.breakers` values.

Source

Thrown at packages/agent/src/breakers.ts:85

const DEFAULT_RETRY_BACKOFF_MS = 250;

export function normalizeRunBreakers(
  breakers: RunBreakers,
  hasBudget: boolean,
): NormalizedBreakers {
  const repeatedToolCalls = breakers.repeatedToolCalls ?? DEFAULT_REPEATED_TOOL_CALLS;
  const repeatedToolCallWindowTurns = breakers.repeatedToolCallWindowTurns ??
    DEFAULT_REPEATED_TOOL_CALL_WINDOW_TURNS;
  const noProgressTurns = breakers.noProgressTurns ?? DEFAULT_NO_PROGRESS_TURNS;
  const maxToolCallsPerTurn = breakers.maxToolCallsPerTurn ?? DEFAULT_MAX_TOOL_CALLS_PER_TURN;
  for (const value of [
    repeatedToolCalls,
    repeatedToolCallWindowTurns,
    noProgressTurns,
    maxToolCallsPerTurn,
  ]) {
    if (!Number.isSafeInteger(value) || value <= 0) {
      throw new Error("cave_breaker_threshold_invalid");
    }
  }
  if (breakers.retry !== undefined && !hasBudget) {
    throw new Error("cave_breaker_retry_requires_budget");
  }
  if (breakers.retry !== undefined &&
      (!Number.isFinite(breakers.retry.maxSpend) || breakers.retry.maxSpend <= 0)) {
    throw new Error("cave_breaker_retry_spend_invalid");
  }
  const retryBackoffMs = breakers.retry?.backoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
  if (!Number.isSafeInteger(retryBackoffMs) || retryBackoffMs < 0) {
    throw new Error("cave_breaker_retry_backoff_invalid");
  }
  return Object.freeze({
    repeatedToolCalls,
    repeatedToolCallWindowTurns,
    noProgressTurns,
    maxToolCallsPerTurn,

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set every supplied threshold to a whole number ≥ 1 (e.g. `repeatedToolCalls: 3`).
  2. To disable a breaker, omit its field from `RunOptions.breakers` entirely rather than passing 0.
  3. Coerce and check config values before passing them: `Number.isSafeInteger(v) && v > 0`.

Example fix

// before
run(agent, { breakers: { repeatedToolCalls: 0, noProgressTurns: 2.5 } });

// after
run(agent, { breakers: { repeatedToolCalls: 3, noProgressTurns: 3 } });
// or omit a breaker to leave it at its default
Defensive patterns

Strategy: validation

Validate before calling

function isValidBreakerThreshold(v: unknown): v is number {
  return Number.isSafeInteger(v) && (v as number) > 0;
}
const thresholds = [breakers.repeatedToolCalls, breakers.noProgressTurns].filter((v) => v !== undefined);
if (!thresholds.every(isValidBreakerThreshold)) throw new Error("breaker thresholds must be positive integers");

Type guard

function isPositiveSafeInteger(v: unknown): v is number {
  return Number.isSafeInteger(v) && (v as number) > 0;
}

Try / catch

try {
  normalizeRunBreakers(breakers, hasBudget);
} catch (err) {
  if (err instanceof Error && err.message === "cave_breaker_threshold_invalid") {
    // config error: fix the offending threshold; retrying unchanged is pointless
  }
}

Prevention

When it happens

Trigger: Setting `breakers: { repeatedToolCalls: 0 }` (perhaps intending "disabled"), `{ noProgressTurns: 2.5 }`, `{ maxToolCallsPerTurn: Infinity }`, or reading thresholds from env/config that yield strings or NaN.

Common situations: Trying to disable a breaker with 0 (breakers are opt-in — omit the field instead); config parsed from CLI flags as strings; computing thresholds dynamically (e.g. `turns * 1.5`) producing fractions; `parseInt(undefined)` yielding NaN.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/98524ce48d12436a. Report an issue: GitHub.