{"record":{"id":"2a9c834e64841bc9","repo":"JuliusBrussee/caveman","slug":"cave-budget-revoked","errorCode":null,"errorMessage":"cave_budget_revoked","messagePattern":"cave_budget_revoked","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/budget.ts","lineNumber":333,"sourceCode":"    return Math.max(0, this.max - this.releasedAmount);\n  }\n\n  /**\n   * Release a further tranche. Throws when it would breach `max`: this is\n   * pre-flight validation at a developer-controlled checkpoint, so the caller\n   * asked for something the contract cannot grant.\n   */\n  release(amount: number, reason: string): BudgetTranche {\n    if (!Number.isFinite(amount) || amount <= 0) {\n      throw new Error(\"cave_budget_release_invalid\");\n    }\n    if (this.denomination === \"tokens\" && !Number.isSafeInteger(amount)) {\n      throw new Error(\"cave_budget_release_invalid\");\n    }\n    if (typeof reason !== \"string\" || reason.trim() === \"\") {\n      throw new Error(\"cave_budget_release_reason_required\");\n    }\n    if (this.revokedFlag) throw new Error(\"cave_budget_revoked\");\n    // A breached ledger is dead. Releasing into it would record a tranche and\n    // raise an escalation for money that can never be spent, and would read on\n    // the receipt as a run that was still being funded after it went past cap.\n    if (this.breachedFlag) throw new Error(\"cave_budget_cap_breached\");\n    if (amount > this.releasable()) throw new Error(\"cave_budget_release_exceeds_max\");\n    this.releasedAmount += amount;\n    const tranche: BudgetTranche = Object.freeze({\n      amount,\n      reason,\n      atCall: this.callIndex,\n    });\n    this.trancheLog.push(tranche);\n    return tranche;\n  }\n\n  /**\n   * Hold `amount` against the ledger. Returns `undefined` when it does not fit,\n   * which is the caller's signal to clamp, compact, or stop — never to proceed.","sourceCodeStart":315,"sourceCodeEnd":351,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/budget.ts#L315-L351","documentation":"Thrown by BudgetMeter.release when the ledger has been revoked. Revocation permanently kills the budget — typically an escalation or abort path — and any later attempt to release funds into it is a logic error: money cannot be granted by a budget that no longer exists as an authority.","triggerScenarios":"Calling release() after an abort/escalation handler called revoke(); a cleanup or retry path that races with revocation and still tries to fund the next call; holding a stale meter reference after the owning run was cancelled.","commonSituations":"Cancellation tokens or AbortSignals firing mid-loop while the loop body unconditionally releases the next tranche; error-recovery code that re-funds after the supervisor already revoked the budget for the run; async callbacks resolving after a timeout revocation.","solutions":["Check the revocation state before releasing if the API exposes it, or structure the loop so revocation short-circuits before the release step.","Wire AbortSignal/cancellation checks immediately before each release call.","Wrap release in try-catch for cave_budget_revoked and treat it as a normal termination signal, ending the funding loop gracefully."],"exampleFix":"// before\nwhile (running) {\n  meter.release(tranche, `call ${i}`); // throws after revoke\n  await doCall(i++);\n}\n\n// after\nwhile (running && !aborted) {\n  try {\n    meter.release(tranche, `call ${i}`);\n  } catch (e) {\n    if (e instanceof Error && e.message === \"cave_budget_revoked\") break;\n    throw e;\n  }\n  await doCall(i++);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  meter.release(amount, reason);\n} catch (e) {\n  if (e instanceof Error && e.message === \"cave_budget_revoked\") {\n    return; // budget revoked mid-run: stop funding, treat as normal termination\n  }\n  throw e;\n}","preventionTips":["Check cancellation/abort state immediately before each release in the driver loop.","Treat cave_budget_revoked as a control-flow signal, not an exceptional failure — end the loop gracefully on it.","Ensure revocation paths also stop the code that would otherwise keep funding calls."],"tags":["budget","lifecycle","cancellation","tranche"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}