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

  1. Audit every call site of settleRetrySpend: confirm each retry event is settled exactly once, guarded by its own control flow rather than hope.
  2. 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.
  3. Validate measuredSpend with Number.isFinite(spend) && spend >= 0 before calling settle; log the raw provider usage payload when it fails.
  4. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/8890b7befc3ef6a6. Report an issue: GitHub.