{"record":{"id":"30b6ae8ad442806f","repo":"JuliusBrussee/caveman","slug":"cave-field-invalid","errorCode":null,"errorMessage":"cave_${field}_invalid","messagePattern":"cave_(.+?)_invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":1460,"sourceCode":"    let modelCalls = 0;\n    let compactionsUsed = 0;\n    // Incremented the moment a compaction takes a reservation, so a paid\n    // attempt counts even when its summary is later discarded.\n    let compactionsSpent = 0;\n    let previousSummary: ContextSummary | undefined;\n    // Provider-reported, never assumed: the last call either read a cached\n    // prefix or it did not. Before the first call there is nothing to report.\n    let lastCallCacheState: \"warm\" | \"cold\" | \"unknown\" = \"unknown\";\n    const toolCalls: string[] = [];\n    let turnStateChanged = false;\n    const startedAt = performance.now();\n    // Caller-supplied ceilings override the derived defaults. They\n    // fail closed: a non-positive or non-integer value is a malformed cap, not\n    // \"no cap\", so it is rejected before the first provider call.\n    const callCeilingOverride = (value: number | undefined, field: string): number | undefined => {\n      if (value === undefined) return undefined;\n      if (!Number.isInteger(value) || value < 1) {\n        throw new Error(`cave_${field}_invalid`);\n      }\n      return value;\n    };\n    const maxModelCalls = callCeilingOverride(options.maxModelCalls, \"max_model_calls\") ??\n      (efficiencyPlan === undefined\n        ? 64\n        : 1 + Math.max(1, Math.ceil(efficiencyPlan.budgets.retry_cascade_reserve / 256)));\n    const maxToolCalls = callCeilingOverride(options.maxToolCalls, \"max_tool_calls\") ??\n      (efficiencyPlan === undefined\n        ? 64\n        : Math.max(1, definition.tools.length) * Math.max(1, maxModelCalls - 1));\n    const accountedAssistantMessages = new WeakSet<object>();\n    const accountProviderMessage = (message: AssistantMessage): void => {\n      // beforeToolCall runs after the provider message but before its tools.\n      // Account there so a call that consumed the deadline or wallet cannot\n      // launch fresh side effects. turn_end calls this too for tool-free turns;\n      // object identity makes the operation exactly once on either path.\n      if (accountedAssistantMessages.has(message)) {","sourceCodeStart":1442,"sourceCodeEnd":1478,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/runtime.ts#L1442-L1478","documentation":"Dynamic template error: field is \"max_model_calls\" or \"max_tool_calls\", producing cave_max_model_calls_invalid / cave_max_tool_calls_invalid. Caller-supplied ceilings (RunOptions.maxModelCalls, RunOptions.maxToolCalls) fail closed: any value that is not an integer >= 1 is a malformed cap, not 'no cap', and is rejected before the first provider call. Defaults are derived (64 calls without a plan; plan-derived with one) when the options are omitted.","triggerScenarios":"Passing maxModelCalls: 0, a negative number, a float like 8.5, or NaN; same for maxToolCalls. Computed ceilings from config that default to 0 are the usual source.","commonSituations":"Config-driven caps where an unset value coerces to 0; trying to express 'one shot only' with 0 (should be 1); dividing budgets into call counts and passing a fractional result.","solutions":["Pass an integer >= 1, e.g. maxModelCalls: 8, or omit to use the derived default","For 'exactly one model call', use maxModelCalls: 1, never 0","Sanitize computed values: Math.max(1, Math.floor(n))"],"exampleFix":"// before\nawait agent.run(input, { maxModelCalls: calls }); // calls = 0 from unset config\n\n// after\nconst calls = Number(process.env.MAX_CALLS);\nawait agent.run(input, {\n  ...(Number.isInteger(calls) && calls >= 1 ? { maxModelCalls: calls } : {}),\n});","handlingStrategy":"validation","validationCode":"function callCeiling(v: unknown): number | undefined {\n  return typeof v === \"number\" && Number.isInteger(v) && v >= 1 ? v : undefined;\n}\nconst opts = {\n  ...(callCeiling(config.maxModelCalls) !== undefined ? { maxModelCalls: callCeiling(config.maxModelCalls)! } : {}),\n  ...(callCeiling(config.maxToolCalls) !== undefined ? { maxToolCalls: callCeiling(config.maxToolCalls)! } : {}),\n};","typeGuard":"const isCallCeiling = (v: unknown): v is number =>\n  typeof v === \"number\" && Number.isInteger(v) && v >= 1;","tryCatchPattern":"try { await agent.run(input, opts); } catch (e) {\n  if (e instanceof Error && /^cave_(max_model_calls|max_tool_calls)_invalid$/.test(e.message)) {\n    // config bug: fix the ceiling value\n  } else throw e;\n}","preventionTips":["Use 1 for single-shot runs, never 0","Validate ceilings alongside other integer run options in one helper","Omit the options to accept the derived defaults"],"tags":["validation","limits","run-options"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}