{"record":{"id":"b15226b1c10db315","repo":"JuliusBrussee/caveman","slug":"cave-budget-denomination-ambiguous","errorCode":"cave_budget_denomination_ambiguous","errorMessage":"cave_budget_denomination_ambiguous","messagePattern":"cave_budget_denomination_ambiguous","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/budget.ts","lineNumber":93,"sourceCode":"\nexport interface NormalizedBudget {\n  readonly denomination: BudgetDenomination;\n  readonly max: number;\n  readonly initial: number;\n  readonly outputFloorTokens: number;\n  readonly onExhausted: \"compact\" | \"stop\";\n  readonly compaction: NormalizedCompaction;\n}\n\n/**\n * Validate a caller-supplied budget. Fails closed: an ambiguous, unbounded, or\n * 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) {","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/budget.ts#L75-L111","documentation":"normalizeRunBudget requires a budget to specify exactly one denomination: exactly one of maxUsd or maxTokens must be defined. If both are defined, or neither is, the budget is self-contradictory or vacuous, so it throws cave_budget_denomination_ambiguous before any provider call rather than silently running with no cap.","triggerScenarios":"Passing { maxUsd: 5, maxTokens: 10000 } (both set) or {} / { initialUsd: 5 } (neither max set) to normalizeRunBudget / budgetMeter.","commonSituations":"Merging two config objects where one sets maxUsd and the other maxTokens; a config file carrying stale maxTokens after maxUsd was added; constructing the budget programmatically with optional fields that all end up set or unset.","solutions":["Set exactly one of maxUsd or maxTokens; delete the other field.","If you must express both, convert one to the other (e.g. estimate tokens from USD) before building the budget.","Deep-check merged config objects for leftover keys before normalizing."],"exampleFix":"// before\nbudget({ maxUsd: 5, maxTokens: 10000 })\n// after\nbudget({ maxUsd: 5 })","handlingStrategy":"validation","validationCode":"function validBudgetDenomination(b) {\n  const usd = b.maxUsd !== undefined, tokens = b.maxTokens !== undefined;\n  return usd !== tokens; // exactly one must be set\n}\nif (!validBudgetDenomination(budget)) throw new Error('set exactly one of maxUsd or maxTokens');","typeGuard":null,"tryCatchPattern":"try { normalizeRunBudget(b); } catch (e) {\n  if (e.message === 'cave_budget_denomination_ambiguous') { b = pickSingleDenomination(b); }\n  else throw e;\n}","preventionTips":["Express the run cap in exactly one denomination; strip the other key when merging configs.","Centralize budget construction in one helper instead of ad-hoc object merges.","When migrating between USD and token budgets, delete stale keys."],"tags":["config","validation","budget"],"backgroundTag":"mutually-exclusive-options","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"}