JuliusBrussee/caveman · error

caveman agent: tool timeoutMs must be a positive integer

Error message

caveman agent: tool timeoutMs must be a positive integer

What it means

tool() defaults timeoutMs to 30_000 and requires it to be a positive safe integer (Number.isSafeInteger and > 0). Zero, negative, fractional, NaN, or values beyond the safe-integer range are rejected so the scheduler always gets a usable timeout.

Solutions

  1. Pass a positive integer in milliseconds, e.g. timeoutMs: 60000
  2. Convert duration values properly (seconds * 1000, Math.round) before passing
  3. Omit timeoutMs to use the 30-second default

Example fix

// before
tool({ name: "bash", timeoutMs: Number(process.env.TOOL_TIMEOUT), ... }) // "30" ok, "" -> NaN
// after
const timeoutMs = Number.parseInt(process.env.TOOL_TIMEOUT ?? "30000", 10);
tool({ name: "bash", timeoutMs: Number.isSafeInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : 30000, ... })
Defensive patterns

Strategy: validation

Validate before calling

function validTimeoutMs(v: unknown): v is number {
  return typeof v === "number" && Number.isSafeInteger(v) && v > 0;
}

Type guard

function isPositiveInt(v: unknown): v is number {
  return Number.isSafeInteger(v) && (v as number) > 0;
}

Try / catch

try {
  const def = tool(options);
} catch (err) {
  if (err instanceof Error && err.message.includes("timeoutMs must be a positive integer")) {
    // rebuild options with timeoutMs: 30000
  } else throw err;
}

Prevention

When it happens

Trigger: tool({ timeoutMs: 0 }), tool({ timeoutMs: -1000 }), tool({ timeoutMs: 1.5 }), tool({ timeoutMs: Number.NaN }), or parsing a timeout from config into a non-integer (e.g. "30s" passed through Number()).

Common situations: Duration strings or seconds-based values from config not converted to integer milliseconds; arithmetic producing floats (30 * 0.5); environment placeholders expanding to empty/garbage; infinity from misconfigured defaults.

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


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

Appendix: source

Thrown at packages/agent/src/primitives.ts:142

  options: ToolOptions<TSchema, unknown> |
    StandardToolOptions<unknown, unknown, unknown> |
    StandardJSONToolOptions<unknown, unknown, unknown>,
): ToolDefinition<unknown, unknown> {
  if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(options.name)) {
    throw new Error(`caveman agent: invalid tool name ${JSON.stringify(options.name)}`);
  }
  if (!["read", "write", "idempotent", "external"].includes(options.effect)) {
    throw new Error(`caveman agent: unknown tool effect ${JSON.stringify(options.effect)}`);
  }
  const result = typeof options.result === "object"
    ? artifactResultPolicy(options.result)
    : options.result ?? "auto";
  if (!["auto", "inline", "page", "compress", "exact_ccr"].includes(result)) {
    throw new Error(`caveman agent: unknown tool result policy ${JSON.stringify(result)}`);
  }
  const timeoutMs = options.timeoutMs ?? 30_000;
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
    throw new Error("caveman agent: tool timeoutMs must be a positive integer");
  }
  const standard = standardToolSchema(options.input);
  let input: TSchema;
  if (standard === undefined) {
    input = options.input as TSchema;
  } else {
    let converted = "inputJSONSchema" in options
      ? options.inputJSONSchema
      : undefined;
    if (converted === undefined && standard.jsonSchema !== undefined) {
      try {
        converted = standard.jsonSchema.input({ target: "draft-07" });
      } catch (error) {
        throw new Error("caveman agent: Standard Schema cannot emit draft-07 input JSON Schema", {
          cause: error,
        });
      }
    }

View on GitHub (pinned to 3ee70a1026)