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

  1. Use only the nextCursor value returned by the previous response, or omit cursor for the first page.
  2. Restart pagination from the beginning (cursor omitted) if the dataset may have changed.
  3. Verify the cursor is the exact string returned as nextCursor (not a client-computed offset).
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/b010930b652f67db. Report an issue: GitHub.