JuliusBrussee/caveman · error
cursor.sort_value is required
Error message
cursor.sort_value is required
What it means
Thrown by validateTraceSearch() in agent-mcp.ts when a cursor object is supplied for caveman_trace_search but its sort_value key is missing, not a string, or an empty string. sort_value is the only required cursor field — it anchors keyset pagination at the last row of the previous page.
Source
Thrown at packages/cli/src/agent-mcp.ts:532
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;
switch (name) {
case "caveman_context": {
const [identity, projects, selectedProjectId] = await Promise.all([View on GitHub (pinned to 766dce6b13)
Solutions
- Use the server-provided next_cursor object from the previous response instead of building one
- Treat a null/absent next_cursor as 'no more pages' and stop paging
- If constructing manually, always include a non-empty string sort_value
Example fix
// before
if (resp.next_cursor !== null) args.cursor = {};
// after
if (resp.next_cursor !== null) args.cursor = resp.next_cursor;
else done = true; Defensive patterns
Strategy: validation
Validate before calling
const okCursor = (c: unknown) =>
c === undefined ||
(typeof c === "object" && c !== null && typeof (c as any).sort_value === "string" && (c as any).sort_value !== "");
if (!okCursor(args.cursor)) throw new Error("cursor.sort_value missing — request the first page without a cursor"); Type guard
const isKeysetCursor = (v: unknown): v is { sort_value: string; request_id?: string; group_key?: string } =>
typeof v === "object" && v !== null && typeof (v as any).sort_value === "string" && (v as any).sort_value !== ""; Try / catch
if (res.isError && res.content?.[0]?.text?.includes("cursor.sort_value is required")) {
done = true; // treat as end-of-results instead of retrying
} Prevention
- Only ever send the next_cursor object returned by the previous response
- Treat a null next_cursor as 'no more pages'
- Never hand-build cursors from row data
When it happens
Trigger: cursor: { request_id: "req_9" } (sort_value omitted), cursor: { sort_value: "" }, or a hand-built cursor where sort_value was taken from a null field of the last row. Note sort_value is compared as a string — numeric sort values must be stringified by whatever produced the cursor.
Common situations: An agent reconstructs a cursor manually from a row it inspected instead of using next_cursor; the previous page was the last page and next_cursor was null, which the caller wraps as an empty object; a serialization step drops empty-string values.
Related errors
- page_size must be an integer from 1 to 500
- cursor must be an object
- filter monitor.id is required
- filter monitor.verdict must be pass, fail, or error
- ${key} must be one of ${values.join(", ")}
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/e93067f0cdff2b79.
Report an issue: GitHub.