openai/codex · error · std::io::Error
BrokenPipe
BrokenPipe
Error message
unknown process
What it means
ExecutorProcessTransport.send serializes an MCP message and writes it to a managed child process via a process registry. When the registry answers WriteStatus::UnknownProcess - the targeted process ID is not currently registered - send maps it to ErrorKind::BrokenPipe with message 'unknown process', signalling the write targeted a process the registry no longer knows.
Source
Thrown at codex-rs/rmcp-client/src/executor_process_transport.rs:253
&mut self,
item: TxJsonRpcMessage<RoleClient>,
) -> impl Future<Output = std::result::Result<(), Self::Error>> + Send + 'static {
let process = Arc::clone(&self.process);
let stdin_write_semaphore = Arc::clone(&self.stdin_write_semaphore);
async move {
let _stdin_write_permit = stdin_write_semaphore
.acquire()
.await
.map_err(io::Error::other)?;
// rmcp hands us a structured JSON-RPC message. Stdio transport on
// the wire is JSON plus one newline delimiter.
let mut bytes = to_vec(&item).map_err(io::Error::other)?;
bytes.push(b'\n');
let response = process.write(bytes).await.map_err(io::Error::other)?;
match response.status {
WriteStatus::Accepted => Ok(()),
WriteStatus::UnknownProcess => {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "unknown process"))
}
WriteStatus::StdinClosed => {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "stdin closed"))
}
WriteStatus::Starting => Err(io::Error::new(
io::ErrorKind::WouldBlock,
"process is starting",
)),
}
}
}
fn receive(&mut self) -> impl Future<Output = Option<RxJsonRpcMessage<RoleClient>>> + Send {
self.receive_message()
}
async fn close(&mut self) -> std::result::Result<(), Self::Error> {
self.process.terminate().await.map_err(io::Error::other)?;View on GitHub (pinned to 339751715c)
Solutions
- Treat BrokenPipe 'unknown process' as terminal for the transport: drop it and build a new one bound to the current process
- Re-resolve the process ID from the registry before retrying the send
- Do not cache transports across restarts - re-create them on process lifecycle events
Example fix
// before
transport.send(item).await?; // BrokenPipe: unknown process
// after
match transport.send(item).await {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
let transport = rebuild_transport_for_current_process().await?;
transport.send(item).await?;
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: fallback
Validate before calling
// if the registry is reachable, confirm the target exists before sending:
if !registry.contains(process_id) {
return Err(stale_transport(process_id));
} Try / catch
match transport.send(item).await {
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe
&& e.to_string().contains("unknown process") =>
{
// stale transport: rebuild against the live registry and resend once
}
other => other,
} Prevention
- Re-create transports whenever the underlying process restarts
- Subscribe to process lifecycle events instead of discovering staleness via failed sends
- Keep MCP requests idempotent so one resend after rebuild is safe
When it happens
Trigger: Calling send() on a transport after the target process was removed from (or never registered in) the registry: the process restarted with a new ID, the registry was cleared, or a stale transport handle was retained across a reconnect.
Common situations: Caching an ExecutorProcessTransport across a child restart; races between process teardown and in-flight sends; reconnect logic that reuses the pre-restart transport.
Related errors
- invalid requirement for MCP server `{server_name}` (set by {
- InvalidData
- failed to read MCP config for selected plugin `{plugin_id}`
- failed to resolve MCP config path `{relative_path}` below se
- failed to parse MCP config for selected plugin `{plugin_id}`
AI-assisted analysis of openai/codex@339751715c (2026-08-25).
Data as JSON: /api/errors/862e62ea6e8af36f.
Report an issue: GitHub.