{"record":{"id":"e8f1ec83c121a102","repo":"can1357/oh-my-pi","slug":"rpc-message-pagination-repeated-a-cursor-e8f1ec","errorCode":null,"errorMessage":"RPC message pagination repeated a cursor","messagePattern":"RPC message pagination repeated a cursor","errorType":"exception","errorClass":"RpcError","httpStatus":null,"severity":"error","filePath":"python/omp-rpc/src/omp_rpc/client.py","lineNumber":1044,"sourceCode":"                seen_cursors: set[str] = set()\n                total_messages: int | None = None\n                cursor: str | None = None\n                while True:\n                    page = self.get_messages_page(cursor=cursor, limit=256)\n                    if (\n                        total_messages is not None\n                        and page.total_messages != total_messages\n                    ):\n                        raise RpcError(\n                            \"RPC message pagination returned an inconsistent total\"\n                        )\n                    total_messages = page.total_messages\n                    messages.extend(page.messages)\n                    cursor = page.next_cursor\n                    if cursor is None:\n                        break\n                    if cursor in seen_cursors:\n                        raise RpcError(\"RPC message pagination repeated a cursor\")\n                    seen_cursors.add(cursor)\n                if len(messages) != total_messages:\n                    raise RpcError(\n                        \"RPC message pagination ended before the advertised total\"\n                    )\n                return tuple(messages)\n            except RpcCommandError as error:\n                if error.command != \"get_messages_page\" or not (\n                    error.code in _RPC_MESSAGES_PAGE_FALLBACK_CODES\n                    or error.error\n                    in (\n                        _RPC_MESSAGES_PAGE_BUSY_ERROR,\n                        _RPC_MESSAGES_PAGE_STALE_ERROR,\n                    )\n                ):\n                    raise\n        payload = self._request(\"get_messages\")\n        return parse_agent_messages(cast(JsonValue | None, payload.get(\"messages\")))","sourceCodeStart":1026,"sourceCodeEnd":1062,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/omp-rpc/src/omp_rpc/client.py#L1026-L1062","documentation":"The pagination loop tracks seen_cursors to detect non-converging iteration. If get_messages_page returns a next_cursor that was already visited, the loop would otherwise spin forever, so the client raises this RpcError. It indicates the server's cursor is not advancing (or is cycling) between pages.","triggerScenarios":"Server returns the same next_cursor for consecutive get_messages_page calls — e.g. a buggy or mocked get_messages_page, a cursor invalidated by concurrent message mutation, or a server version whose cursor encoding changed.","commonSituations":"Older server build with a broken pagination cursor; custom/stub server that always echoes the same cursor; session state rewritten mid-pagination so the cursor points back to an earlier offset.","solutions":["Upgrade the omp server to a build with correct cursor advancement","Retry full pagination once the session is idle (concurrent mutation can invalidate cursors)","If testing, make the mock server advance the cursor monotonically and terminate with nextCursor=None","Catch RpcError and fall back to the non-paginated messages fetch if one exists"],"exampleFix":"# before: stub server always returns the same cursor\nreturn {\"messages\": page, \"totalMessages\": n, \"nextCursor\": \"abc\"}  # loops forever\n\n# after: advance or terminate the cursor\ncursor = None if at_end else encode_offset(offset + len(page))\nreturn {\"messages\": page, \"totalMessages\": n, \"nextCursor\": cursor}","handlingStrategy":"retry","validationCode":"# basic server sanity check before paginating\npage = client.get_messages_page(cursor=None, limit=1)\n# a healthy server must terminate or advance; mocks must never echo cursors\n","typeGuard":"def cursor_advances(page, seen: set) -> bool:\n    return page.next_cursor is None or page.next_cursor not in seen\n","tryCatchPattern":"try:\n    messages = client.get_all_messages()\nexcept RpcError as e:\n    if \"repeated a cursor\" in str(e):\n        messages = retry_pagination_or_fallback()  # restart once session idle\n    else:\n        raise\n","preventionTips":["Keep server and client versions aligned (cursor bugs are usually server-side)","Make test doubles advance cursors monotonically and end with nextCursor=None","Don't mutate the session while paginating","Bound retries so a persistently broken server fails fast"],"tags":["rpc","pagination","infinite-loop","cursor"],"backgroundTag":"pagination-repeated-cursor","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}