{"record":{"id":"e61b63e941b67b5f","repo":"JuliusBrussee/caveman","slug":"cave-budget-release-exceeds-max","errorCode":"cave_budget_release_exceeds_max","errorMessage":"cave_budget_release_exceeds_max","messagePattern":"cave_budget_release_exceeds_max","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/budget.ts","lineNumber":338,"sourceCode":"   * pre-flight validation at a developer-controlled checkpoint, so the caller\n   * asked for something the contract cannot grant.\n   */\n  release(amount: number, reason: string): BudgetTranche {\n    if (!Number.isFinite(amount) || amount <= 0) {\n      throw new Error(\"cave_budget_release_invalid\");\n    }\n    if (this.denomination === \"tokens\" && !Number.isSafeInteger(amount)) {\n      throw new Error(\"cave_budget_release_invalid\");\n    }\n    if (typeof reason !== \"string\" || reason.trim() === \"\") {\n      throw new Error(\"cave_budget_release_reason_required\");\n    }\n    if (this.revokedFlag) throw new Error(\"cave_budget_revoked\");\n    // A breached ledger is dead. Releasing into it would record a tranche and\n    // raise an escalation for money that can never be spent, and would read on\n    // the receipt as a run that was still being funded after it went past cap.\n    if (this.breachedFlag) throw new Error(\"cave_budget_cap_breached\");\n    if (amount > this.releasable()) throw new Error(\"cave_budget_release_exceeds_max\");\n    this.releasedAmount += amount;\n    const tranche: BudgetTranche = Object.freeze({\n      amount,\n      reason,\n      atCall: this.callIndex,\n    });\n    this.trancheLog.push(tranche);\n    return tranche;\n  }\n\n  /**\n   * Hold `amount` against the ledger. Returns `undefined` when it does not fit,\n   * which is the caller's signal to clamp, compact, or stop — never to proceed.\n   */\n  reserve(amount: number, outputTokenCap: number): BudgetReservation | undefined {\n    const held = this.hold(amount, outputTokenCap);\n    if (held !== undefined) this.callIndex++;\n    return held;","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/budget.ts#L320-L356","documentation":"Staged release can top up tranches only up to the run's hard max: an amount greater than releasable() (max − released) throws at the release site, both for BudgetController.releaseBudget and the exhaustion handler's { release, reason } answer. max is the contract; a checkpoint asking beyond it is a programming or policy error, not a run outcome.","triggerScenarios":"budget: { maxUsd: 5, initialUsd: 4 } then releaseBudget(2, …) when only 1 is releasable; an exhaustion handler returning a constant { release: 1 } on every escalation; several checkpoints each releasing max-sized tranches.","commonSituations":"Fixed top-up amounts that ignore how much was already released; retrying the same release after partial success; multiple exhaustion escalations within one run.","solutions":["Clamp the top-up: Math.min(wanted, releasable) using context.releasable from BudgetExhaustionContext or controller.max - controller.released","If the plan genuinely needs more, set a higher maxUsd/maxTokens up front","Return \"stop\" from the handler when releasable is 0"],"exampleFix":"// before\nreturn { release: 1, reason: \"top-up\" };\n\n// after\nconst wanted = Math.min(1, ctx.releasable);\nreturn wanted > 0 ? { release: wanted, reason: \"top-up\" } : \"stop\";","handlingStrategy":"validation","validationCode":"// At a checkpoint, before releasing:\nconst releasable = controller.max - controller.released;\nconst topUp = Math.min(wanted, releasable);\nif (topUp > 0) controller.releaseBudget(topUp, \"phase 2\");\n\n// Inside onBudgetExhausted, prefer the context value:\nasync function onBudgetExhausted(ctx: BudgetExhaustionContext) {\n  const release = Math.min(1, ctx.releasable);\n  return release > 0 ? { release, reason: \"top-up\" } : \"stop\";\n}","typeGuard":null,"tryCatchPattern":"try {\n  controller.releaseBudget(amount, reason);\n} catch (error) {\n  if (error instanceof Error && error.message === \"cave_budget_release_exceeds_max\") {\n    // amount exceeded max - released; clamp to the true headroom or raise max on a new run\n  } else throw error;\n}","preventionTips":["Always compute top-ups as Math.min(wanted, releasable) — never a constant","Size maxUsd/maxTokens for the whole plan up front; staged release only paces spending, it cannot extend the cap","Return \"stop\" when releasable is 0 rather than retrying the release"],"tags":["budget","staged-release","cap","validation"],"backgroundTag":"spend-limit-exceeded","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}