JuliusBrussee/caveman · error
cave_retry_accounting_invalid
cave_retry_accounting_invalid
Error message
cave_retry_accounting_invalid
What it means
settleRetry records the actual spend (measuredSpend) of a retried model call onto a breaker event so budget accounting stays consistent. If no matching candidate event with an undefined measuredSpend exists, or the measuredSpend is not finite or negative, the internal accounting invariant is broken and this error is thrown.
Solutions
- Check the provider usage/cost data feeding measuredSpend for NaN or negative values and clamp/validate before the call.
- Ensure settleRetry is called exactly once per retry attempt and that the attempt was opened through the same breaker instance.
- Upgrade/align the SDK and provider adapter versions so event recording and settlement match.
Example fix
// before settleRetry(measuredSpend: usage.cost) // usage.cost may be NaN // after const spend = Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : 0; settleRetry(spend)
Defensive patterns
Strategy: try-catch
Validate before calling
const spend = Number(usage.cost);
if (!Number.isFinite(spend) || spend < 0) throw new Error(`bad measuredSpend from provider: ${usage.cost}`); Type guard
const isValidSpend = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v >= 0;
Try / catch
try { await retryModelCall(...); } catch (e) {
if (e.message === 'cave_retry_accounting_invalid') {
logger.error('retry accounting invariant broken — check provider usage data and settleRetry call pattern');
}
throw e;
} Prevention
- Validate provider cost/usage values (finite, >= 0) at the adapter boundary.
- Settle each retry attempt exactly once; never re-settle settled events.
- Keep the SDK and provider adapter on compatible versions.
When it happens
Trigger: Calling retryModelCall (which reaches settleRetry) where the measured spend of the attempt is negative or NaN/Infinity, or settle is invoked for an event that was never opened / already settled (no candidate with measuredSpend === undefined).
Common situations: A provider adapter returning a NaN or negative cost estimate (e.g. zero-token usage divided incorrectly); double-settling the same attempt due to a bug in the retry wrapper; version mismatch where events were recorded differently.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- cave_breaker_retry_requires_budget
- budget is below CCR storage minimum
- cave_breaker_retry_backoff_invalid
- cave_breaker_retry_spend_invalid
- cave_budget_cap_breached
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/8890b7befc3ef6a6.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/breakers.ts:319
/** Settle receipt evidence for one already-recorded retry attempt. */
settleRetry(
attempt: number,
measuredSpend: number,
spendBasis: NonNullable<BreakerEvent["spendBasis"]>,
): void {
// Attempt numbers restart for each model call. Settle the newest matching
// event that has not already been settled, never an earlier call's retry.
let event: BreakerEvent | undefined;
for (let index = this.events.length - 1; index >= 0; index--) {
const candidate = this.events[index]!;
if (candidate.kind === "retry_attempted" && candidate.count === attempt &&
candidate.measuredSpend === undefined) {
event = candidate;
break;
}
}
if (event === undefined || !Number.isFinite(measuredSpend) || measuredSpend < 0) {
throw new Error("cave_retry_accounting_invalid");
}
(event as { measuredSpend?: number; spendBasis?: BreakerEvent["spendBasis"] }).measuredSpend = measuredSpend;
(event as { measuredSpend?: number; spendBasis?: BreakerEvent["spendBasis"] }).spendBasis = spendBasis;
}
/** Deterministic exponential backoff. No jitter: a breaker must be reproducible. */
backoffMs(attempt: number): number {
return this.config.retryBackoffMs * 2 ** Math.max(0, attempt - 1);
}
}
export function callSignature(toolName: string, args: unknown): string {
return sha256(stableStringify({ tool: toolName, args: args ?? null }));
}
View on GitHub (pinned to 3ee70a1026)