{"record":{"id":"f9f75912fdf40ec7","repo":"JuliusBrussee/caveman","slug":"cave-breaker-retry-backoff-invalid","errorCode":"cave_breaker_retry_backoff_invalid","errorMessage":"cave_breaker_retry_backoff_invalid","messagePattern":"cave_breaker_retry_backoff_invalid","errorType":"error_code","errorClass":null,"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/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/breakers.ts#L79-L115","documentation":"normalizeRunBreakers validates retry.backoffMs, defaulting to DEFAULT_RETRY_BACKOFF_MS when omitted. The value must be a non-negative safe integer of milliseconds; backoff drives the deterministic exponential delay between retries, so fractional, negative, non-integer, or out-of-safe-integer values are rejected with this sentinel error.","triggerScenarios":"Passing `breakers.retry.backoffMs` as a negative number, a float (e.g. 1.5), a non-numeric string coerced to NaN, or a value beyond Number.MAX_SAFE_INTEGER.","commonSituations":"Specifying backoff in seconds instead of milliseconds (0.5); computing backoff with arithmetic that yields NaN from a bad config string; copy-pasting durations with units ('500ms' as a string).","solutions":["Set backoffMs to a whole, non-negative millisecond count (e.g. 500).","Omit backoffMs entirely to use DEFAULT_RETRY_BACKOFF_MS.","Convert seconds/config strings to integer ms with Number.parseInt before passing them in."],"exampleFix":"// before\nretry: { maxSpend: 0.5, backoffMs: 0.5 }\n// after\nretry: { maxSpend: 0.5, backoffMs: 500 }","handlingStrategy":"validation","validationCode":"function validBackoffMs(v) { return v === undefined || (Number.isSafeInteger(v) && v >= 0); }\nif (breakers.retry && !validBackoffMs(breakers.retry.backoffMs)) throw new Error('cave_breaker_retry_backoff_invalid: need safe integer ms >= 0');","typeGuard":"const isMsDuration = (v: unknown): v is number => Number.isSafeInteger(v) && v >= 0;","tryCatchPattern":"try { normalizeRunBreakers(cfg); } catch (e) {\n  if (e.message === 'cave_breaker_retry_backoff_invalid') { cfg.breakers.retry.backoffMs = Math.max(0, Math.round(Number(cfg.breakers.retry.backoffMs))); }\n  else throw e;\n}","preventionTips":["Express durations in whole milliseconds; convert seconds with Math.round(s * 1000).","Coerce config strings to integers before constructing breakers.","Omit backoffMs to accept the library default."],"tags":["config","validation","retry","backoff"],"backgroundTag":"invalid-duration-format","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"}