can1357/oh-my-pi · error · RpcMessagesPageError
stale_cursor
stale_cursor
Error message
stale_cursor
What it means
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.
Source
Thrown at packages/coding-agent/src/modes/rpc/rpc-messages.ts:107
);
}
/** Page one stable in-memory message snapshot without crossing the v1 frame budget. */
export function pageRpcMessages(
messages: readonly AgentMessage[],
snapshot: RpcMessageSnapshot,
options: RpcMessagesPageOptions = {},
): RpcMessagesPage {
if (snapshot.messageCount !== messages.length)
throw new Error("RPC message snapshot does not match current messages");
const limit = options.limit ?? DEFAULT_RPC_MESSAGE_PAGE_LIMIT;
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_RPC_MESSAGE_PAGE_LIMIT)
throw new Error(`RPC message page limit must be between 1 and ${MAX_RPC_MESSAGE_PAGE_LIMIT}`);
let offset = 0;
if (options.cursor !== undefined) {
const cursor = decodeCursor(options.cursor);
if (!sameSnapshot(cursor, snapshot))
throw new RpcMessagesPageError(RPC_MESSAGES_PAGE_STALE_ERROR, "stale_cursor");
offset = cursor.offset;
}
const page: AgentMessage[] = [];
let pageBytes = 2;
while (offset + page.length < messages.length && page.length < limit) {
const message = messages[offset + page.length];
const messageBytes = Buffer.byteLength(JSON.stringify(message), "utf8") + (page.length === 0 ? 0 : 1);
if (page.length > 0 && pageBytes + messageBytes > MAX_RPC_MESSAGE_PAGE_BYTES) break;
page.push(message);
pageBytes += messageBytes;
}
const nextOffset = offset + page.length;
return {
messages: page,
...(nextOffset < messages.length ? { nextCursor: encodeCursor(snapshot, nextOffset) } : {}),
totalMessages: messages.length,View on GitHub (pinned to 9690622007)
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.
Example fix
// before
const page = await rpc.pageRpcMessages({ cursor: savedCursor, limit: 50 });
// after
let page;
try {
page = await rpc.pageRpcMessages({ cursor: savedCursor, limit: 50 });
} catch (err) {
if (isRpcMessagesPageError(err) && err.code === "stale_cursor") {
savedCursor = undefined;
page = await rpc.pageRpcMessages({ limit: 50 });
} else throw err;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (cursor && !cursorBelongsToCurrentSnapshot) cursor = undefined; // or simply always re-fetch a fresh cursor after message-list changes
Type guard
function isStaleCursorError(err: unknown): err is RpcMessagesPageError {
return err instanceof RpcMessagesPageError && err.code === "stale_cursor";
} Try / catch
try {
page = await pageRpcMessages({ cursor, limit });
} catch (err) {
if (isStaleCursorError(err)) {
cursor = undefined;
page = await pageRpcMessages({ cursor, limit });
} else throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- RPC message pagination repeated a cursor
- Invalid RPC message cursor
- RPC message pagination repeated a cursor
- Cursor blob not found
- RPC message pagination returned an inconsistent total
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/fd08486425fc22f9.
Report an issue: GitHub.