can1357/oh-my-pi · error · RpcError

get_messages_page response has an invalid nextCursor

Error message

get_messages_page response has an invalid nextCursor

What it means

In the same _parse_messages_page validation, nextCursor must be either absent/null or a string. Any other JSON type (number, object, bool) raises this RpcError. The cursor is echoed back verbatim on the next get_messages_page call, so a non-string cursor would fail downstream request construction.

Source

Thrown at python/omp-rpc/src/omp_rpc/client.py:1077

                ):
                    raise
        payload = self._request("get_messages")
        return parse_agent_messages(cast(JsonValue | None, payload.get("messages")))

    def get_messages_page(
        self, *, cursor: str | None = None, limit: int | None = None
    ) -> MessagesPage:
        payload = self._request("get_messages_page", cursor=cursor, limit=limit)
        raw_total = payload.get("totalMessages")
        if (
            not isinstance(raw_total, int)
            or isinstance(raw_total, bool)
            or raw_total < 0
        ):
            raise RpcError("get_messages_page response has an invalid totalMessages")
        raw_cursor = payload.get("nextCursor")
        if raw_cursor is not None and not isinstance(raw_cursor, str):
            raise RpcError("get_messages_page response has an invalid nextCursor")
        return MessagesPage(
            messages=parse_agent_messages(
                cast(JsonValue | None, payload.get("messages"))
            ),
            total_messages=raw_total,
            next_cursor=raw_cursor,
        )

    def set_custom_tools(self, tools: Sequence[HostTool[Any, Any]]) -> tuple[str, ...]:
        self._custom_tools = tuple(tools)
        if self._process is None:
            return tuple(tool.name for tool in self._custom_tools)

        payload = self._request(
            "set_host_tools",
            tools=cast(
                JsonValue,
                [

View on GitHub (pinned to 9690622007)

Solutions

  1. Upgrade server and client to matching versions so the cursor type agrees
  2. In mocks/stubs, emit nextCursor as a string (or omit it / use null at the end)
  3. Coerce the server-side cursor to a string before responding
  4. Log the raw payload to confirm the offending cursor type

Example fix

# before: numeric cursor
return {"messages": msgs, "totalMessages": 10, "nextCursor": 42}

# after: opaque string cursor
return {"messages": msgs, "totalMessages": 10, "nextCursor": str(offset)}
Defensive patterns

Strategy: type-guard

Validate before calling

def valid_cursor(payload: dict) -> bool:
    c = payload.get("nextCursor")
    return c is None or isinstance(c, str)

Type guard

def is_valid_cursor(v: object) -> bool:
    return v is None or isinstance(v, str)

Try / catch

try:
    page = client.get_messages_page(cursor=cursor, limit=256)
except RpcError as e:
    if "invalid nextCursor" in str(e):
        page = None  # treat as schema drift; stop paging or upgrade server
    else:
        raise

Prevention

When it happens

Trigger: Server (or mock) returns nextCursor as a number, dict, or other non-string value; server version that encodes cursors as ints while the client expects opaque strings.

Common situations: Version skew where the server changed cursor encoding; hand-written test doubles returning raw offsets; middleware re-serializing the payload.

Related errors


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