github/copilot-sdk · error

writer actor dropped ack without responding

Error message

writer actor dropped ack without responding

What it means

After write enqueues a frame, it awaits a oneshot ack from the writer actor confirming the bytes were flushed. If the writer task is dropped before sending on that ack channel (task aborted, panic, or shutdown race), the recv returns RecvError and the library reports it as a BrokenPipe, since the write outcome is unknowable.

Solutions

  1. Treat as a connection-loss signal: reconnect and re-send the request (it may or may not have been delivered)
  2. Avoid dropping/shutting down the runtime while writes are pending; use graceful shutdown with ack draining
  3. Keep the Client alive until all awaited writes resolve
  4. Enable debug logging on the writer task to find the earlier error that ended the actor

Example fix

// before
let result = client.write(frame).await; // BrokenPipe: writer dropped ack
// after
match client.write(frame).await {
    Err(e) if e.to_string().contains("writer actor") => {
        client = reconnect().await?;
        client.write(frame).await?;
    }
    r => r?,
}
Defensive patterns

Strategy: retry

Try / catch

// Rust
match client.write(frame).await {
    Err(e) if e.to_string().contains("dropped ack") => {
        // outcome unknown: reconnect and resend idempotent requests only
        client = Client::connect(endpoint).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: The writer actor task is dropped between receiving the WriteCommand and replying on the ack oneshot — typically when the runtime is shutting down or the actor loop exits on a prior flush error while a write is in flight.

Common situations: Cancelling/shutting down the tokio runtime while a request is mid-flight; a panic or early return in the writer loop; dropping the Client without draining pending writes.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/d9d1726d9a94e056. Report an issue: GitHub.

Appendix: source

Thrown at rust/src/jsonrpc.rs:672

        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 the
/// happy path so the read loop's response handling owns the cleanup.
struct PendingGuard<'a> {
    map: &'a RwLock<HashMap<u64, PendingRequest>>,
    id: u64,
    armed: bool,
}

impl PendingGuard<'_> {
    fn disarm(&mut self) {

View on GitHub (pinned to cd8cf15dc3)