{"record":{"id":"f9f75912fdf40ec7","repo":"JuliusBrussee/caveman","slug":"cave-breaker-retry-backoff-invalid","errorCode":null,"errorMessage":"cave_breaker_retry_backoff_invalid","messagePattern":"cave_breaker_retry_backoff_invalid","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/breakers.ts","lineNumber":97,"sourceCode":"    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\";\n  readonly tool: string | undefined;\n  /** Repeats for a loop, identical turns for no-progress, blocked calls for fan-out, attempt number for a retry. */\n  readonly count: number;\n  /** The call hash for a loop break, so the offending window is identifiable. */","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/breakers.ts#L79-L115","documentation":"The retry backoff (`breakers.retry.backoffMs`, default 250ms — first backoff, doubled each attempt, deliberately no jitter) must be a safe integer ≥ 0. `normalizeRunBreakers` throws `cave_breaker_retry_backoff_invalid` for fractional milliseconds, negatives, `NaN`, `Infinity`, or unsafe-large integers. Zero is allowed and means the first retry is immediate.","triggerScenarios":"Setting `backoffMs: 250.5` (fraction), `backoffMs: -1`, or `backoffMs: Infinity`; deriving backoff from a division (`1000 / 3`) without rounding; parsing from a string config.","commonSituations":"Computing backoff from environment-scaled formulas; passing a float-typed config field through JSON; assuming any number works because the default is optional.","solutions":["Round to a whole number: `backoffMs: Math.round(1000 / 3)` → `333`.","Use 0 only if you intentionally want no initial delay.","Omit `backoffMs` entirely to accept the 250ms default."],"exampleFix":"// before\nbreakers: { retry: { maxSpend: 1, backoffMs: 1000 / 3 } } // 333.333...\n\n// after\nbreakers: { retry: { maxSpend: 1, backoffMs: Math.round(1000 / 3) } } // 333","handlingStrategy":"validation","validationCode":"const backoffMs = options.breakers?.retry?.backoffMs ?? 250; // default\nif (!Number.isSafeInteger(backoffMs) || backoffMs < 0) {\n  throw new Error(\"breakers.retry.backoffMs must be a safe integer >= 0\");\n}","typeGuard":"function isValidBackoffMs(v: unknown): v is number {\n  return Number.isSafeInteger(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_backoff_invalid\") {\n    // round the computed backoff, or omit the field to take the 250ms default\n  }\n}","preventionTips":["Round computed backoff values with Math.round before passing them.","Omit backoffMs to accept the deterministic 250ms default.","Remember 0 is valid (immediate first retry); fractional and negative values are not."],"tags":["breakers","retry","configuration","validation"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}