{"record":{"id":"b010930b652f67db","repo":"paperclipai/paperclip","slug":"codex-history-invalid-cursor","errorCode":"codex_history_invalid_cursor","errorMessage":"codex_history_invalid_cursor","messagePattern":"codex_history_invalid_cursor","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/live/runnerd-codex-transport.ts","lineNumber":3373,"sourceCode":"      } else {\n        if (params.turnId !== this.#turnId) throw new Error(\"codex_history_unavailable: requested turn is outside the retained runner event window\");\n        const items = new Map<string, Record<string, unknown>>();\n        let observedTurn = \"\";\n        let observedStart = false;\n        for (const event of this.#core?.store.state.committedEvents ?? []) {\n          const payload = record(record(event.envelope.payload).payload);\n          if (event.eventType === \"turn.started\") observedTurn = String(payload.providerTurnId ?? payload.turnId ?? record(payload.turn).id ?? \"\");\n          if (event.eventType === \"turn.started\" && observedTurn === params.turnId) observedStart = true;\n          if (event.eventType !== \"item.completed\" || observedTurn !== params.turnId) continue;\n          const item = record(rehydrateRunnerdItemNotification(payload, this.#threadId, observedTurn).item);\n          if (typeof item.id === \"string\") items.set(item.id, { turnId: observedTurn, item });\n        }\n        if (!observedStart) throw new Error(\"codex_history_incomplete: requested turn start is outside the retained runner event window\");\n        data = [...items.values()];\n      }\n      if (params.sortDirection === \"desc\") data.reverse();\n      const offset = params.cursor == null ? 0 : Number(params.cursor);\n      if (!Number.isSafeInteger(offset) || offset < 0 || offset > data.length) throw new Error(\"codex_history_invalid_cursor\");\n      const limit = typeof params.limit === \"number\" ? Math.max(1, Math.min(100, params.limit)) : 100;\n      return { data: data.slice(offset, offset + limit), nextCursor: offset + limit < data.length ? String(offset + limit) : null };\n    }\n    if (method === \"thread/read\") {\n      if (this.#core === null) {\n        this.#recoveryTurnBindingPending = true;\n        await this.#resume();\n      }\n      // Ask the authenticated runner for its live provider snapshot rather\n      // than reading its filesystem. This both supports remote process owners\n      // and proves any identity restored after PRP event compaction before the\n      // checkpoint-backed thread is exposed to the driver.\n      const snapshot = await this.#commandResult(\"session.snapshot\", {});\n      this.#confirmCheckpointProviderIdentity(\n        snapshot,\n        \"authenticated session.snapshot\",\n      );\n      const activeProviderTurnId =","sourceCodeStart":3355,"sourceCodeEnd":3391,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/live/runnerd-codex-transport.ts#L3355-L3391","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nawait transport.request('thread/items/list', { threadId, turnId, cursor: '-1' });\n// after\nlet cursor: string | null = undefined;\ndo {\n  const page = await transport.request('thread/items/list', { threadId, turnId, cursor });\n  cursor = page.nextCursor;\n} while (cursor !== null);","handlingStrategy":"validation","validationCode":"const isValidCursor = (c: unknown, len: number): boolean =>\n  c == null || (typeof c === 'string' && /^\\d+$/.test(c) && Number(c) <= len && Number.isSafeInteger(Number(c)));","typeGuard":"const isCursor = (c: unknown): c is string => typeof c === 'string' && /^\\d+$/.test(c);","tryCatchPattern":"try {\n  page = await transport.request('thread/items/list', { threadId, turnId, cursor });\n} catch (err) {\n  if ((err as Error).message === 'codex_history_invalid_cursor') {\n    cursor = undefined; // restart pagination\n    page = await transport.request('thread/items/list', { threadId, turnId });\n  } else throw err;\n}","preventionTips":["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"],"tags":["pagination","cursor","validation","history"],"backgroundTag":"invalid-query-parameter","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}