{"record":{"id":"98524ce48d12436a","repo":"JuliusBrussee/caveman","slug":"cave-breaker-threshold-invalid","errorCode":"cave_breaker_threshold_invalid","errorMessage":"cave_breaker_threshold_invalid","messagePattern":"cave_breaker_threshold_invalid","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/breakers.ts","lineNumber":85,"sourceCode":"const DEFAULT_RETRY_BACKOFF_MS = 250;\n\nexport function normalizeRunBreakers(\n  breakers: RunBreakers,\n  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,","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/breakers.ts#L67-L103","documentation":"`normalizeRunBreakers` validates the four breaker thresholds — `repeatedToolCalls`, `repeatedToolCallWindowTurns`, `noProgressTurns`, `maxToolCallsPerTurn` — after applying defaults. Each must be a safe integer greater than zero; anything else (zero, negative, fractional, `Infinity`, `NaN`, or a value beyond `Number.MAX_SAFE_INTEGER`) throws `cave_breaker_threshold_invalid`. Defaults are valid, so this error always traces to explicitly supplied `RunOptions.breakers` values.","triggerScenarios":"Setting `breakers: { repeatedToolCalls: 0 }` (perhaps intending \"disabled\"), `{ noProgressTurns: 2.5 }`, `{ maxToolCallsPerTurn: Infinity }`, or reading thresholds from env/config that yield strings or NaN.","commonSituations":"Trying to disable a breaker with 0 (breakers are opt-in — omit the field instead); config parsed from CLI flags as strings; computing thresholds dynamically (e.g. `turns * 1.5`) producing fractions; `parseInt(undefined)` yielding NaN.","solutions":["Set every supplied threshold to a whole number ≥ 1 (e.g. `repeatedToolCalls: 3`).","To disable a breaker, omit its field from `RunOptions.breakers` entirely rather than passing 0.","Coerce and check config values before passing them: `Number.isSafeInteger(v) && v > 0`."],"exampleFix":"// before\nrun(agent, { breakers: { repeatedToolCalls: 0, noProgressTurns: 2.5 } });\n\n// after\nrun(agent, { breakers: { repeatedToolCalls: 3, noProgressTurns: 3 } });\n// or omit a breaker to leave it at its default","handlingStrategy":"validation","validationCode":"function isValidBreakerThreshold(v: unknown): v is number {\n  return Number.isSafeInteger(v) && (v as number) > 0;\n}\nconst thresholds = [breakers.repeatedToolCalls, breakers.noProgressTurns].filter((v) => v !== undefined);\nif (!thresholds.every(isValidBreakerThreshold)) throw new Error(\"breaker thresholds must be positive integers\");","typeGuard":"function isPositiveSafeInteger(v: unknown): v is number {\n  return Number.isSafeInteger(v) && (v as number) > 0;\n}","tryCatchPattern":"try {\n  normalizeRunBreakers(breakers, hasBudget);\n} catch (err) {\n  if (err instanceof Error && err.message === \"cave_breaker_threshold_invalid\") {\n    // config error: fix the offending threshold; retrying unchanged is pointless\n  }\n}","preventionTips":["Omit a breaker field to use its default instead of passing 0.","Validate config-sourced numbers with Number.isSafeInteger before building RunOptions.","Type breaker config fields as `number` and lint against string env values."],"tags":["breakers","configuration","validation","typescript"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}