{"record":{"id":"32efe6aec1940926","repo":"JuliusBrussee/caveman","slug":"cursor-key-must-be-a-string","errorCode":null,"errorMessage":"cursor.${key} must be a string","messagePattern":"cursor\\.(.+?) must be a string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/agent-mcp.ts","lineNumber":534,"sourceCode":"  if (pageSize !== undefined && (!Number.isSafeInteger(pageSize) || (pageSize as number) < 1 || (pageSize as number) > 500)) {\n    throw new Error(\"page_size must be an integer from 1 to 500\");\n  }\n  const sort = args.sort;\n  if (sort !== undefined) {\n    if (!sort || typeof sort !== \"object\" || Array.isArray(sort)) throw new Error(\"sort must be an object\");\n    const unknown = Object.keys(sort).filter((key) => key !== \"by\" && key !== \"dir\");\n    if (unknown.length > 0) throw new Error(`sort has unknown key(s): ${unknown.sort().join(\", \")}`);\n    optionalEnum(sort, \"by\", [\"timestamp\", \"total_cost_usd\", \"latency_ms\", \"total_tokens\"]);\n    optionalEnum(sort, \"dir\", [\"asc\", \"desc\"]);\n  }\n  const cursor = args.cursor;\n  if (cursor !== undefined) {\n    if (!cursor || typeof cursor !== \"object\" || Array.isArray(cursor)) throw new Error(\"cursor must be an object\");\n    const unknown = Object.keys(cursor).filter((key) => key !== \"sort_value\" && key !== \"request_id\" && key !== \"group_key\");\n    if (unknown.length > 0) throw new Error(`cursor has unknown key(s): ${unknown.sort().join(\", \")}`);\n    if (typeof cursor.sort_value !== \"string\" || cursor.sort_value === \"\") throw new Error(\"cursor.sort_value is required\");\n    for (const key of [\"request_id\", \"group_key\"]) {\n      if (cursor[key] !== undefined && typeof cursor[key] !== \"string\") throw new Error(`cursor.${key} must be a string`);\n    }\n  }\n}\n\nasync function callTool(client: AgentMcpClient, name: string, rawArgs: unknown): Promise<ToolResult> {\n  const started = Date.now();\n  let outcome: \"ok\" | \"error\" = \"ok\";\n  try {\n    const args = objectArg(rawArgs);\n    const allowed = TOOL_ARGUMENT_KEYS[name];\n    const unknown = Object.keys(args).filter((key) => !allowed?.has(key));\n    if (unknown.length > 0) throw new Error(`unknown argument(s): ${unknown.sort().join(\", \")}`);\n    let value: JSONValue;\n    switch (name) {\n      case \"caveman_context\": {\n        const [identity, projects, selectedProjectId] = await Promise.all([\n          client.request(\"/api/v1/auth/me\"),\n          client.request(\"/api/v1/projects\"),","sourceCodeStart":516,"sourceCodeEnd":552,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/agent-mcp.ts#L516-L552","documentation":"After checking allowed cursor keys, validateTraceSearch verifies that the optional request_id and group_key cursor fields are strings when present. This error means one of those keys was supplied with a non-string value (number, boolean, object, null).","triggerScenarios":"Calling trace search with cursor like {sort_value:\"x\", request_id:42} or {sort_value:\"x\", group_key:{id:1}}.","commonSituations":"IDs parsed from JSON as numbers; template-built cursors inserting numeric group keys; type coercion after YAML/config load.","solutions":["Pass request_id and group_key as strings, e.g. String(id)","Omit the key entirely if you do not intend to filter by it","Check where the cursor is serialized/deserialized that the ID types survive the round trip"],"exampleFix":"// before\n{ sort_value: \"x\", request_id: 42 }\n// after\n{ sort_value: \"x\", request_id: \"42\" }","handlingStrategy":"type-guard","validationCode":"function cursorOptionalStringsOk(c) { return c.request_id === undefined || typeof c.request_id === \"string\"; }\nconst gkOk = c.group_key === undefined || typeof c.group_key === \"string\";","typeGuard":"function hasStringOrAbsent<K extends string>(c: Record<string, unknown>, k: K): boolean { return c[k] === undefined || typeof c[k] === \"string\"; }","tryCatchPattern":"try { ... } catch (e) { if (/cursor\\.(request_id|group_key) must be a string/.test(e.message)) { coerceCursorIds(args.cursor); retry(); } else throw e; }","preventionTips":["Coerce IDs to strings at the boundary with String(id)","Type cursor request_id/group_key as string | undefined","Beware JSON parsers that keep numbers numeric — normalize after parse"],"tags":["validation","type-mismatch","cursor"],"backgroundTag":"type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}