{"record":{"id":"b33fd52e991337b5","repo":"JuliusBrussee/caveman","slug":"cave-budget-controller-in-use","errorCode":null,"errorMessage":"cave_budget_controller_in_use","messagePattern":"cave_budget_controller_in_use","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/budget.ts","lineNumber":844,"sourceCode":"  }\n\n  private meter(): BudgetMeter {\n    const bound = controllerMeters.get(this);\n    if (bound === undefined) throw new Error(\"cave_budget_controller_unbound\");\n    return bound;\n  }\n}\n\nexport function createBudgetController(): BudgetController {\n  return new BudgetController();\n}\n\n/** Package-internal. Binds a controller to the run that owns its budget. */\nexport function bindBudgetController(\n  controller: BudgetController,\n  meter: BudgetMeter,\n): void {\n  if (controllerMeters.has(controller)) throw new Error(\"cave_budget_controller_in_use\");\n  controllerMeters.set(controller, meter);\n}\n\n/** Package-internal. Releases the binding when the run ends. */\nexport function unbindBudgetController(controller: BudgetController): void {\n  controllerMeters.delete(controller);\n}\n\n/** What the meter says the runtime may do with the next provider call. */\nexport type CallPlan =\n  | { readonly action: \"proceed\"; readonly reservation: BudgetReservation | undefined; readonly outputTokenCap: number }\n  | { readonly action: \"compact\" }\n  | { readonly action: \"stop\"; readonly reason: RunStopReason };\n\n/**\n * The exhaustion ladder's arithmetic: full allowance, else a clamped allowance\n * down to the floor, else exhaustion. `compactionAvailable` lets the caller\n * insert the compaction rung between the full and clamped rungs.","sourceCodeStart":826,"sourceCodeEnd":862,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/budget.ts#L826-L862","documentation":"Thrown by bindBudgetController when the controller is already bound to a meter. The binding map is keyed by controller instance and one controller can be bound to only one run's meter at a time; attempting a second bind (before unbindBudgetController ran) is rejected. This is package-internal API, so user code typically hits it only via a wrapper that mismanages controller lifecycles.","triggerScenarios":"Passing the same controller instance to two concurrent runs; starting a second run with a controller whose first run never unbound (crashed or leaked binding); a wrapper that binds on every retry without unbinding first.","commonSituations":"Pooling or caching controller instances across runs to save allocation; re-entering a run loop after a partial failure where cleanup (unbind) was skipped; tests that reuse module-level controllers across sequential runs without unbinding.","solutions":["Create a fresh controller per run with createBudgetController(); never share one across concurrent runs.","If reuse is required, call unbindBudgetController(controller) in a finally block when each run ends before binding again.","In wrappers, assert the controller is unbound before starting a run and fail loudly at the wrapper boundary."],"exampleFix":"// before\nconst shared = createBudgetController();\nawait Promise.all([runAgent({ budget: b1, controller: shared }), runAgent({ budget: b2, controller: shared })]); // second bind throws\n\n// after\nawait Promise.all([\n  runAgent({ budget: b1, controller: createBudgetController() }),\n  runAgent({ budget: b2, controller: createBudgetController() }),\n]);","handlingStrategy":"validation","validationCode":"// One controller per run; no shared instances.\nfunction freshRun(budget: RunBudget) {\n  return runAgent({ budget, controller: createBudgetController() });\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never pass the same controller instance to two runs, concurrent or overlapping.","If reuse is unavoidable, ensure unbindBudgetController runs in a finally block before rebinding.","Treat bind errors as lifecycle bugs in your wrapper: fail loudly at the wrapper boundary instead of retrying."],"tags":["budget","lifecycle","controller","concurrency","binding"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}