JuliusBrussee/caveman · error · Error
cave_budget_revoked
Error message
cave_budget_revoked
What it means
Thrown by BudgetMeter.release when the ledger has been revoked. Revocation permanently kills the budget — typically an escalation or abort path — and any later attempt to release funds into it is a logic error: money cannot be granted by a budget that no longer exists as an authority.
Source
Thrown at packages/agent/src/budget.ts:333
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;
}
/**
* 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.View on GitHub (pinned to 27d5a3981a)
Solutions
- Check the revocation state before releasing if the API exposes it, or structure the loop so revocation short-circuits before the release step.
- Wire AbortSignal/cancellation checks immediately before each release call.
- Wrap release in try-catch for cave_budget_revoked and treat it as a normal termination signal, ending the funding loop gracefully.
Example fix
// before
while (running) {
meter.release(tranche, `call ${i}`); // throws after revoke
await doCall(i++);
}
// after
while (running && !aborted) {
try {
meter.release(tranche, `call ${i}`);
} catch (e) {
if (e instanceof Error && e.message === "cave_budget_revoked") break;
throw e;
}
await doCall(i++);
} Defensive patterns
Strategy: try-catch
Try / catch
try {
meter.release(amount, reason);
} catch (e) {
if (e instanceof Error && e.message === "cave_budget_revoked") {
return; // budget revoked mid-run: stop funding, treat as normal termination
}
throw e;
} Prevention
- Check cancellation/abort state immediately before each release in the driver loop.
- Treat cave_budget_revoked as a control-flow signal, not an exceptional failure — end the loop gracefully on it.
- Ensure revocation paths also stop the code that would otherwise keep funding calls.
When it happens
Trigger: Calling release() after an abort/escalation handler called revoke(); a cleanup or retry path that races with revocation and still tries to fund the next call; holding a stale meter reference after the owning run was cancelled.
Common situations: Cancellation tokens or AbortSignals firing mid-loop while the loop body unconditionally releases the next tranche; error-recovery code that re-funds after the supervisor already revoked the budget for the run; async callbacks resolving after a timeout revocation.
Related errors
- cave_budget_cap_breached
- cave_budget_release_invalid
- cave_budget_release_reason_required
- cave_budget_controller_unbound
- cave_budget_controller_in_use
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/2a9c834e64841bc9.
Report an issue: GitHub.