JuliusBrussee/caveman · error · Error

cave_budget_release_reason_required

Error message

cave_budget_release_reason_required

What it means

Thrown by BudgetMeter.release when the reason argument is not a non-empty string (after trimming). Every tranche is audited — amount, reason, and call index are frozen into the tranche log — so an anonymous funding event cannot be recorded; the reason is mandatory, not decorative.

Source

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

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

  /**

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Always pass a descriptive non-empty reason, e.g. release(1000, "model call 3: gpt-class provider").
  2. If the reason is assembled from variables, default the pieces: `call-${call.label ?? "unlabeled"}`.
  3. Type your wrapper so reason is a required string parameter, letting the compiler catch omissions.

Example fix

// before
meter.release(1000, "");

// after
meter.release(1000, `model call ${index}: ${toolName}`);
Defensive patterns

Strategy: validation

Validate before calling

function reasonFor(call: { name?: string }, index: number): string {
  return `call ${index}: ${call.name ?? "unlabeled"}`;
}

Type guard

function isNonEmptyReason(v: unknown): v is string {
  return typeof v === "string" && v.trim() !== "";
}

Prevention

When it happens

Trigger: Calling release(amount, ""); passing a whitespace-only string; omitting the argument entirely (undefined); passing a non-string such as a number or an object.

Common situations: Refactoring release call sites and dropping the second argument; generating reasons from variables that can be undefined at edge cases (e.g. `call-${call.name}` where name is missing); copy-pasted stub code with a placeholder empty string.

Related errors


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