JuliusBrussee/caveman · error
cave_breaker_retry_spend_invalid
cave_breaker_retry_spend_invalid
Error message
cave_breaker_retry_spend_invalid
What it means
normalizeRunBreakers requires that when a retry breaker is configured, its `maxSpend` is a finite positive number. maxSpend caps the additional budget the retry loop may consume; a missing, non-finite (NaN/Infinity), zero, or negative value makes the retry cap meaningless, so this sentinel error is thrown.
Solutions
- Set retry.maxSpend to a finite number > 0.
- If you want retries without extra spend allowance, remove the retry breaker or reconsider the value — 0 is rejected.
- Validate config values are parsed to numbers before constructing breakers.
Example fix
// before
retry: { maxSpend: 0 }
// after
retry: { maxSpend: 0.25 } Defensive patterns
Strategy: validation
Validate before calling
function validMaxSpend(v) { return typeof v === 'number' && Number.isFinite(v) && v > 0; }
if (breakers.retry && !validMaxSpend(breakers.retry.maxSpend)) throw new Error('cave_breaker_retry_spend_invalid: need finite maxSpend > 0'); Type guard
const isPositiveFinite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v > 0;
Try / catch
try { normalizeRunBreakers(cfg); } catch (e) {
if (e.message === 'cave_breaker_retry_spend_invalid') console.error('retry.maxSpend must be a finite number > 0, got', cfg.breakers?.retry?.maxSpend);
else throw e;
} Prevention
- Parse spend amounts with Number() and check Number.isFinite before use.
- Never default maxSpend to 0 — use undefined plus a sane default > 0.
- Add runtime config validation at startup for all retry fields.
When it happens
Trigger: Setting `breakers.retry` to an object where maxSpend is undefined, NaN, Infinity, <= 0 (e.g. `retry: { maxSpend: 0 }` or `retry: {}`), while a valid budget exists.
Common situations: Computing maxSpend from another value that is NaN (e.g. parsing a config string); defaulting maxSpend to 0 intending 'no extra spend'; JSON config where maxSpend was omitted and TS types didn't catch it.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- cave_breaker_retry_backoff_invalid
- cave_breaker_retry_requires_budget
- cave_breaker_threshold_invalid
- cave_budget_denomination_ambiguous
- cave_budget_initial_invalid
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/f13f8b290a0602a4.
Report an issue: GitHub.
Appendix: 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 3ee70a1026)