{"record":{"id":"b3c61dc999c7288d","repo":"JuliusBrussee/caveman","slug":"cave-budget-initial-invalid","errorCode":null,"errorMessage":"cave_budget_initial_invalid","messagePattern":"cave_budget_initial_invalid","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/budget.ts","lineNumber":105,"sourceCode":" * self-contradicting budget is rejected before the first provider call rather\n * than silently degrading into no cap at all.\n */\nexport function normalizeRunBudget(budget: RunBudget): NormalizedBudget {\n  const usd = budget.maxUsd !== undefined;\n  const tokens = budget.maxTokens !== undefined;\n  if (usd === tokens) throw new Error(\"cave_budget_denomination_ambiguous\");\n  const denomination: BudgetDenomination = usd ? \"usd\" : \"tokens\";\n  const max = usd ? budget.maxUsd! : budget.maxTokens!;\n  if (!Number.isFinite(max) || max <= 0) throw new Error(\"cave_budget_max_invalid\");\n  if (denomination === \"tokens\" && !Number.isSafeInteger(max)) {\n    throw new Error(\"cave_budget_max_invalid\");\n  }\n  const wrongInitial = denomination === \"usd\" ? budget.initialTokens : budget.initialUsd;\n  if (wrongInitial !== undefined) throw new Error(\"cave_budget_denomination_ambiguous\");\n  const declaredInitial = denomination === \"usd\" ? budget.initialUsd : budget.initialTokens;\n  const initial = declaredInitial ?? max;\n  if (!Number.isFinite(initial) || initial <= 0 || initial > max) {\n    throw new Error(\"cave_budget_initial_invalid\");\n  }\n  if (denomination === \"tokens\" && !Number.isSafeInteger(initial)) {\n    throw new Error(\"cave_budget_initial_invalid\");\n  }\n  const outputFloorTokens = budget.outputFloorTokens ?? OUTPUT_CLAMP_FLOOR_TOKENS;\n  if (!Number.isSafeInteger(outputFloorTokens) || outputFloorTokens <= 0) {\n    throw new Error(\"cave_budget_output_floor_invalid\");\n  }\n  const onExhausted = budget.onExhausted ?? \"compact\";\n  if (onExhausted !== \"compact\" && onExhausted !== \"stop\") {\n    throw new Error(\"cave_budget_on_exhausted_invalid\");\n  }\n  return Object.freeze({\n    denomination,\n    max,\n    initial,\n    outputFloorTokens,\n    onExhausted,","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/budget.ts#L87-L123","documentation":"Thrown by normalizeRunBudget when the initial budget value (explicit initialUsd/initialTokens, or the cap itself when no initial is given) is non-finite, non-positive, or greater than the cap. The 'initial' field models spend already consumed before this run (a warm start); it must be a sensible fraction of the cap, not exceed it, and not be garbage.","triggerScenarios":"Passing initialUsd: -1 or initialTokens: 0; passing an initial greater than max (e.g. initialUsd: 10 with maxUsd: 5); passing NaN or Infinity as the initial; the initial defaulting to max after the max itself already failed validation upstream in the same call.","commonSituations":"Seeding a run with prior-session spend read from a telemetry store that returns NaN for missing entries; carrying an initial value forward from a previous larger budget while shrinking the cap; off-by-sign bugs when subtracting refunds from a running total to compute the initial.","solutions":["Clamp and validate the initial before constructing the budget: it must satisfy 0 < initial <= max.","If the initial is derived from prior spend, default it to the max when the prior-spend source is unavailable instead of passing NaN.","When shrinking a cap, re-check any persisted initial value against the new max and reset it if it now exceeds the cap."],"exampleFix":"// before\nconst budget = { maxUsd: 5, initialUsd: priorRun?.spent }; // NaN when priorRun is undefined\n\n// after\nconst initial = priorRun !== undefined && Number.isFinite(priorRun.spent) && priorRun.spent > 0\n  ? Math.min(priorRun.spent, 5)\n  : undefined;\nconst budget = { maxUsd: 5, ...(initial !== undefined ? { initialUsd: initial } : {}) };","handlingStrategy":"validation","validationCode":"function normalizeInitial(initial: number | undefined, max: number): number | undefined {\n  if (initial === undefined) return undefined;\n  if (!Number.isFinite(initial) || initial <= 0 || initial > max) return undefined; // fall back to default\n  return initial;\n}","typeGuard":"function isValidInitial(v: unknown, max: number): v is number {\n  return typeof v === \"number\" && Number.isFinite(v) && v > 0 && v <= max;\n}","tryCatchPattern":null,"preventionTips":["Validate prior-spend records (Number.isFinite) before seeding a run with them.","When shrinking a cap, re-check any carried-over initial against the new max.","Prefer omitting initial over passing edge values; the default (initial = max) is always valid."],"tags":["budget","validation","initial-spend","numbers"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}