JuliusBrussee/caveman · error
cave_budget_revoked
cave_budget_revoked
Error message
cave_budget_revoked
What it means
`BudgetMeter.release` throws `cave_budget_revoked` when the ledger's `revokedFlag` is set. A revoked budget is dead by contract: a parent run that is revoked (e.g. aborted or failed above) revokes every wallet under it, and releasing a new tranche into a dead ledger would book money that can never be spent. The check runs before any tranche is recorded, so a revoked meter rejects all further releases unconditionally.
Solutions
- Check `meter.revoked` before releasing: `if (!meter.revoked) meter.release(amount, reason);`
- If this is a subagent wallet, stop releasing once the parent run is revoked — the whole tree is dead.
- Rework the controller so release attempts stop after run termination (abort/deadline/stop handlers) instead of continuing on a stale meter.
- If revocation was unintentional, restart the run with a fresh budget rather than trying to un-revoke the meter.
Example fix
// before
controller.onCheckpoint(() => meter.release(next, "top-up"));
// after
controller.onCheckpoint(() => {
if (meter.revoked) return;
meter.release(next, "top-up");
}); Defensive patterns
Strategy: validation
Validate before calling
if (!meter.revoked) {
meter.release(amount, reason);
} Type guard
const releasableMeter = (m: BudgetMeter): boolean => !m.revoked && !m.capBreached;
Try / catch
try {
meter.release(amount, reason);
} catch (e) {
if (e instanceof Error && e.message === "cave_budget_revoked") {
// run terminated; stop releasing and finalize
return null;
}
throw e;
} Prevention
- Check meter.revoked before every release, especially inside controllers and callbacks.
- Stop all budget-controller work once the run aborts, hits its deadline, or stops.
- Remember a revoked parent revokes all subagent wallets — never top up child wallets after a root failure.
- Avoid caching BudgetMeter references across run restarts; obtain a fresh meter per run.
When it happens
Trigger: Calling `release()` on a `BudgetMeter` after `revoke()` was called; releasing on a subagent wallet whose parent run was revoked (a revoked parent revokes every wallet beneath it); resuming a checkpoint/controller after the run already terminated; racing an abort — the run is revoked between the controller's decision and the `release` call.
Common situations: A budget controller that keeps releasing tranches from a callback after the run was aborted or hit its deadline; a nested agent workflow where a failure at the root revokes wallets but a per-child checkpoint loop still tries to top them up; retry logic that holds a stale meter reference across run restarts.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- cave_budget_cap_breached
- cave_budget_controller_in_use
- cave_budget_controller_unbound
- cave_budget_reservation_double_settle
- A dynamic Strands scope requires the native agent…
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/2a9c834e64841bc9.
Report an issue: GitHub.
Appendix: 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 3ee70a1026)