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

  1. Create a fresh controller with createBudgetController() for each run instead of reusing one
  2. Call unbindBudgetController(controller) when the run ends before any rebind
  3. Verify run teardown always executes (finally/afterEach) so bindings are released
  4. 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

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


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)