{"record":{"id":"3f06669c4680dbf1","repo":"JuliusBrussee/caveman","slug":"caveman-agent-tool-timeoutms-must-be-a-positive-i","errorCode":null,"errorMessage":"caveman agent: tool timeoutMs must be a positive integer","messagePattern":"caveman agent: tool timeoutMs must be a positive integer","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/primitives.ts","lineNumber":142,"sourceCode":"  options: ToolOptions<TSchema, unknown> |\n    StandardToolOptions<unknown, unknown, unknown> |\n    StandardJSONToolOptions<unknown, unknown, unknown>,\n): ToolDefinition<unknown, unknown> {\n  if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(options.name)) {\n    throw new Error(`caveman agent: invalid tool name ${JSON.stringify(options.name)}`);\n  }\n  if (![\"read\", \"write\", \"idempotent\", \"external\"].includes(options.effect)) {\n    throw new Error(`caveman agent: unknown tool effect ${JSON.stringify(options.effect)}`);\n  }\n  const result = typeof options.result === \"object\"\n    ? artifactResultPolicy(options.result)\n    : options.result ?? \"auto\";\n  if (![\"auto\", \"inline\", \"page\", \"compress\", \"exact_ccr\"].includes(result)) {\n    throw new Error(`caveman agent: unknown tool result policy ${JSON.stringify(result)}`);\n  }\n  const timeoutMs = options.timeoutMs ?? 30_000;\n  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {\n    throw new Error(\"caveman agent: tool timeoutMs must be a positive integer\");\n  }\n  const standard = standardToolSchema(options.input);\n  let input: TSchema;\n  if (standard === undefined) {\n    input = options.input as TSchema;\n  } else {\n    let converted = \"inputJSONSchema\" in options\n      ? options.inputJSONSchema\n      : undefined;\n    if (converted === undefined && standard.jsonSchema !== undefined) {\n      try {\n        converted = standard.jsonSchema.input({ target: \"draft-07\" });\n      } catch (error) {\n        throw new Error(\"caveman agent: Standard Schema cannot emit draft-07 input JSON Schema\", {\n          cause: error,\n        });\n      }\n    }","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/primitives.ts#L124-L160","documentation":"tool() validates the per-call timeout: timeoutMs must be a safe positive integer (default 30_000 ms). Fractional timeouts, zero, negatives, NaN, or numeric strings are rejected because the runtime schedules calls with this exact value. Throws synchronously at definition time.","triggerScenarios":"Passing timeoutMs: 0 (often meant as 'no timeout'), timeoutMs: 1.5, timeoutMs: \"30000\", or timeoutMs: -1. Computed values like seconds-to-ms math gone wrong (0.5 * 1000 with truncation elsewhere) also land here.","commonSituations":"Config-driven timeouts parsed from YAML/CLI as strings, milliseconds/seconds confusion (passing 30 meaning 30 ms when 30 s was intended — valid but a footgun — or 0.03 * 1000 = 30.000000000000004 float), or copying a default of -1 from another library meaning 'infinite'.","solutions":["Pass a positive integer of milliseconds, e.g. timeoutMs: 60_000","Omit timeoutMs to accept the 30 s default","Convert and round user input: Math.max(1, Math.round(Number(raw)))","There is no 'no timeout' value — pick the largest ceiling you accept"],"exampleFix":"// before\ntool({ name: \"run\", effect: \"external\", timeoutMs: Number(cfg.timeout), execute: ... }); // cfg.timeout = \"30s\" -> NaN\n\n// after\nconst secs = parseFloat(cfg.timeout);\ntool({\n  name: \"run\",\n  effect: \"external\",\n  timeoutMs: Number.isFinite(secs) ? Math.max(1, Math.round(secs * 1000)) : 30_000,\n  execute: ...,\n});","handlingStrategy":"validation","validationCode":"const t = Number(cfg.timeoutMs);\nif (!Number.isSafeInteger(t) || t <= 0) {\n  throw new Error(`timeoutMs must be positive integer milliseconds, got ${JSON.stringify(cfg.timeoutMs)}`);\n}","typeGuard":"const isPositiveInt = (v: unknown): v is number =>\n  Number.isSafeInteger(v) && (v as number) > 0;","tryCatchPattern":null,"preventionTips":["Always name timeout config in milliseconds explicitly (timeoutMs, never 'timeout' in seconds)","Round parsed values: Math.max(1, Math.round(Number(raw)))","Reject -1/0 sentinels at your config boundary — this library has no infinite timeout"],"tags":["validation","timeout","tool-config","api-misuse"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}