JuliusBrussee/caveman · error
cursor. must be a string
Error message
cursor.${key} must be a string What it means
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).
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
Example fix
// before
{ sort_value: "x", request_id: 42 }
// after
{ sort_value: "x", request_id: "42" } Defensive patterns
Strategy: type-guard
Validate before calling
function cursorOptionalStringsOk(c) { return c.request_id === undefined || typeof c.request_id === "string"; }
const gkOk = c.group_key === undefined || typeof c.group_key === "string"; Type guard
function hasStringOrAbsent<K extends string>(c: Record<string, unknown>, k: K): boolean { return c[k] === undefined || typeof c[k] === "string"; } Try / catch
try { ... } catch (e) { if (/cursor\.(request_id|group_key) must be a string/.test(e.message)) { coerceCursorIds(args.cursor); retry(); } else throw e; } Prevention
- 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
When it happens
Trigger: Calling trace search with cursor like {sort_value:"x", request_id:42} or {sort_value:"x", group_key:{id:1}}.
Common situations: IDs parsed from JSON as numbers; template-built cursors inserting numeric group keys; type coercion after YAML/config load.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- cursor must be an object
- cursor.sort_value is required
- is not an array (got )
- Invalid caveman_branch_id
- Invalid caveman_cache_epoch
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/32efe6aec1940926.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/agent-mcp.ts:534
if (pageSize !== undefined && (!Number.isSafeInteger(pageSize) || (pageSize as number) < 1 || (pageSize as number) > 500)) {
throw new Error("page_size must be an integer from 1 to 500");
}
const sort = args.sort;
if (sort !== undefined) {
if (!sort || typeof sort !== "object" || Array.isArray(sort)) throw new Error("sort must be an object");
const unknown = Object.keys(sort).filter((key) => key !== "by" && key !== "dir");
if (unknown.length > 0) throw new Error(`sort has unknown key(s): ${unknown.sort().join(", ")}`);
optionalEnum(sort, "by", ["timestamp", "total_cost_usd", "latency_ms", "total_tokens"]);
optionalEnum(sort, "dir", ["asc", "desc"]);
}
const cursor = args.cursor;
if (cursor !== undefined) {
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) throw new Error("cursor must be an object");
const unknown = Object.keys(cursor).filter((key) => key !== "sort_value" && key !== "request_id" && key !== "group_key");
if (unknown.length > 0) throw new Error(`cursor has unknown key(s): ${unknown.sort().join(", ")}`);
if (typeof cursor.sort_value !== "string" || cursor.sort_value === "") throw new Error("cursor.sort_value is required");
for (const key of ["request_id", "group_key"]) {
if (cursor[key] !== undefined && typeof cursor[key] !== "string") throw new Error(`cursor.${key} must be a string`);
}
}
}
async function callTool(client: AgentMcpClient, name: string, rawArgs: unknown): Promise<ToolResult> {
const started = Date.now();
let outcome: "ok" | "error" = "ok";
try {
const args = objectArg(rawArgs);
const allowed = TOOL_ARGUMENT_KEYS[name];
const unknown = Object.keys(args).filter((key) => !allowed?.has(key));
if (unknown.length > 0) throw new Error(`unknown argument(s): ${unknown.sort().join(", ")}`);
let value: JSONValue;
switch (name) {
case "caveman_context": {
const [identity, projects, selectedProjectId] = await Promise.all([
client.request("/api/v1/auth/me"),
client.request("/api/v1/projects"),View on GitHub (pinned to 3ee70a1026)