JuliusBrussee/caveman · error
cave_budget_controller_in_use
cave_budget_controller_in_use
Error message
cave_budget_controller_in_use
What it means
bindBudgetController enforces a one-to-one controller-to-meter mapping held in a WeakMap. This error means you attempted to bind a controller that already has a live meter binding. The library refuses to silently rebind because the existing meter may own budget already spent.
Solutions
- Create a fresh controller with createBudgetController() for each run instead of reusing one
- Call unbindBudgetController(controller) when the run ends before any rebind
- Verify run teardown always executes (finally/afterEach) so bindings are released
- Check for duplicate initialization calls on retry or hot-reload paths
Example fix
// before bindBudgetController(controller, meter1); bindBudgetController(controller, meter2); // throws // after bindBudgetController(controller, meter1); unbindBudgetController(controller); bindBudgetController(controller, meter2);
Defensive patterns
Strategy: try-catch
Validate before calling
// track ownership yourself before rebinding if (controllerOwner.has(controller)) unbindBudgetController(controller);
Try / catch
try {
bindBudgetController(controller, meter);
} catch (e) {
if (e instanceof Error && e.message === "cave_budget_controller_in_use") {
unbindBudgetController(controller);
bindBudgetController(controller, meter);
} else throw e;
} Prevention
- Use one controller per run; create a fresh one each time
- Always unbind in a finally block when the run ends
- Avoid controller singletons shared across concurrent runs
When it happens
Trigger: Calling bindBudgetController(controller, meter) a second time on the same controller without an intervening unbindBudgetController — e.g. starting a second run while reusing one controller, or double-invoking run setup on retry paths.
Common situations: Concurrent runs sharing a module-level controller singleton; a retry wrapper re-running initialization after a partial failure without unbinding; tests leaking a bound controller across cases.
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_unbound
- cave_budget_revoked
- A dynamic Strands scope requires the native agent…
- budget is below CCR storage minimum
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/b33fd52e991337b5.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/budget.ts:844
}
private meter(): BudgetMeter {
const bound = controllerMeters.get(this);
if (bound === undefined) throw new Error("cave_budget_controller_unbound");
return bound;
}
}
export function createBudgetController(): BudgetController {
return new BudgetController();
}
/** Package-internal. Binds a controller to the run that owns its budget. */
export function bindBudgetController(
controller: BudgetController,
meter: BudgetMeter,
): void {
if (controllerMeters.has(controller)) throw new Error("cave_budget_controller_in_use");
controllerMeters.set(controller, meter);
}
/** Package-internal. Releases the binding when the run ends. */
export function unbindBudgetController(controller: BudgetController): void {
controllerMeters.delete(controller);
}
/** What the meter says the runtime may do with the next provider call. */
export type CallPlan =
| { readonly action: "proceed"; readonly reservation: BudgetReservation | undefined; readonly outputTokenCap: number }
| { readonly action: "compact" }
| { readonly action: "stop"; readonly reason: RunStopReason };
/**
* The exhaustion ladder's arithmetic: full allowance, else a clamped allowance
* down to the floor, else exhaustion. `compactionAvailable` lets the caller
* insert the compaction rung between the full and clamped rungs.View on GitHub (pinned to 3ee70a1026)