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
- Set `retry.maxSpend` to a positive finite number, e.g. `1` or `0.5` USD (or tokens, matching the budget denomination).
- If you want retries off, remove `breakers.retry` instead of zeroing maxSpend.
- 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
- Remove breakers.retry rather than setting maxSpend to 0 to disable retries.
- Coerce env/config strings with Number() and check Number.isFinite before use.
- Cap computed maxSpend formulas so they can never evaluate to 0 or Infinity.
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
- cave_breaker_retry_requires_budget
- cave_breaker_retry_backoff_invalid
- cave_breaker_threshold_invalid
- cave_vercel_terminal_failure
- cave_retry_accounting_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/f13f8b290a0602a4.
Report an issue: GitHub.