{"record":{"id":"62849c896fe2fc8b","repo":"JuliusBrussee/caveman","slug":"cave-budget-controller-unbound","errorCode":null,"errorMessage":"cave_budget_controller_unbound","messagePattern":"cave_budget_controller_unbound","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/budget.ts","lineNumber":830,"sourceCode":"  get released(): number {\n    return this.meter().released;\n  }\n\n  get max(): number {\n    return this.meter().max;\n  }\n\n  get denomination(): BudgetDenomination {\n    return this.meter().denomination;\n  }\n\n  get tranches(): readonly BudgetTranche[] {\n    return this.meter().tranches;\n  }\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. */","sourceCodeStart":812,"sourceCodeEnd":848,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/budget.ts#L812-L848","documentation":"Thrown by BudgetController accessors (denomination, tranches, and every other method that needs the meter) when the controller has no bound BudgetMeter. Controllers are created unbound via createBudgetController() and only become usable inside a run that binds them with the package-internal bindBudgetController(). Using one outside a run — before the run starts or after it ends and unbinds — throws this error.","triggerScenarios":"Calling controller.release()/controller.tranches/etc. before the run that owns the budget has started; storing a controller and using it after the run finished (unbindBudgetController removed the binding); creating a controller manually with new BudgetController() instead of the factory and never binding it.","commonSituations":"Exporting a controller from a module and reading it in startup code to display budget config; caching controllers across runs for reuse without re-binding; tests that instantiate a controller directly and call methods without setting up a run.","solutions":["Only touch the controller within the run callback that owns it — capture it from the run's context, not from global state.","If you need controller data before a run (e.g. to display limits), read it from the RunBudget config you passed in, not from the controller.","Create a fresh controller per run via createBudgetController() rather than reusing one across runs."],"exampleFix":"// before\nconst controller = createBudgetController();\nconsole.log(controller.denomination); // throws: not yet bound to any run\nawait runAgent({ budget, controller });\n\n// after\nconst controller = createBudgetController();\nconsole.log(budget.maxUsd !== undefined ? \"usd\" : \"tokens\"); // read config instead\nawait runAgent({ budget, controller });","handlingStrategy":"validation","validationCode":"// Read budget facts from your config before the run; touch the controller only inside the run.\nfunction describeBudget(budget: RunBudget): string {\n  return budget.maxUsd !== undefined ? `usd cap ${budget.maxUsd}` : `token cap ${budget.maxTokens}`;\n}","typeGuard":null,"tryCatchPattern":"try {\n  controller.release(100, \"top-up\");\n} catch (e) {\n  if (e instanceof Error && e.message === \"cave_budget_controller_unbound\") {\n    throw new Error(\"controller used outside its owning run — obtain it from the run context\");\n  }\n  throw e;\n}","preventionTips":["Scope controller usage strictly inside the run callback that binds it.","Do not export controllers from modules or cache them in globals for later inspection.","In tests, use the run harness to bind controllers instead of instantiating and calling directly."],"tags":["budget","lifecycle","controller","binding"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}