JuliusBrussee/caveman · error · Error
cave_retry_accounting_invalid
Error message
cave_retry_accounting_invalid
What it means
Thrown by BreakerEvents.settleRetrySpend when it cannot settle a retry's measured spend: either no 'retry_attempted' event exists whose count matches the given attempt and whose measuredSpend is still unset, or the measuredSpend value itself is non-finite or negative. The breaker keeps a strict audit trail; attempt numbers restart per model call, and each retry event must be settled exactly once. This error means the retry ledger and the settle call have diverged — a double settle, a wrong attempt index, or garbage cost data from the provider adapter.
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 27d5a3981a)
Solutions
- Audit every call site of settleRetrySpend: confirm each retry event is settled exactly once, guarded by its own control flow rather than hope.
- Check the attempt value being passed — it must match the count recorded on the retry_attempted event for the current model call, not a global retry counter.
- Validate measuredSpend with Number.isFinite(spend) && spend >= 0 before calling settle; log the raw provider usage payload when it fails.
- If usage arrives from multiple sources (stream + final message), pick one canonical source and ignore the other, or make settle idempotent per event upstream.
Example fix
// before
breaker.settleRetrySpend(attempt, parsedUsage.totalCost); // parsedUsage.totalCost is NaN when provider omits usage
// after
const spend = parsedUsage?.totalCost;
if (spend !== undefined && Number.isFinite(spend) && spend >= 0 && !retrySettled.has(attempt)) {
retrySettled.add(attempt);
breaker.settleRetrySpend(attempt, spend);
} Defensive patterns
Strategy: validation
Validate before calling
const settled = new Set<number>();
function settleRetry(breaker: BreakerEvents, attempt: number, spend: number | undefined): boolean {
if (settled.has(attempt)) return false; // already settled for this call
if (spend === undefined || !Number.isFinite(spend) || spend < 0) return false; // unusable measurement
settled.add(attempt);
breaker.settleRetrySpend(attempt, spend);
return true;
} Type guard
function isSettleableSpend(v: unknown): v is number {
return typeof v === "number" && Number.isFinite(v) && v >= 0;
} Try / catch
try {
breaker.settleRetrySpend(attempt, spend);
} catch (e) {
if (e instanceof Error && e.message === "cave_retry_accounting_invalid") {
// Ledger divergence: log the attempt number, event log length, and raw usage payload; do not retry the settle.
logger.error("retry settle diverged", { attempt, spend, events: breaker.events.length });
return;
}
throw e;
} Prevention
- Track settled attempt numbers in a Set per model call so double settles are impossible.
- Validate provider usage payloads (Number.isFinite, >= 0) at the adapter boundary before they reach the breaker.
- Treat settle as single-owner: exactly one code path terminates a retry attempt and records its spend.
When it happens
Trigger: Calling settleRetrySpend(attempt, spend) twice for the same retry event; passing an attempt number that never had a retry_attempted event recorded; calling settle before recordRetry; passing NaN, Infinity, or a negative number as measuredSpend; settling attempt N when the newest unsettled matching event belongs to an earlier model call that was already settled.
Common situations: A provider adapter that reports usage twice (once from streaming chunks and once from the final response), leading the caller to settle the same retry twice; an off-by-one when computing attempt numbers across a retry loop that spans multiple model calls; a usage parser producing NaN when the provider omits a usage field.
Related errors
- cave_budget_reservation_double_settle
- cave_nested_usage_incomplete
- cave_breaker_retry_requires_budget
- cave_breaker_retry_spend_invalid
- cave_breaker_retry_backoff_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/8890b7befc3ef6a6.
Report an issue: GitHub.