JuliusBrussee/caveman · error · Error
cave_${field}_invalid
cave_${field}_invalid
Error message
cave_${field}_invalid What it means
Template error thrown by the callCeilingOverride validator when a caller-supplied ceiling (e.g. maxModelCalls) is not a positive integer. The library treats a malformed cap as fail-closed data rather than "no cap", rejecting before any provider call.
Solutions
- Pass a positive integer (>= 1) for the ceiling option
- Coerce/round the value before calling: Math.max(1, Math.round(value))
- Omit the option to use the derived default (64 without an efficiency plan)
Example fix
// before
streamAgent({ maxModelCalls: 0 });
// after
streamAgent({ maxModelCalls: Math.max(1, Math.round(userCap)) }); Defensive patterns
Strategy: validation
Validate before calling
function assertCeiling(v) { if (v !== undefined && (!Number.isInteger(v) || v < 1)) throw new Error(`invalid ceiling: ${v}`); }
assertCeiling(opts.maxModelCalls); Type guard
const isValidCeiling = (v: unknown): v is number => typeof v === "number" && Number.isInteger(v) && v >= 1;
Try / catch
try { await streamAgent(opts); } catch (e) { if (/^cave_.*_invalid$/.test(e.message)) { /* fix the named ceiling option */ } else throw e; } Prevention
- Validate numeric options come from parsed integers, not raw strings
- Never use 0 to mean "unlimited" — omit the option instead
- Round/normalize computed caps before passing them
When it happens
Trigger: Passing options.maxModelCalls (or any validated ceiling field) as 0, a negative number, a float (e.g. 1.5), or a non-integer value to streamAgent.
Common situations: Reading the cap from env/config as a string-ish or computed value, dividing to compute a cap, or defaulting with 0 intending "unlimited".
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- cave_breaker_retry_requires_budget
- cave_breaker_threshold_invalid
- cave_budget_conflicting_cap
- cave_budget_controller_without_budget
- cave_budget_denomination_ambiguous
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/30b6ae8ad442806f.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/runtime.ts:1460
let modelCalls = 0;
let compactionsUsed = 0;
// Incremented the moment a compaction takes a reservation, so a paid
// attempt counts even when its summary is later discarded.
let compactionsSpent = 0;
let previousSummary: ContextSummary | undefined;
// Provider-reported, never assumed: the last call either read a cached
// prefix or it did not. Before the first call there is nothing to report.
let lastCallCacheState: "warm" | "cold" | "unknown" = "unknown";
const toolCalls: string[] = [];
let turnStateChanged = false;
const startedAt = performance.now();
// Caller-supplied ceilings override the derived defaults. They
// fail closed: a non-positive or non-integer value is a malformed cap, not
// "no cap", so it is rejected before the first provider call.
const callCeilingOverride = (value: number | undefined, field: string): number | undefined => {
if (value === undefined) return undefined;
if (!Number.isInteger(value) || value < 1) {
throw new Error(`cave_${field}_invalid`);
}
return value;
};
const maxModelCalls = callCeilingOverride(options.maxModelCalls, "max_model_calls") ??
(efficiencyPlan === undefined
? 64
: 1 + Math.max(1, Math.ceil(efficiencyPlan.budgets.retry_cascade_reserve / 256)));
const maxToolCalls = callCeilingOverride(options.maxToolCalls, "max_tool_calls") ??
(efficiencyPlan === undefined
? 64
: Math.max(1, definition.tools.length) * Math.max(1, maxModelCalls - 1));
const accountedAssistantMessages = new WeakSet<object>();
const accountProviderMessage = (message: AssistantMessage): void => {
// beforeToolCall runs after the provider message but before its tools.
// Account there so a call that consumed the deadline or wallet cannot
// launch fresh side effects. turn_end calls this too for tool-free turns;
// object identity makes the operation exactly once on either path.
if (accountedAssistantMessages.has(message)) {View on GitHub (pinned to 3ee70a1026)