{"record":{"id":"8d7d6cf82a2cb60c","repo":"can1357/oh-my-pi","slug":"rpc-message-snapshot-does-not-match-current-messag","errorCode":null,"errorMessage":"RPC message snapshot does not match current messages","messagePattern":"RPC message snapshot does not match current messages","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/modes/rpc/rpc-messages.ts","lineNumber":99,"sourceCode":"\treturn { version, sessionId, leafId, messageCount, offset };\n}\n\nfunction sameSnapshot(cursor: RpcMessageCursorPayload, snapshot: RpcMessageSnapshot): boolean {\n\treturn (\n\t\tcursor.sessionId === snapshot.sessionId &&\n\t\tcursor.leafId === snapshot.leafId &&\n\t\tcursor.messageCount === snapshot.messageCount\n\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);","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/modes/rpc/rpc-messages.ts#L81-L117","documentation":"pageRpcMessages compares snapshot.messageCount with the actual messages array length; they must match because the cursor pins a specific stable snapshot. This error means the message list changed between when the snapshot was taken and when the page was requested, so the snapshot is no longer valid.","triggerScenarios":"Calling get_messages/page while the agent is actively appending messages so messages.length differs from the snapshot's messageCount; reusing an old snapshot with a mutated message list; concurrent requests during an agent run.","commonSituations":"A client pages a live session mid-run instead of waiting for agent_end; the caller built the snapshot once but passed a different messages array; a race between snapshot capture and paging in multi-request clients.","solutions":["Wait until the session is idle (agent_end / willContinue false) before paging messages.","Re-fetch a fresh snapshot (and discard old cursors) whenever the message count changes — cursors are snapshot-bound and not valid across snapshots.","Catch this error, refresh snapshot + messages, and retry the page request against the new snapshot.","Use the structured error handling path: this raw Error indicates a caller bug/race, unlike RpcMessagesPageError which has a wire code — fix the calling code to snapshot and page atomically."],"exampleFix":"// before\nconst snapshot = { sessionId, leafId, messageCount: messages.length };\nawait runAgent(); // messages grows\npage(messages, snapshot); // mismatch\n// after\nawait runAgent();\nconst snapshot = { sessionId, leafId, messageCount: messages.length };\npage(messages, snapshot); // snapshot taken after messages stabilized","handlingStrategy":"try-catch","validationCode":"function snapshotMatches(messages: readonly unknown[], snapshot: RpcMessageSnapshot): boolean {\n  return snapshot.messageCount === messages.length;\n}\nif (!snapshotMatches(messages, snapshot)) {\n  ({ messages, snapshot } = refreshSnapshot()); // re-capture before paging\n}","typeGuard":null,"tryCatchPattern":"try {\n  return pageRpcMessages(messages, snapshot, opts);\n} catch (err) {\n  if (err.message === \"RPC message snapshot does not match current messages\") {\n    const fresh = getFreshSnapshotAndMessages();\n    return pageRpcMessages(fresh.messages, fresh.snapshot, {}); // old cursor is void\n  }\n  throw err;\n}","preventionTips":["Page only while the session is idle (after agent_end)","Take the snapshot and call page atomically from the same messages array","Discard cursors whenever the message count changes — they are bound to one snapshot"],"tags":["rpc","pagination","snapshot","concurrency","race-condition"],"backgroundTag":"snapshot-stale-messages-changed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}