{"record":{"id":"f13f8b290a0602a4","repo":"JuliusBrussee/caveman","slug":"cave-breaker-retry-spend-invalid","errorCode":null,"errorMessage":"cave_breaker_retry_spend_invalid","messagePattern":"cave_breaker_retry_spend_invalid","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/breakers.ts","lineNumber":93,"sourceCode":"    DEFAULT_REPEATED_TOOL_CALL_WINDOW_TURNS;\n  const noProgressTurns = breakers.noProgressTurns ?? DEFAULT_NO_PROGRESS_TURNS;\n  const maxToolCallsPerTurn = breakers.maxToolCallsPerTurn ?? DEFAULT_MAX_TOOL_CALLS_PER_TURN;\n  for (const value of [\n    repeatedToolCalls,\n    repeatedToolCallWindowTurns,\n    noProgressTurns,\n    maxToolCallsPerTurn,\n  ]) {\n    if (!Number.isSafeInteger(value) || value <= 0) {\n      throw new Error(\"cave_breaker_threshold_invalid\");\n    }\n  }\n  if (breakers.retry !== undefined && !hasBudget) {\n    throw new Error(\"cave_breaker_retry_requires_budget\");\n  }\n  if (breakers.retry !== undefined &&\n      (!Number.isFinite(breakers.retry.maxSpend) || breakers.retry.maxSpend <= 0)) {\n    throw new Error(\"cave_breaker_retry_spend_invalid\");\n  }\n  const retryBackoffMs = breakers.retry?.backoffMs ?? DEFAULT_RETRY_BACKOFF_MS;\n  if (!Number.isSafeInteger(retryBackoffMs) || retryBackoffMs < 0) {\n    throw new Error(\"cave_breaker_retry_backoff_invalid\");\n  }\n  return Object.freeze({\n    repeatedToolCalls,\n    repeatedToolCallWindowTurns,\n    noProgressTurns,\n    maxToolCallsPerTurn,\n    retryMaxSpend: breakers.retry?.maxSpend,\n    retryBackoffMs,\n  });\n}\n\n/** One breaker decision, recorded on the receipt so a break is never silent. */\nexport interface BreakerEvent {\n  readonly kind: \"loop_detected\" | \"no_progress\" | \"fan_out_blocked\" | \"retry_attempted\" | \"retry_exhausted\";","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/breakers.ts#L75-L111","documentation":"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.","triggerScenarios":"Setting `retry: { maxSpend: 0 }` (intending \"no retries\"), `maxSpend: Infinity`, `maxSpend: NaN` from bad math, or a string like `\"1.00\"` from config parsing.","commonSituations":"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.","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."],"exampleFix":"// before\nrun(agent, { budget: { maxUsd: 5 }, breakers: { retry: { maxSpend: 0 } } });\n\n// after\nrun(agent, { budget: { maxUsd: 5 }, breakers: { retry: { maxSpend: 1 } } });","handlingStrategy":"validation","validationCode":"if (options.breakers?.retry !== undefined) {\n  const spend = options.breakers.retry.maxSpend;\n  if (!Number.isFinite(spend) || spend <= 0) {\n    throw new Error(\"breakers.retry.maxSpend must be a finite positive number\");\n  }\n}","typeGuard":"function isValidRetrySpend(v: unknown): v is number {\n  return Number.isFinite(v) && (v as number) > 0;\n}","tryCatchPattern":"try {\n  normalizeRunBreakers(breakers, true);\n} catch (err) {\n  if (err instanceof Error && err.message === \"cave_breaker_retry_spend_invalid\") {\n    // fix the config value; do not retry with the same options\n  }\n}","preventionTips":["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."],"tags":["breakers","budget","retry","configuration"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}