JuliusBrussee/caveman · error · Error

cave_budget_cap_breached

Error message

cave_budget_cap_breached

What it means

Thrown by BudgetMeter.release when the ledger has already breached its cap. A breached ledger is deliberately dead: 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 going past cap. Any release after breach is a caller logic error.

Source

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

   * 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,
    });
    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++;

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Check the meter's breach state (if exposed) before each release, and stop or compact instead of funding further calls.
  2. Order the loop as: settle previous call, inspect state, only then release for the next call.
  3. Catch this error at the top-level run driver and translate it into the run's normal budget-exhausted termination path rather than a crash.

Example fix

// before
for (const call of calls) {
  meter.release(1000, call.reason);
  await run(call);
}

// after
for (const call of calls) {
  try {
    meter.release(1000, call.reason);
  } catch (e) {
    if (e instanceof Error && e.message.startsWith("cave_budget_")) break; // breached/revoked: stop funding
    throw e;
  }
  await run(call);
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  meter.release(amount, reason);
} catch (e) {
  if (e instanceof Error && e.message === "cave_budget_cap_breached") {
    return { stop: true, cause: "budget-breach" }; // translate into run termination
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling release() after a settle() pushed settledAmount past max (setting breachedFlag); a loop that funds the next call before checking the breach state from the previous call's settlement; concurrent settles breaching the cap while another path releases in parallel.

Common situations: A driver loop structured as release-then-call without consulting the breach flag between iterations; retry logic that re-funds a retry attempt after the underlying cost already exceeded the budget; long-running agents where an expensive call settles above cap and the scheduler immediately funds the next one.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/7b761f23186ab39e. Report an issue: GitHub.