paperclipai/paperclip · error
codex_history_invalid_cursor
codex_history_invalid_cursor
Error message
codex_history_invalid_cursor
What it means
thread/items/list supports cursor pagination; the cursor is a plain numeric offset into the computed item array. The transport validates it is a safe non-negative integer no greater than the data length and throws codex_history_invalid_cursor otherwise (null/undefined cursor is treated as 0 and is valid).
Source
Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:3373
} else {
if (params.turnId !== this.#turnId) throw new Error("codex_history_unavailable: requested turn is outside the retained runner event window");
const items = new Map<string, Record<string, unknown>>();
let observedTurn = "";
let observedStart = false;
for (const event of this.#core?.store.state.committedEvents ?? []) {
const payload = record(record(event.envelope.payload).payload);
if (event.eventType === "turn.started") observedTurn = String(payload.providerTurnId ?? payload.turnId ?? record(payload.turn).id ?? "");
if (event.eventType === "turn.started" && observedTurn === params.turnId) observedStart = true;
if (event.eventType !== "item.completed" || observedTurn !== params.turnId) continue;
const item = record(rehydrateRunnerdItemNotification(payload, this.#threadId, observedTurn).item);
if (typeof item.id === "string") items.set(item.id, { turnId: observedTurn, item });
}
if (!observedStart) throw new Error("codex_history_incomplete: requested turn start is outside the retained runner event window");
data = [...items.values()];
}
if (params.sortDirection === "desc") data.reverse();
const offset = params.cursor == null ? 0 : Number(params.cursor);
if (!Number.isSafeInteger(offset) || offset < 0 || offset > data.length) throw new Error("codex_history_invalid_cursor");
const limit = typeof params.limit === "number" ? Math.max(1, Math.min(100, params.limit)) : 100;
return { data: data.slice(offset, offset + limit), nextCursor: offset + limit < data.length ? String(offset + limit) : null };
}
if (method === "thread/read") {
if (this.#core === null) {
this.#recoveryTurnBindingPending = true;
await this.#resume();
}
// Ask the authenticated runner for its live provider snapshot rather
// than reading its filesystem. This both supports remote process owners
// and proves any identity restored after PRP event compaction before the
// checkpoint-backed thread is exposed to the driver.
const snapshot = await this.#commandResult("session.snapshot", {});
this.#confirmCheckpointProviderIdentity(
snapshot,
"authenticated session.snapshot",
);
const activeProviderTurnId =View on GitHub (pinned to 01ad858492)
Solutions
- Use only the nextCursor value returned by the previous response, or omit cursor for the first page.
- Restart pagination from the beginning (cursor omitted) if the dataset may have changed.
- Verify the cursor is the exact string returned as nextCursor (not a client-computed offset).
- Treat a persistent invalid cursor as signal to re-read thread/read and rebuild the item window.
Example fix
// before
await transport.request('thread/items/list', { threadId, turnId, cursor: '-1' });
// after
let cursor: string | null = undefined;
do {
const page = await transport.request('thread/items/list', { threadId, turnId, cursor });
cursor = page.nextCursor;
} while (cursor !== null); Defensive patterns
Strategy: validation
Validate before calling
const isValidCursor = (c: unknown, len: number): boolean => c == null || (typeof c === 'string' && /^\d+$/.test(c) && Number(c) <= len && Number.isSafeInteger(Number(c)));
Type guard
const isCursor = (c: unknown): c is string => typeof c === 'string' && /^\d+$/.test(c);
Try / catch
try {
page = await transport.request('thread/items/list', { threadId, turnId, cursor });
} catch (err) {
if ((err as Error).message === 'codex_history_invalid_cursor') {
cursor = undefined; // restart pagination
page = await transport.request('thread/items/list', { threadId, turnId });
} else throw err;
} Prevention
- Only echo back nextCursor values from prior responses
- Drop cursors when switching turnId or after reconnect
- Treat cursors as opaque strings, never parse/modify them client-side
When it happens
Trigger: Passing params.cursor as a non-numeric string, negative number, float beyond Number.isSafeInteger, or an offset greater than the current item count (e.g. a cursor from a previous, differently-sized page).
Common situations: Client sending the nextCursor from a different turn/window; cursor round-tripped through JSON and mangled; hand-constructed cursors in scripts; history changed between pages shrinking the dataset.
Understand the failure class
Background: "Invalid query parameter" / "Failed to parse value of ...": fixing bad query string parameters across APIs — this error's family across 36 libraries.
Related errors
- codex_history_read_failed
- Invalid status '${String(rawStatus)}'. Must be one of: ${PLU
- "tool" is required and must be a string
- "runContext" is required and must be an object
- "runContext" must include agentId, runId, companyId, and pro
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/b010930b652f67db.
Report an issue: GitHub.