fix(rpc): stream v2 chunk frames with backpressure and recover stale page cursors

Two fixes on top of the paged transport:

- Near-limit v2 framing no longer materializes the full base64 transport:
  chunk lines are generated lazily from a single serialization, the 64 MiB
  reassembly ceiling is enforced via Buffer.byteLength before any
  full-payload allocation, and RPC stdout writes drain with backpressure
  one physical line at a time. Peak RSS for a 63 MiB response drops
  ~686 MB -> ~521 MB; a rejected 80 MiB response drops ~507 MB -> ~259 MB
  (parity with the v1 path).

- get_messages_page errors now carry a machine-readable code
  (session_busy | stale_cursor). Both bundled clients' high-level
  getMessages() drains discard partial pages and fall back to the legacy
  snapshot on either code — previously a cursor invalidated by a
  background mutation (e.g. an appended bash message) threw instead of
  falling back. Direct page calls remain strict.
This commit is contained in:
can1357
2026-07-23 12:38:13 +02:00
parent 3e09e4b1ea
commit f833810d0e
11 changed files with 210 additions and 40 deletions
+39
View File
@@ -519,6 +519,34 @@ V2_MESSAGES_SERVER = textwrap.dedent(
"command": command_type,
"success": False,
"error": "Cannot page messages while the session is changing",
"code": "session_busy",
}
)
continue
if os.environ.get("V2_MESSAGES_STALE") == "1":
if command.get("cursor") is not None:
emit(
{
"id": request_id,
"type": "response",
"command": command_type,
"success": False,
"error": "RPC message cursor is stale",
"code": "stale_cursor",
}
)
continue
emit(
{
"id": request_id,
"type": "response",
"command": command_type,
"success": True,
"data": {
"messages": [message],
"totalMessages": 2,
"nextCursor": "page-two",
},
}
)
continue
@@ -1036,6 +1064,17 @@ class RpcClientTests(unittest.TestCase):
self.assertEqual(len(messages), 1)
self.assertEqual(messages[0]["content"][0]["text"], "streaming snapshot")
def test_protocol_v2_get_messages_discards_stale_page_walk(self) -> None:
with self.make_client(
server=V2_MESSAGES_SERVER, env={"V2_MESSAGES_STALE": "1"}
) as client:
with self.assertRaisesRegex(RpcCommandError, "RPC message cursor is stale"):
client.get_messages_page(cursor="page-two")
messages = client.get_messages()
self.assertEqual(len(messages), 1)
self.assertEqual(messages[0]["content"][0]["text"], "streaming snapshot")
def test_collect_events_returns_turn_events(self) -> None:
with self.make_client() as client:
client.prompt("slow")