{"record":{"id":"f03f488a80f2401a","repo":"JuliusBrussee/caveman","slug":"cave-budget-conflicting-cap","errorCode":"cave_budget_conflicting_cap","errorMessage":"cave_budget_conflicting_cap","messagePattern":"cave_budget_conflicting_cap","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":1078,"sourceCode":"      (!Number.isSafeInteger(options.maxSubagentInvocations) || options.maxSubagentInvocations <= 0 ||\n        options.maxSubagentInvocations > ABSOLUTE_SUBAGENT_INVOCATION_LIMIT)) {\n    throw new Error(\"cave_subagent_invocation_limit_invalid\");\n  }\n  if (options.maxConcurrentSubagents !== undefined &&\n      (!Number.isSafeInteger(options.maxConcurrentSubagents) || options.maxConcurrentSubagents <= 0 ||\n        options.maxConcurrentSubagents > ABSOLUTE_SUBAGENT_INVOCATION_LIMIT)) {\n    throw new Error(\"cave_subagent_concurrency_limit_invalid\");\n  }\n  if (options.lockedBuild !== undefined && options.candidatePlan !== undefined) {\n    throw new Error(\"cave_execution_authorization_ambiguous\");\n  }\n  // Budget shape is settled before anything else happens: an ambiguous or\n  // unbounded budget must fail at run() start, not after the first dollar.\n  // maxCostUsd and budget are two different contracts for the same money —\n  // one terminates with an error, the other returns a planned partial result —\n  // so carrying both would leave the run's own stop semantics undecided.\n  if (options.budget !== undefined && options.maxCostUsd !== undefined) {\n    throw new Error(\"cave_budget_conflicting_cap\");\n  }\n  const budgetMeter = executionContext.budgetMeter ?? (options.budget === undefined\n    ? undefined\n    : new BudgetMeter(normalizeRunBudget(options.budget)));\n  if (options.deadlineMs !== undefined &&\n      (!Number.isSafeInteger(options.deadlineMs) || options.deadlineMs <= 0)) {\n    throw new Error(\"cave_run_deadline_invalid\");\n  }\n  if (options.maxSubagentDepth !== undefined &&\n      (!Number.isSafeInteger(options.maxSubagentDepth) || options.maxSubagentDepth <= 0 ||\n        options.maxSubagentDepth > ABSOLUTE_SUBAGENT_DEPTH_LIMIT)) {\n    throw new Error(\"cave_subagent_depth_limit_invalid\");\n  }\n  const deadlineAt = executionContext.deadlineAt ?? (options.deadlineMs === undefined\n    ? undefined\n    : performance.now() + options.deadlineMs);\n  // A controller with nothing to release would be a silent no-op at the\n  // checkpoint that expected it to matter.","sourceCodeStart":1060,"sourceCodeEnd":1096,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/runtime.ts#L1060-L1096","documentation":"Thrown at run start when both RunOptions.budget and RunOptions.maxCostUsd are set. They are two different contracts for the same money: exhausting maxCostUsd terminates with an error, while exhausting budget returns a planned partial result with a stopReason. Carrying both would leave the run's stop semantics undecided, so the conflict fails fast before the first provider call.","triggerScenarios":"run/stream called with { budget: { maxUsd: 5 }, maxCostUsd: 5 } or { budget: { maxTokens: 100000 }, maxCostUsd: 2 } in the same options object; merging two config objects where each contributed one of the caps.","commonSituations":"Layered defaults: a wrapper always sets maxCostUsd while the caller adds a budget; copy-paste migration from the legacy maxCostUsd to the newer budget without removing the old field; spread of a base options object into per-call options.","solutions":["Keep exactly one cap: prefer RunOptions.budget (reserve-and-clamp, graceful stop) and delete maxCostUsd","If you must keep the legacy error-terminating semantics, remove the budget field","When merging option layers, make the budget cap an explicit override instead of spreading both"],"exampleFix":"// before\nawait agent.run(input, { ...defaults, budget: { maxUsd: 5 } }); // defaults has maxCostUsd: 10\n\n// after\nconst { maxCostUsd: _omit, ...rest } = defaults;\nawait agent.run(input, { ...rest, budget: { maxUsd: 5 } });","handlingStrategy":"validation","validationCode":"function singleCap<T extends { budget?: unknown; maxCostUsd?: unknown }>(o: T): T {\n  if (o.budget !== undefined && o.maxCostUsd !== undefined) {\n    const { maxCostUsd: _drop, ...rest } = o;\n    return rest as T; // or throw, per your policy\n  }\n  return o;\n}","typeGuard":"const hasBothCaps = (o: { budget?: unknown; maxCostUsd?: unknown }): boolean =>\n  o.budget !== undefined && o.maxCostUsd !== undefined;","tryCatchPattern":null,"preventionTips":["Pick one budget contract project-wide (prefer RunOptions.budget)","When merging option layers, destructure out maxCostUsd before adding budget","Add a lint/grep check that flags maxCostUsd appearing next to budget"],"tags":["budget","conflicting-options","validation"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}