{"record":{"id":"62849c896fe2fc8b","repo":"JuliusBrussee/caveman","slug":"cave-budget-controller-unbound","errorCode":"cave_budget_controller_unbound","errorMessage":"cave_budget_controller_unbound","messagePattern":"cave_budget_controller_unbound","errorType":"error_code","errorClass":null,"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/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/budget.ts#L812-L848","documentation":"BudgetController lazily resolves its BudgetMeter through a WeakMap of controller-to-meter bindings. This error means the controller was created (createBudgetController) but never bound to a meter before any budget-reading method (e.g. tranches) was accessed. The library throws rather than silently reporting an empty budget.","triggerScenarios":"Calling any BudgetController accessor (tranches, and other methods routed through the private meter()) on a controller that has not had bindBudgetController(controller, meter) called, or after unbindBudgetController(controller) released its binding.","commonSituations":"Constructing a controller directly instead of through the run lifecycle that binds it; accessing budget state after the run ended and unbound the controller; sharing one controller across two runs where the second bind attempt failed; test code instantiating BudgetController without simulating the bind step.","solutions":["Ensure the controller is created and consumed within the run lifecycle that calls bindBudgetController before any budget access","If reusing a controller across runs, re-bind it via bindBudgetController after unbind, or create a fresh controller per run","Check for early access in startup/logging code that reads tranches before the run binds the meter","Catch the error to detect unbound controllers and fall back to a no-op or re-initialization path"],"exampleFix":"// before\nconst controller = createBudgetController();\nconsole.log(controller.tranches); // throws\n// after\nconst controller = createBudgetController();\nbindBudgetController(controller, meter);\nconsole.log(controller.tranches);","handlingStrategy":"try-catch","validationCode":"// WeakMap binding is not directly observable; track binding yourself\nlet bound = false;\nfunction bind(c: BudgetController, m: BudgetMeter) { bindBudgetController(c, m); bound = true; }\nif (!bound) throw new Error(\"controller not bound yet\");","typeGuard":null,"tryCatchPattern":"try {\n  const tranches = controller.tranches;\n} catch (e) {\n  if (e instanceof Error && e.message === \"cave_budget_controller_unbound\") {\n    // re-bind or defer budget access until the run is active\n  } else throw e;\n}","preventionTips":["Always create controllers through the run lifecycle that binds them","Never read budget properties outside an active run","In tests, mirror the production bind/unbind sequence in setup/teardown"],"tags":["budget","lifecycle","invalid-state"],"backgroundTag":"internal-invariant-violation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}