JuliusBrussee/caveman · error
cursor must be an object
Error message
cursor must be an object
What it means
The optional `cursor` argument for trace_search pagination must be a plain object. If cursor is supplied as a non-object (string token, number, null, array), this error is thrown before the object's keys are inspected.
Solutions
- Pass the cursor object exactly as returned by the previous page's response (with sort_value/request_id/group_key).
- Omit `cursor` entirely for the first page instead of sending null.
- If you have a string token from elsewhere, it won't work here — construct the object form {"sort_value":"..."}.
- Ensure the cursor object is not an array and its keys are only sort_value, request_id, group_key.
Example fix
// before
{ "cursor": "abc123token" }
// after
{ "cursor": { "sort_value": "2025-01-01T00:00:00Z", "request_id": "req_1" } } Defensive patterns
Strategy: validation
Validate before calling
if (args.cursor !== undefined && (typeof args.cursor !== "object" || args.cursor === null || Array.isArray(args.cursor)))
throw new Error("cursor must be the object returned by the previous page, not a string token"); Type guard
function isCursorObject(c) {
return typeof c === "object" && c !== null && !Array.isArray(c) && typeof c.sort_value === "string";
} Try / catch
try {
await traceSearch(args);
} catch (e) {
if (e.message === "cursor must be an object") {
throw new Error("Pass the cursor object from the previous response, or omit cursor for page 1");
}
throw e;
} Prevention
- Echo the previous response's cursor object verbatim; don't stringify it.
- Initialize cursor to undefined, not null, in client state.
- Don't reuse cursor tokens from other paginated APIs.
When it happens
Trigger: trace_search with {"cursor":"eyJvZmZzZXQi...} (opaque token string from another API), {"cursor":0}, {"cursor":[...]}, or {"cursor":null} instead of omitting it.
Common situations: Reusing cursor tokens from a different paginated API that uses string cursors; JSON round-trips that turned the object into a string; client code that always initializes cursor to null and sends it.
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 has unknown key(s)
- must be a string
- page_size must be an integer from 1 to 500
- sort must be an object
- cursor. must be a string
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/c0f362784990e71e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/agent-mcp.ts:529
if (value !== undefined && typeof value !== "string") throw new Error(`${key} must be a string`);
}
optionalEnum(args, "date_field", ["occurred", "updated"]);
optionalEnum(args, "group_by", ["session", "workflow", "model", "member"]);
const pageSize = args.page_size;
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;View on GitHub (pinned to 3ee70a1026)