JuliusBrussee/caveman · error · Error
cave_budget_release_invalid
Error message
cave_budget_release_invalid
What it means
Thrown by BudgetMeter.release when the tranche amount is not a finite number or is not positive. Releasing a tranche is pre-flight funding at a developer-controlled checkpoint; a zero, negative, NaN, or Infinity amount is a programming error in the caller's funding logic, not a runtime condition to absorb.
Source
Thrown at packages/agent/src/budget.ts:325
/** Budget available to a new reservation right now. */
remaining(): number {
return Math.max(0, this.releasedAmount - this.settledAmount - this.reservedAmount);
}
/** Budget that could still be released without breaching `max`. */
releasable(): number {
return Math.max(0, this.max - this.releasedAmount);
}
/**
* Release a further tranche. Throws when it would breach `max`: this is
* pre-flight validation at a developer-controlled checkpoint, so the caller
* asked for something the contract cannot grant.
*/
release(amount: number, reason: string): BudgetTranche {
if (!Number.isFinite(amount) || amount <= 0) {
throw new Error("cave_budget_release_invalid");
}
if (this.denomination === "tokens" && !Number.isSafeInteger(amount)) {
throw new Error("cave_budget_release_invalid");
}
if (typeof reason !== "string" || reason.trim() === "") {
throw new Error("cave_budget_release_reason_required");
}
if (this.revokedFlag) throw new Error("cave_budget_revoked");
// A breached ledger is dead. Releasing into it would record a tranche and
// raise an escalation for money that can never be spent, and would read on
// the receipt as a run that was still being funded after it went past cap.
if (this.breachedFlag) throw new Error("cave_budget_cap_breached");
if (amount > this.releasable()) throw new Error("cave_budget_release_exceeds_max");
this.releasedAmount += amount;
const tranche: BudgetTranche = Object.freeze({
amount,
reason,
atCall: this.callIndex,View on GitHub (pinned to 27d5a3981a)
Solutions
- Guard the call: if (!(amount > 0) || !Number.isFinite(amount)) skip or log instead of calling release.
- Fix the tranche sizing arithmetic — especially divisors — so it cannot produce NaN or Infinity.
- Represent 'release nothing' by not calling release at all, never by passing 0.
Example fix
// before
meter.release(remaining / callCount, `tranche ${i}`); // callCount can be 0 -> Infinity
// after
if (callCount > 0 && Number.isFinite(remaining) && remaining > 0) {
meter.release(remaining / callCount, `tranche ${i}`);
} Defensive patterns
Strategy: validation
Validate before calling
function releaseTranche(meter: BudgetMeter, amount: number, reason: string): boolean {
if (!Number.isFinite(amount) || amount <= 0) return false; // skip invalid tranche
meter.release(amount, reason);
return true;
} Type guard
function isReleasableAmount(v: unknown): v is number {
return typeof v === "number" && Number.isFinite(v) && v > 0;
} Prevention
- Never call release(0, ...) to no-op; skip the call instead.
- Guard tranche-sizing divisions against zero divisors and NaN inputs.
- Centralize release calls in one funding helper so validation lives in one place.
When it happens
Trigger: Calling meter.release(0, ...) or release(-50, ...); passing an amount computed from a NaN source (failed parse, missing field); passing Infinity from a division by zero when computing tranche size.
Common situations: Sizing tranches as remaining/ncalls where ncall can be zero; passing a spend estimate whose input field was absent from the provider response; a caller trying to 'no-op' a release by passing zero instead of skipping the call.
Related errors
- cave_budget_max_invalid
- cave_budget_initial_invalid
- cave_budget_release_reason_required
- cave_budget_denomination_ambiguous
- cave_budget_output_floor_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/8d4b6aa05e6df2b2.
Report an issue: GitHub.