{"record":{"id":"1c94bb9c1dc6f5fe","repo":"JuliusBrussee/caveman","slug":"cave-breaker-retry-requires-budget","errorCode":null,"errorMessage":"cave_breaker_retry_requires_budget","messagePattern":"cave_breaker_retry_requires_budget","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/breakers.ts","lineNumber":89,"sourceCode":"  hasBudget: boolean,\n): NormalizedBreakers {\n  const repeatedToolCalls = breakers.repeatedToolCalls ?? DEFAULT_REPEATED_TOOL_CALLS;\n  const repeatedToolCallWindowTurns = breakers.repeatedToolCallWindowTurns ??\n    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}","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/breakers.ts#L71-L107","documentation":"A retry policy (`RunOptions.breakers.retry`) declares a worst-case spend in the run budget's denomination, and every retry takes a real hold from the run's BudgetMeter. Without a budget there is no denomination to reserve against, so `normalizeRunBreakers` throws `cave_breaker_retry_requires_budget` when `breakers.retry` is set and the run has no budget. This is a deliberate fail-closed design: retries without a metered wallet could spend without bound.","triggerScenarios":"Calling `run(agent, { breakers: { retry: { maxSpend: 1 } } })` without also setting `RunOptions.budget` (`{ maxUsd: ... }` or `{ maxTokens: ... }`); adding retry config to an existing run call that never had a budget; combining `retry` with `maxCostUsd` only (the older cap is not a budget).","commonSituations":"Copying a breakers snippet from docs into a run that uses no budget; migrating from `maxCostUsd` to breakers and assuming the cap satisfies the requirement.","solutions":["Add a budget alongside retry: `run(agent, { budget: { maxUsd: 5 }, breakers: { retry: { maxSpend: 1 } } })`.","Or drop `breakers.retry` if you don't need cost-bounded automatic retries."],"exampleFix":"// before\nrun(agent, { breakers: { retry: { maxSpend: 1 } } }); // no budget\n\n// after\nrun(agent, {\n  budget: { maxUsd: 5 },\n  breakers: { retry: { maxSpend: 1 } },\n});","handlingStrategy":"validation","validationCode":"if (options.breakers?.retry !== undefined && options.budget === undefined) {\n  throw new Error(\"breakers.retry requires RunOptions.budget — add budget: { maxUsd } or { maxTokens }\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  run(agent, options);\n} catch (err) {\n  if (err instanceof Error && err.message === \"cave_breaker_retry_requires_budget\") {\n    // add a budget or remove retry — a config fix, not a transient failure\n  }\n}","preventionTips":["Always pair breakers.retry with an explicit RunOptions.budget.","Note that maxCostUsd is not a budget; migrate to budget.maxUsd when adding retry.","Centralize run-option construction so the budget/retry pairing is checked in one place."],"tags":["breakers","budget","configuration","retry"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}