{"record":{"id":"f13f8b290a0602a4","repo":"JuliusBrussee/caveman","slug":"cave-breaker-retry-spend-invalid","errorCode":"cave_breaker_retry_spend_invalid","errorMessage":"cave_breaker_retry_spend_invalid","messagePattern":"cave_breaker_retry_spend_invalid","errorType":"error_code","errorClass":null,"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/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/breakers.ts#L75-L111","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nretry: { maxSpend: 0 }\n// after\nretry: { maxSpend: 0.25 }","handlingStrategy":"validation","validationCode":"function validMaxSpend(v) { return typeof v === 'number' && Number.isFinite(v) && v > 0; }\nif (breakers.retry && !validMaxSpend(breakers.retry.maxSpend)) throw new Error('cave_breaker_retry_spend_invalid: need finite maxSpend > 0');","typeGuard":"const isPositiveFinite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v > 0;","tryCatchPattern":"try { normalizeRunBreakers(cfg); } catch (e) {\n  if (e.message === 'cave_breaker_retry_spend_invalid') console.error('retry.maxSpend must be a finite number > 0, got', cfg.breakers?.retry?.maxSpend);\n  else throw e;\n}","preventionTips":["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."],"tags":["config","validation","retry","spend-limit"],"backgroundTag":"invalid-config-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}