From f0e49c2b49b4299de094c3bc82ecfbdd45f17c88 Mon Sep 17 00:00:00 2001 From: Jason Robert Date: Thu, 5 Mar 2026 21:30:58 -0500 Subject: [PATCH] fix(copilot): clear O_NONBLOCK on subprocess pipes to prevent BlockingIOError Large JSON-RPC messages (e.g., prompts with many gathered articles) can exceed the OS pipe buffer. When the asyncio event loop sets O_NONBLOCK on inherited file descriptors, writes raise BlockingIOError instead of blocking until the reader drains the pipe. This adds a post-start fixup that clears O_NONBLOCK on stdin/stdout of the Copilot CLI subprocess, since the SDK already runs writes in a thread-pool executor where blocking is safe and correct. Co-Authored-By: Claude Opus 4.6 --- src/conductor/providers/copilot.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/conductor/providers/copilot.py b/src/conductor/providers/copilot.py index 12cbef7..213e7eb 100644 --- a/src/conductor/providers/copilot.py +++ b/src/conductor/providers/copilot.py @@ -1373,6 +1373,39 @@ async def _ensure_client_started(self) -> None: await self._client.start() self._started = True + # Ensure subprocess pipes are in blocking mode to prevent + # BlockingIOError on large payloads. The asyncio event loop + # may set O_NONBLOCK on inherited file descriptors. + self._fix_pipe_blocking_mode() + + def _fix_pipe_blocking_mode(self) -> None: + """Clear O_NONBLOCK on the Copilot CLI subprocess pipes. + + Large JSON-RPC messages (e.g., prompts with many gathered articles) + can exceed the OS pipe buffer. When O_NONBLOCK is set, writes raise + BlockingIOError instead of blocking until the reader drains the pipe. + Since the SDK already runs writes in a thread-pool executor, blocking + is safe and correct here. + """ + import fcntl + import os + + process = getattr(self._client, "_process", None) + if not process: + return + + for name, stream in [("stdin", process.stdin), ("stdout", process.stdout)]: + if stream is None: + continue + try: + fd = stream.fileno() + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + if flags & os.O_NONBLOCK: + fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + logger.debug(f"Cleared O_NONBLOCK on Copilot CLI {name}") + except (OSError, ValueError): + pass # fd may already be closed or invalid + def _calculate_delay(self, attempt: int, config: RetryConfig) -> float: """Calculate delay with exponential backoff and jitter.