JuliusBrussee/caveman · error · Error

cave_budget_initial_invalid

Error message

cave_budget_initial_invalid

What it means

Thrown by normalizeRunBudget when the initial budget value (explicit initialUsd/initialTokens, or the cap itself when no initial is given) is non-finite, non-positive, or greater than the cap. The 'initial' field models spend already consumed before this run (a warm start); it must be a sensible fraction of the cap, not exceed it, and not be garbage.

Source

Thrown at packages/agent/src/budget.ts:105

 * self-contradicting budget is rejected before the first provider call rather
 * than silently degrading into no cap at all.
 */
export function normalizeRunBudget(budget: RunBudget): NormalizedBudget {
  const usd = budget.maxUsd !== undefined;
  const tokens = budget.maxTokens !== undefined;
  if (usd === tokens) throw new Error("cave_budget_denomination_ambiguous");
  const denomination: BudgetDenomination = usd ? "usd" : "tokens";
  const max = usd ? budget.maxUsd! : budget.maxTokens!;
  if (!Number.isFinite(max) || max <= 0) throw new Error("cave_budget_max_invalid");
  if (denomination === "tokens" && !Number.isSafeInteger(max)) {
    throw new Error("cave_budget_max_invalid");
  }
  const wrongInitial = denomination === "usd" ? budget.initialTokens : budget.initialUsd;
  if (wrongInitial !== undefined) throw new Error("cave_budget_denomination_ambiguous");
  const declaredInitial = denomination === "usd" ? budget.initialUsd : budget.initialTokens;
  const initial = declaredInitial ?? max;
  if (!Number.isFinite(initial) || initial <= 0 || initial > max) {
    throw new Error("cave_budget_initial_invalid");
  }
  if (denomination === "tokens" && !Number.isSafeInteger(initial)) {
    throw new Error("cave_budget_initial_invalid");
  }
  const outputFloorTokens = budget.outputFloorTokens ?? OUTPUT_CLAMP_FLOOR_TOKENS;
  if (!Number.isSafeInteger(outputFloorTokens) || outputFloorTokens <= 0) {
    throw new Error("cave_budget_output_floor_invalid");
  }
  const onExhausted = budget.onExhausted ?? "compact";
  if (onExhausted !== "compact" && onExhausted !== "stop") {
    throw new Error("cave_budget_on_exhausted_invalid");
  }
  return Object.freeze({
    denomination,
    max,
    initial,
    outputFloorTokens,
    onExhausted,

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Clamp and validate the initial before constructing the budget: it must satisfy 0 < initial <= max.
  2. If the initial is derived from prior spend, default it to the max when the prior-spend source is unavailable instead of passing NaN.
  3. When shrinking a cap, re-check any persisted initial value against the new max and reset it if it now exceeds the cap.

Example fix

// before
const budget = { maxUsd: 5, initialUsd: priorRun?.spent }; // NaN when priorRun is undefined

// after
const initial = priorRun !== undefined && Number.isFinite(priorRun.spent) && priorRun.spent > 0
  ? Math.min(priorRun.spent, 5)
  : undefined;
const budget = { maxUsd: 5, ...(initial !== undefined ? { initialUsd: initial } : {}) };
Defensive patterns

Strategy: validation

Validate before calling

function normalizeInitial(initial: number | undefined, max: number): number | undefined {
  if (initial === undefined) return undefined;
  if (!Number.isFinite(initial) || initial <= 0 || initial > max) return undefined; // fall back to default
  return initial;
}

Type guard

function isValidInitial(v: unknown, max: number): v is number {
  return typeof v === "number" && Number.isFinite(v) && v > 0 && v <= max;
}

Prevention

When it happens

Trigger: Passing initialUsd: -1 or initialTokens: 0; passing an initial greater than max (e.g. initialUsd: 10 with maxUsd: 5); passing NaN or Infinity as the initial; the initial defaulting to max after the max itself already failed validation upstream in the same call.

Common situations: Seeding a run with prior-session spend read from a telemetry store that returns NaN for missing entries; carrying an initial value forward from a previous larger budget while shrinking the cap; off-by-sign bugs when subtracting refunds from a running total to compute the initial.

Related errors


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