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
- Always pass a descriptive non-empty reason, e.g. release(1000, "model call 3: gpt-class provider").
- If the reason is assembled from variables, default the pieces: `call-${call.label ?? "unlabeled"}`.
- 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
- Make reason a required string parameter in your own wrapper signature.
- Build reasons from variables with defaulted pieces so they can never be empty.
- Write reasons that will be useful in an audit later: what the tranche funds and why.
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
- cave_budget_release_invalid
- cave_budget_denomination_ambiguous
- cave_budget_max_invalid
- cave_budget_initial_invalid
- cave_budget_output_floor_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/aa95c42236ca0657.
Report an issue: GitHub.