JuliusBrussee/caveman · error

cave_claude_max_turns_invalid

cave_claude_max_turns_invalid

Error message

cave_claude_max_turns_invalid

What it means

The options.maxTurns value passed to the Claude runtime is validated to be a safe integer strictly between 0 and 1000 (inclusive). Anything else — non-integer, zero, negative, above 1000, or non-numeric — throws this error before the agent runs.

Solutions

  1. Set maxTurns to a positive safe integer within 1..1000
  2. Clamp or validate the value before the call: Math.min(1000, Math.max(1, Number(maxTurns)))
  3. Omit maxTurns entirely to use the runtime default

Example fix

// before
await runClaudeAgent({ ...opts, maxTurns: Number(process.env.MAX_TURNS) });
// after
const turns = Number(process.env.MAX_TURNS);
await runClaudeAgent({ ...opts, maxTurns: Math.min(1000, Math.max(1, Math.trunc(turns))) });
Defensive patterns

Strategy: validation

Validate before calling

if (opts.maxTurns !== undefined && (!Number.isSafeInteger(opts.maxTurns) || opts.maxTurns <= 0 || opts.maxTurns > 1000)) throw new RangeError("maxTurns must be an integer in 1..1000");

Type guard

const isValidMaxTurns = (v) => v === undefined || (Number.isSafeInteger(v) && v > 0 && v <= 1000);

Try / catch

try { await runClaudeAgent(opts); } catch (e) { if (e.message === "cave_claude_max_turns_invalid") { opts.maxTurns = 10; return runClaudeAgent(opts); } throw e; }

Prevention

When it happens

Trigger: Calling runClaudeAgent/runClaudeAgentInternal with options.maxTurns set to 0, -1, 1001, NaN, 2.5, or a value exceeding Number.MAX_SAFE_INTEGER.

Common situations: Computing maxTurns from config or user input without clamping; passing 0 intending 'unlimited'; parsing a string like "50" from an env var without conversion.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/8e615fcb0fab07e0. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/claude-runtime.ts:107

    throw new Error("cave_claude_candidate_execution_unavailable");
  }
  if (options.lockedBuild !== undefined) {
    throw new Error("cave_claude_locked_execution_unavailable");
  }
  if (definition.memory !== undefined) {
    throw new Error("cave_claude_memory_bridge_unavailable");
  }
  if (definition.tools.some((item) => item.runtime?.kind === "subagent")) {
    throw new Error("cave_claude_subagent_bridge_unavailable");
  }
  const unsupportedTool = definition.tools.find((item) =>
    item.effect !== "read" || item.result !== "inline");
  if (unsupportedTool !== undefined) {
    throw new Error(`cave_claude_tool_contract_unsupported:${unsupportedTool.name}`);
  }
  if (options.maxTurns !== undefined &&
      (!Number.isSafeInteger(options.maxTurns) || options.maxTurns <= 0 || options.maxTurns > 1_000)) {
    throw new Error("cave_claude_max_turns_invalid");
  }
  if (options.maxBudgetUsd !== undefined &&
      (!Number.isFinite(options.maxBudgetUsd) || options.maxBudgetUsd <= 0)) {
    throw new Error("cave_claude_max_budget_invalid");
  }
  const rootDir = resolve(options.rootDir ?? process.cwd());
  const lowered = await lowerAgentContext(definition, { rootDir, input });
  const bill = contextBill(lowered.ir);
  const selected = resolveClaudeModel(definition, rootDir);
  const reasoningOptions = claudeReasoningOptions(
    selected,
    definition.reasoning,
    definition.output?.maxTokens,
  );
  const sandbox = await prepareHarnessToolSandbox(definition, {
    rootDir,
    ...(options.entryPath === undefined ? {} : { entryPath: options.entryPath }),
  });

View on GitHub (pinned to 3ee70a1026)