-
Notifications
You must be signed in to change notification settings - Fork 35
Description
Summary
The JSON-RPC client in copilot/jsonrpc.py fails to read large responses (>64KB) because _read_message() assumes read(n) always returns exactly n bytes. On Unix pipes, this is not guaranteed—the kernel may return fewer bytes ("short read"), especially when data exceeds the pipe buffer size.
Reproduction
Send a prompt with ~35KB+ of context. The response (~72KB with context echo) exceeds the 64KB pipe buffer:
import asyncio
from copilot import CopilotClient
async def test():
client = CopilotClient()
await client.start()
session = await client.create_session({
"model": "claude-sonnet-4.5",
"streaming": False,
"available_tools": [],
})
# 35KB context triggers the bug
context = "Status update: work in progress. " * 1000 # ~35KB
await session.send({"prompt": f"Summarize: {context}"})
# ... wait for response
asyncio.run(test())Error:
JSON-RPC read loop error: Unterminated string starting at: line 1 column 36026 (char 36025)
Root Cause
In copilot/jsonrpc.py, the _read_message() method:
content_length = int(header.split(":")[1].strip())
# ...
content_bytes = self.process.stdout.read(content_length) # BUG: assumes full read
content = content_bytes.decode("utf-8")
return json.loads(content)When content_length exceeds the pipe buffer (64KB on macOS, varies on Linux), read() returns only what is currently buffered. The truncated bytes fail JSON parsing.
Evidence
Diagnostic testing shows the exact behavior:
| Context Size | Expected Response | Received | Missing |
|---|---|---|---|
| 35KB | 72,094 bytes | 65,513 bytes | 9.1% |
| 40KB | 82,258 bytes | 65,514 bytes | 20.4% |
| 45KB | 92,554 bytes | 65,512 bytes | 29.2% |
Note: Received bytes are consistently ~65,512 (≈64KB), the macOS pipe buffer limit.
Suggested Fix
Read in a loop until all expected bytes are received:
def _read_exact(self, num_bytes: int) -> bytes:
"""Read exactly num_bytes, handling partial/short reads from pipes."""
chunks = []
remaining = num_bytes
while remaining > 0:
chunk = self.process.stdout.read(remaining)
if not chunk:
raise EOFError("Unexpected end of stream while reading JSON-RPC message")
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)
def _read_message(self) -> Optional[dict]:
# ... existing header parsing ...
content_bytes = self._read_exact(content_length) # Use loop-based read
# ... rest unchanged ...Environment
- OS: macOS 14.x (also affects Linux with different buffer sizes)
- Python: 3.11
- SDK version: Latest from pip
Workaround
Limit context size to ~30KB to keep responses under 64KB.
References
- POSIX
read()specification: reads from pipes may return fewer bytes than requested - macOS pipe buffer: 64KB (
PIPE_SIZEin XNU kernel) - Linux pipe buffer: typically 64KB (configurable via
fcntl(F_SETPIPE_SZ))