{"record":{"id":"b9ff0691454fd6f5","repo":"can1357/oh-my-pi","slug":"invalid-rpc-message-cursor","errorCode":null,"errorMessage":"Invalid RPC message cursor","messagePattern":"Invalid RPC message cursor","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/modes/rpc/rpc-messages.ts","lineNumber":55,"sourceCode":"\ninterface RpcMessageCursorPayload extends RpcMessageSnapshot {\n\tversion: 1;\n\toffset: number;\n}\n\nexport interface RpcMessagesPageOptions {\n\tcursor?: string;\n\tlimit?: number;\n}\n\nfunction encodeCursor(snapshot: RpcMessageSnapshot, offset: number): string {\n\tconst payload: RpcMessageCursorPayload = { version: 1, ...snapshot, offset };\n\treturn Buffer.from(JSON.stringify(payload), \"utf8\").toString(\"base64url\");\n}\n\nfunction decodeCursor(cursor: string): RpcMessageCursorPayload {\n\tif (cursor.length === 0 || cursor.length > MAX_RPC_MESSAGE_CURSOR_CHARS || !/^[A-Za-z0-9_-]+$/.test(cursor))\n\t\tthrow new Error(\"Invalid RPC message cursor\");\n\tconst bytes = Buffer.from(cursor, \"base64url\");\n\tif (bytes.toString(\"base64url\") !== cursor) throw new Error(\"Invalid RPC message cursor\");\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes));\n\t} catch {\n\t\tthrow new Error(\"Invalid RPC message cursor\");\n\t}\n\tif (!isRecord(value)) throw new Error(\"Invalid RPC message cursor\");\n\tconst { version, sessionId, leafId, messageCount, offset } = value;\n\tif (\n\t\tversion !== 1 ||\n\t\ttypeof sessionId !== \"string\" ||\n\t\tsessionId.length === 0 ||\n\t\tsessionId.length > 256 ||\n\t\t!((typeof leafId === \"string\" && leafId.length > 0 && leafId.length <= 256) || leafId === null) ||\n\t\ttypeof messageCount !== \"number\" ||\n\t\t!Number.isSafeInteger(messageCount) ||","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/modes/rpc/rpc-messages.ts#L37-L73","documentation":"decodeCursor validates pagination cursors before decoding: it must be non-empty, at most 2048 chars, and match base64url alphabet [A-Za-z0-9_-]. This throw means the cursor string is empty, too long, or contains characters outside the base64url alphabet (e.g. '+' or '/' from standard base64, padding '=', or whitespace).","triggerScenarios":"Calling pageRpcMessages/get_messages_page with a cursor that is '', longer than 2048 chars, contains '=' padding, standard-base64 '+/' characters, URL-encoded characters, or surrounding whitespace/newline.","commonSituations":"Client stores the cursor in a URL and something URL-decodes or re-encodes it (base64url → base64); cursor passed through JSON form round-trip with escaping; a client fabricates or hand-truncates a cursor; cursor persisted with a trailing newline in a file.","solutions":["Pass the cursor string back verbatim exactly as returned in nextCursor — no decoding, trimming, padding, or re-encoding.","If storing in a URL, use encodeURIComponent or keep it in a path-safe context; base64url chars are already URL-safe so no transformation is needed.","Trim environment/file-read artifacts (strip whitespace/newlines only — do not alter core characters).","If the cursor came from an older session, treat it as invalid and restart pagination without a cursor (offset 0) instead of passing garbage."],"exampleFix":"// before\nconst cursor = fs.readFileSync(path, 'utf8'); // may include trailing \\n\nawait rpc.page({ cursor });\n// after\nconst cursor = fs.readFileSync(path, 'utf8').trim();\nif (cursor && /^[A-Za-z0-9_-]+$/.test(cursor)) await rpc.page({ cursor });","handlingStrategy":"validation","validationCode":"function isPlausibleCursor(c: unknown): c is string {\n  return typeof c === \"string\" && c.length > 0 && c.length <= 2048 && /^[A-Za-z0-9_-]+$/.test(c);\n}\nif (cursor !== undefined && !isPlausibleCursor(cursor)) cursor = undefined; // restart from page 1","typeGuard":"function isCursorShape(v: unknown): v is string {\n  return typeof v === \"string\" && /^[A-Za-z0-9_-]{1,2048}$/.test(v);\n}","tryCatchPattern":"try {\n  return pageRpcMessages(messages, snapshot, { cursor });\n} catch (err) {\n  if (err.message === \"Invalid RPC message cursor\") {\n    return pageRpcMessages(messages, snapshot, {}); // restart pagination\n  }\n  throw err;\n}","preventionTips":["Pass nextCursor back verbatim; never decode/trim/re-encode it","base64url is already URL-safe — do not apply additional URL decoding","Strip only surrounding whitespace when reading cursors from files or storage","Validate persisted cursors on load and fall back to page 1 if malformed"],"tags":["rpc","pagination","cursor","validation"],"backgroundTag":"invalid-pagination-cursor","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}