{"record":{"id":"7b761f23186ab39e","repo":"JuliusBrussee/caveman","slug":"cave-budget-cap-breached","errorCode":"cave_budget_cap_breached","errorMessage":"cave_budget_cap_breached","messagePattern":"cave_budget_cap_breached","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/budget.ts","lineNumber":337,"sourceCode":"   * Release a further tranche. Throws when it would breach `max`: this is\n   * 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++;","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/budget.ts#L319-L355","documentation":"`BudgetMeter.release` throws `cave_budget_cap_breached` when the ledger's `breachedFlag` is set — i.e. measured spend has already crossed `max`. A breached ledger is dead and terminal: releasing into it would record a tranche and raise an escalation for money that can never be spent, and would make the receipt read as if the run was still being funded after it went past cap. This check fires before the `releasable()` comparison, so it takes precedence over `cave_budget_release_exceeds_max`.","triggerScenarios":"Calling `release()` on a meter whose settled spend already exceeded `max` (cap breach is flagged when `settledAmount > max`); releasing into a parent ledger after a settled reservation came in above the bound; a subagent wallet breach rolling up — the flag rolls up from any wallet that breached beneath the run; continuing to top up a staged budget after `capBreached`/`overspent > 0` appeared on the receipt.","commonSituations":"A provider reported usage larger than the worst-case bound (input bigger than the byte-derived ceiling), flagging a breach; a controller loop that keeps releasing tranches each checkpoint without ever inspecting `meter.capBreached`; token-denominated runs where a call consumed far more than the output allowance.","solutions":["Check `meter.capBreached` (or receipt `capBreached`/`overspent`) before releasing: `if (!meter.capBreached) meter.release(amount, reason);`","Treat breach as terminal: stop the controller loop when a release throws `cave_budget_cap_breached` and end the run with the signed `overspent` figure.","Investigate why the ledger breached — a call whose real cost could not be bounded by the reserve — and adjust `max` or reservation sizing on the next run.","If breach came from a subagent wallet settling its real spend into the parent, re-carve smaller wallets so parent settlements stay under `max`."],"exampleFix":"// before\nfor (const stage of stages) meter.release(stage.cost, stage.name);\n\n// after\nfor (const stage of stages) {\n  if (meter.capBreached) break;\n  meter.release(stage.cost, stage.name);\n}","handlingStrategy":"validation","validationCode":"if (!meter.capBreached && meter.releasable() >= amount) {\n  meter.release(amount, reason);\n}","typeGuard":"const fundable = (m: BudgetMeter, amount: number): boolean =>\n  !m.capBreached && !m.revoked && amount <= m.releasable();","tryCatchPattern":"try {\n  meter.release(amount, reason);\n} catch (e) {\n  if (e instanceof Error && e.message === \"cave_budget_cap_breached\") {\n    // ledger is terminal; read meter.overspent and stop the run\n    finalizeWithOverspend(meter.overspent);\n    return null;\n  }\n  throw e;\n}","preventionTips":["Inspect receipt.capBreached / meter.capBreached after each settle before deciding to release more.","Treat breach as terminal — never attempt releases or escalations after it.","Size reservations and wallets so real provider usage stays within the worst-case bound.","Break controller loops when a release is refused; do not keep polling with larger amounts."],"tags":["budget","cap","lifecycle"],"backgroundTag":"invalid-state-transition","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"}