github/copilot-sdk · error
writer actor has shut down
Error message
writer actor has shut down
What it means
Client::write sends a serialized frame to the dedicated writer actor over a channel. If the writer task has already terminated (connection closed or being torn down), the send fails and the library converts it into a BrokenPipe io::Error so callers see a familiar std::io-style failure rather than a channel-send panic.
Solutions
- Check connection liveness and reconnect the client before retrying the write
- Ensure Client::stop is not called concurrently with in-flight writes; await pending operations first
- Handle BrokenPipe by recreating the Client (transport and writer actor) instead of reusing it
- Inspect writer task logs for an earlier underlying I/O error that killed the actor
Example fix
// before
client.write(request).await?; // panics/fails after shutdown
// after
if client.is_running() {
client.write(request).await?;
} else {
client = Client::connect(endpoint).await?;
client.write(request).await?;
} Defensive patterns
Strategy: try-catch
Validate before calling
// Rust
fn can_write(client: &Client) -> bool { client.is_running() } // or track shutdown state yourself Try / catch
// Rust
match client.write(frame).await {
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => reconnect_and_retry().await?,
other => other?,
} Prevention
- Await in-flight writes before calling Client::stop
- Recreate the Client after any BrokenPipe instead of reusing it
- Monitor connection state and reconnect proactively on peer disconnect
When it happens
Trigger: Calling any public write/RPC method on a JsonRpc client whose background writer actor has exited — e.g. after the peer closed the connection, after Client::stop, or after the writer task panicked on an underlying I/O error.
Common situations: Sending a request over a socket the server already closed; racing a write against Client::stop during shutdown; long-lived connections dropped by idle timeouts or network interruption.
Related errors
- failed to write a frame to the in-process runtime connection
- writer actor dropped ack without responding
- runtime.shutdown timed out during Client::stop
- The in-process runtime connection is closed.
- FfiRuntimeHost is already closed.
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/9b4abbdc21012f80.
Report an issue: GitHub.
Appendix: source
Thrown at rust/src/jsonrpc.rs:663
/// # Cancel safety
///
/// **Cancel-safe.** Pre-serializes the body, enqueues it on the writer
/// actor's command channel, and awaits an ack. Caller cancellation
/// drops the ack receiver; the actor still completes the frame and
/// flushes. A partial frame can never appear on the wire.
pub async fn write<T: serde::Serialize>(&self, message: &T) -> Result<(), Error> {
let body = serde_json::to_vec(message)?;
let mut frame = Vec::with_capacity(CONTENT_LENGTH_HEADER.len() + 16 + body.len() + 4);
frame.extend_from_slice(CONTENT_LENGTH_HEADER.as_bytes());
frame.extend_from_slice(body.len().to_string().as_bytes());
frame.extend_from_slice(b"\r\n\r\n");
frame.extend_from_slice(&body);
let (ack_tx, ack_rx) = oneshot::channel();
self.write_tx
.send(WriteCommand { frame, ack: ack_tx })
.map_err(|_| {
Error::from(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"writer actor has shut down",
))
})?;
match ack_rx.await {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(Error::from(e)),
Err(_) => Err(Error::from(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"writer actor dropped ack without responding",
))),
}
}
}
/// RAII guard that removes a pending-request entry from the map if the
/// owning future is dropped before the response arrives. Disarmed on theView on GitHub (pinned to cd8cf15dc3)