{"record":{"id":"bad8656a16d7f011","repo":"JuliusBrussee/caveman","slug":"cave-budget-reservation-double-settle","errorCode":null,"errorMessage":"cave_budget_reservation_double_settle","messagePattern":"cave_budget_reservation_double_settle","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/budget.ts","lineNumber":408,"sourceCode":"    return Object.freeze({ child, reservation });\n  }\n\n  /** Return a carved wallet's unspent remainder to this meter. */\n  settleCarve(carve: BudgetCarve): void {\n    this.liveWallets.delete(carve.child);\n    this.settle(carve.reservation, carve.child.settled);\n  }\n\n  /**\n   * Replace a reservation with the measured cost of the call it covered.\n   *\n   * `actual` is recorded as it came in. It is never clamped to the reservation\n   * and never floored at `max`: a ledger that quietly rewrites what a call cost\n   * is a fake ledger, and the honest failure is a flagged breach, not a\n   * flattering number.\n   */\n  settle(reservation: BudgetReservation, actual: number): void {\n    if (reservation.settled) throw new Error(\"cave_budget_reservation_double_settle\");\n    reservation.settled = true;\n    this.reservedAmount = Math.max(0, this.reservedAmount - reservation.amount);\n    if (!Number.isFinite(actual)) {\n      // A NaN/Infinity measured cost is not knowable, so it fails closed at the\n      // reservation's worst case rather than booking $0. A call\n      // whose real cost we cannot read is never a free call — booking zero would\n      // be the flattering number this ledger refuses to write.\n      this.settledAmount += reservation.amount;\n    } else if (actual > 0) {\n      this.settledAmount += actual;\n    }\n    if (this.settledAmount > this.max) this.breachedFlag = true;\n  }\n\n  /** Drop a reservation whose call never reached the provider. */\n  cancel(reservation: BudgetReservation): void {\n    if (reservation.settled) return;\n    reservation.settled = true;","sourceCodeStart":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/budget.ts#L390-L426","documentation":"Thrown by BudgetMeter.settle when the reservation passed in has already been settled. Reservations are single-use: settle() replaces the reserved worst-case amount with the measured cost, and the reservation is flagged settled on first use. Settling twice would double-count spend and corrupt the ledger, so it is rejected as an invariant violation.","triggerScenarios":"Calling settle(reservation, cost) twice with the same reservation object; two code paths both handling the same completed call (e.g. a success handler and a completion callback); a carve-out helper that settles a child reservation and then the caller settles the parent reservation again.","commonSituations":"Promise wrappers where both .then() and .finally() trigger accounting; retry loops that reuse a reservation object across attempts; fan-out code where multiple observers of one provider call each try to record its cost.","solutions":["Settle exactly once per reservation: route all accounting through a single owner of the reservation object.","In guard code, check reservation.settled (or track settled IDs in a Set) before calling settle.","If multiple observers exist, have only the terminal one (success or error, exclusively) perform the settle — use a done flag or Promise's settle-once semantics."],"exampleFix":"// before\nstream.on(\"end\", () => meter.settle(reservation, cost));\nrequest.then(() => meter.settle(reservation, cost)); // second settle throws\n\n// after\nlet settled = false;\nconst settleOnce = (cost: number) => {\n  if (!settled) { settled = true; meter.settle(reservation, cost); }\n};\nstream.on(\"end\", () => settleOnce(cost));\nrequest.then(() => settleOnce(cost));","handlingStrategy":"try-catch","validationCode":"class SettleOnce {\n  private done = false;\n  constructor(private meter: BudgetMeter, private reservation: BudgetReservation) {}\n  settle(cost: number): void {\n    if (this.done || this.reservation.settled) return;\n    this.done = true;\n    this.meter.settle(this.reservation, cost);\n  }\n}","typeGuard":"function isUnsettled(r: BudgetReservation): boolean {\n  return !r.settled;\n}","tryCatchPattern":"try {\n  meter.settle(reservation, actual);\n} catch (e) {\n  if (e instanceof Error && e.message === \"cave_budget_reservation_double_settle\") {\n    logger.warn(\"settle skipped: reservation already settled\", { atCall });\n    return;\n  }\n  throw e;\n}","preventionTips":["Give each reservation exactly one owner responsible for settling it.","Use a done flag or Set to make settle idempotent in adapters with multiple completion callbacks (stream end, promise resolution, error).","In retry loops, create a fresh reservation per attempt rather than reusing one."],"tags":["budget","reservation","invariant","double-settle","accounting"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}