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
- Clamp and validate the initial before constructing the budget: it must satisfy 0 < initial <= max.
- If the initial is derived from prior spend, default it to the max when the prior-spend source is unavailable instead of passing NaN.
- 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
- Validate prior-spend records (Number.isFinite) before seeding a run with them.
- When shrinking a cap, re-check any carried-over initial against the new max.
- Prefer omitting initial over passing edge values; the default (initial = max) is always valid.
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
- cave_budget_max_invalid
- cave_budget_release_invalid
- cave_budget_denomination_ambiguous
- cave_budget_output_floor_invalid
- cave_budget_on_exhausted_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/b3c61dc999c7288d.
Report an issue: GitHub.