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

  1. Catch RpcMessagesPageError and check the stale error code, then restart pagination from the beginning (no cursor) or from the newest page.
  2. Keep the snapshot coherent: re-request a fresh cursor each time messages change instead of reusing long-lived cursors.
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/fd08486425fc22f9. Report an issue: GitHub.