JuliusBrussee/caveman · error · Error

cave_budget_on_exhausted_invalid

Error message

cave_budget_on_exhausted_invalid

What it means

Thrown by normalizeRunBudget when the onExhausted policy is anything other than the two supported strings "compact" or "stop". This field controls what the runtime does when the budget runs out — compact the context and continue cheaper, or stop the run — and an unknown value cannot be safely guessed, so it fails closed. The field is optional with a default of "compact".

Source

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

    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,
    compaction: normalizeCompaction(budget.compaction),
  });
}

/** Identity and shape of one prospective provider call. */
export interface CallCeiling {
  readonly provider: string;
  readonly model: string;
  /** Upper bound on the request's input tokens. Never an estimate that can be low. */
  readonly inputTokenCeiling: number;
  /** The output allowance this call would ask for. */

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Use exactly "compact" or "stop" (lowercase), or omit the field to accept the "compact" default.
  2. Type the config field in your own code as a union type so the compiler rejects bad values before runtime.
  3. If the value comes from user input, normalize it: lowercase and trim before validation, and reject unknown values with a helpful message.

Example fix

// before
const budget = { maxUsd: 5, onExhausted: "abort" as any };

// after
type OnExhausted = "compact" | "stop";
const raw = String(process.env.ON_EXHAUSTED ?? "").toLowerCase().trim();
const budget = { maxUsd: 5, ...(raw === "compact" || raw === "stop" ? { onExhausted: raw satisfies OnExhausted } : {}) };
Defensive patterns

Strategy: type-guard

Validate before calling

const raw = String(process.env.ON_EXHAUSTED ?? "").trim().toLowerCase();
const onExhausted = raw === "stop" ? "stop" : "compact"; // unknown values fall back to default

Type guard

function isOnExhausted(v: unknown): v is "compact" | "stop" {
  return v === "compact" || v === "stop";
}

Prevention

When it happens

Trigger: Passing onExhausted: "abort", "halt", "continue", or any string outside the union; passing a non-string (number, boolean, object); passing a string with different casing like "Stop" or "COMPACT".

Common situations: Config files written from memory instead of the docs, using plausible-but-unsupported verbs like "abort" or "fail"; case mismatches after a config-linting tool normalizes enum-like values to uppercase; version drift if a newer release renamed the policy values and the config was not migrated.

Related errors


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