{"record":{"id":"fd08486425fc22f9","repo":"can1357/oh-my-pi","slug":"stale-cursor","errorCode":"stale_cursor","errorMessage":"stale_cursor","messagePattern":"stale_cursor","errorType":"error_code","errorClass":"RpcMessagesPageError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/modes/rpc/rpc-messages.ts","lineNumber":107,"sourceCode":"\t);\n}\n\n/** Page one stable in-memory message snapshot without crossing the v1 frame budget. */\nexport function pageRpcMessages(\n\tmessages: readonly AgentMessage[],\n\tsnapshot: RpcMessageSnapshot,\n\toptions: RpcMessagesPageOptions = {},\n): RpcMessagesPage {\n\tif (snapshot.messageCount !== messages.length)\n\t\tthrow new Error(\"RPC message snapshot does not match current messages\");\n\tconst limit = options.limit ?? DEFAULT_RPC_MESSAGE_PAGE_LIMIT;\n\tif (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_RPC_MESSAGE_PAGE_LIMIT)\n\t\tthrow new Error(`RPC message page limit must be between 1 and ${MAX_RPC_MESSAGE_PAGE_LIMIT}`);\n\tlet offset = 0;\n\tif (options.cursor !== undefined) {\n\t\tconst cursor = decodeCursor(options.cursor);\n\t\tif (!sameSnapshot(cursor, snapshot))\n\t\t\tthrow new RpcMessagesPageError(RPC_MESSAGES_PAGE_STALE_ERROR, \"stale_cursor\");\n\t\toffset = cursor.offset;\n\t}\n\n\tconst page: AgentMessage[] = [];\n\tlet pageBytes = 2;\n\twhile (offset + page.length < messages.length && page.length < limit) {\n\t\tconst message = messages[offset + page.length];\n\t\tconst messageBytes = Buffer.byteLength(JSON.stringify(message), \"utf8\") + (page.length === 0 ? 0 : 1);\n\t\tif (page.length > 0 && pageBytes + messageBytes > MAX_RPC_MESSAGE_PAGE_BYTES) break;\n\t\tpage.push(message);\n\t\tpageBytes += messageBytes;\n\t}\n\n\tconst nextOffset = offset + page.length;\n\treturn {\n\t\tmessages: page,\n\t\t...(nextOffset < messages.length ? { nextCursor: encodeCursor(snapshot, nextOffset) } : {}),\n\t\ttotalMessages: messages.length,","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/modes/rpc/rpc-messages.ts#L89-L125","documentation":"pageRpcMessages paginates the in-memory agent message list using an opaque cursor. The cursor encodes a snapshot fingerprint and an offset; if the caller passes a cursor that was minted against a different message snapshot (messages added or removed since), the pagination result would be meaningless, so the function throws RpcMessagesPageError with code 'stale_cursor'. This protects consumers from silently skipping or duplicating messages across pages.","triggerScenarios":"Calling pageRpcMessages with options.cursor set to a cursor returned by a previous call whose message list has since changed (new messages appended, session reloaded, or a different snapshot instance), so sameSnapshot(cursor, snapshot) returns false.","commonSituations":"A host application caches a pagination cursor across a long-lived RPC session while the agent keeps producing messages; the host resumes paging after idle time and the snapshot no longer matches. Also common when a session is restarted/reloaded and an old cursor is replayed.","solutions":["Catch RpcMessagesPageError and check the stale error code, then restart pagination from the beginning (no cursor) or from the newest page.","Keep the snapshot coherent: re-request a fresh cursor each time messages change instead of reusing long-lived cursors.","If replaying an old cursor is intentional, treat 'stale_cursor' as 'restart paging' signal rather than a fatal failure."],"exampleFix":"// before\nconst page = await rpc.pageRpcMessages({ cursor: savedCursor, limit: 50 });\n// after\nlet page;\ntry {\n  page = await rpc.pageRpcMessages({ cursor: savedCursor, limit: 50 });\n} catch (err) {\n  if (isRpcMessagesPageError(err) && err.code === \"stale_cursor\") {\n    savedCursor = undefined;\n    page = await rpc.pageRpcMessages({ limit: 50 });\n  } else throw err;\n}","handlingStrategy":"try-catch","validationCode":"if (cursor && !cursorBelongsToCurrentSnapshot) cursor = undefined; // or simply always re-fetch a fresh cursor after message-list changes","typeGuard":"function isStaleCursorError(err: unknown): err is RpcMessagesPageError {\n  return err instanceof RpcMessagesPageError && err.code === \"stale_cursor\";\n}","tryCatchPattern":"try {\n  page = await pageRpcMessages({ cursor, limit });\n} catch (err) {\n  if (isStaleCursorError(err)) {\n    cursor = undefined;\n    page = await pageRpcMessages({ cursor, limit });\n  } else throw err;\n}","preventionTips":["Invalidate cached cursors whenever you observe new messages or a session reload.","Re-mint the cursor from the latest page response each time instead of persisting it across restarts.","Design hosts to treat stale cursors as restart-from-beginning, never as fatal."],"tags":["pagination","cursor","rpc","state"],"backgroundTag":"stale-pagination-cursor","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}