JuliusBrussee/caveman · error · Error

cave_budget_release_exceeds_max

cave_budget_release_exceeds_max

Error message

cave_budget_release_exceeds_max

What it means

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.

Source

Thrown at packages/agent/src/budget.ts:338

   * 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,
    });
    this.trancheLog.push(tranche);
    return tranche;
  }

  /**
   * Hold `amount` against the ledger. Returns `undefined` when it does not fit,
   * which is the caller's signal to clamp, compact, or stop — never to proceed.
   */
  reserve(amount: number, outputTokenCap: number): BudgetReservation | undefined {
    const held = this.hold(amount, outputTokenCap);
    if (held !== undefined) this.callIndex++;
    return held;

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Clamp the top-up: Math.min(wanted, releasable) using context.releasable from BudgetExhaustionContext or controller.max - controller.released
  2. If the plan genuinely needs more, set a higher maxUsd/maxTokens up front
  3. Return "stop" from the handler when releasable is 0

Example fix

// before
return { release: 1, reason: "top-up" };

// after
const wanted = Math.min(1, ctx.releasable);
return wanted > 0 ? { release: wanted, reason: "top-up" } : "stop";
Defensive patterns

Strategy: validation

Validate before calling

// At a checkpoint, before releasing:
const releasable = controller.max - controller.released;
const topUp = Math.min(wanted, releasable);
if (topUp > 0) controller.releaseBudget(topUp, "phase 2");

// Inside onBudgetExhausted, prefer the context value:
async function onBudgetExhausted(ctx: BudgetExhaustionContext) {
  const release = Math.min(1, ctx.releasable);
  return release > 0 ? { release, reason: "top-up" } : "stop";
}

Try / catch

try {
  controller.releaseBudget(amount, reason);
} catch (error) {
  if (error instanceof Error && error.message === "cave_budget_release_exceeds_max") {
    // amount exceeded max - released; clamp to the true headroom or raise max on a new run
  } else throw error;
}

Prevention

When it happens

Trigger: 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.

Common situations: Fixed top-up amounts that ignore how much was already released; retrying the same release after partial success; multiple exhaustion escalations within one run.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/e61b63e941b67b5f. Report an issue: GitHub.