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

  1. Check the provider usage/cost data feeding measuredSpend for NaN or negative values and clamp/validate before the call.
  2. Ensure settleRetry is called exactly once per retry attempt and that the attempt was opened through the same breaker instance.
  3. 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

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


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)