JuliusBrussee/caveman · error · Error

cave_breaker_retry_spend_invalid

Error message

cave_breaker_retry_spend_invalid

What it means

When a retry policy is present (which requires a budget, see error 37), its `maxSpend` — the worst-case spend the run may expose to retries, in the budget's denomination — must be a finite number greater than zero. `normalizeRunBreakers` throws `cave_breaker_retry_spend_invalid` for `0`, negatives, `NaN`, `Infinity`, or non-numbers. Note the looser check compared to thresholds: finite float is fine, only the safe-integer rule does not apply here.

Source

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

    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,
    retryMaxSpend: breakers.retry?.maxSpend,
    retryBackoffMs,
  });
}

/** One breaker decision, recorded on the receipt so a break is never silent. */
export interface BreakerEvent {
  readonly kind: "loop_detected" | "no_progress" | "fan_out_blocked" | "retry_attempted" | "retry_exhausted";

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set `retry.maxSpend` to a positive finite number, e.g. `1` or `0.5` USD (or tokens, matching the budget denomination).
  2. If you want retries off, remove `breakers.retry` instead of zeroing maxSpend.
  3. Validate config-derived values: `Number.isFinite(v) && v > 0` before passing them in.

Example fix

// before
run(agent, { budget: { maxUsd: 5 }, breakers: { retry: { maxSpend: 0 } } });

// after
run(agent, { budget: { maxUsd: 5 }, breakers: { retry: { maxSpend: 1 } } });
Defensive patterns

Strategy: validation

Validate before calling

if (options.breakers?.retry !== undefined) {
  const spend = options.breakers.retry.maxSpend;
  if (!Number.isFinite(spend) || spend <= 0) {
    throw new Error("breakers.retry.maxSpend must be a finite positive number");
  }
}

Type guard

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

Try / catch

try {
  normalizeRunBreakers(breakers, true);
} catch (err) {
  if (err instanceof Error && err.message === "cave_breaker_retry_spend_invalid") {
    // fix the config value; do not retry with the same options
  }
}

Prevention

When it happens

Trigger: Setting `retry: { maxSpend: 0 }` (intending "no retries"), `maxSpend: Infinity`, `maxSpend: NaN` from bad math, or a string like `"1.00"` from config parsing.

Common situations: Reading maxSpend from an env var without numeric coercion; computing it as a ratio that can be 0 on small budgets (`budget * 0`); copy-pasting a config template with a placeholder value.

Related errors


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