xai-org/grok-build · error · anyhow::Error

PTY write channel closed

Error message

PTY write channel closed

What it means

Session::send_bytes sends raw input to the PTY through a tokio mpsc channel (pty_write_tx) consumed by the writer task. This error is thrown when that send fails, meaning the receiver end is dropped — the PTY's write task (or the whole session runtime) has shut down, so keystrokes can no longer be delivered.

Source

Thrown at crates/codegen/ptyctl/src/session.rs:219

            generation_rx,
            generation_tx: generation_tx_weak,
            raw_tail,
            _shutdown_tx: Some(shutdown_tx),
            output_tx,
        })
    }

    /// Send keystrokes using vim notation (e.g. `"<C-c>"`, `"hello<CR>"`).
    pub async fn send_keys(&self, notation: &str) -> Result<()> {
        let bytes = keys::parse_keys(notation)?;
        self.send_bytes(&bytes).await
    }

    /// Send raw bytes to the PTY.
    pub async fn send_bytes(&self, bytes: &[u8]) -> Result<()> {
        self.pty_write_tx
            .send(bytes.to_vec())
            .map_err(|_| anyhow::anyhow!("PTY write channel closed"))
    }

    /// Read screen content as plain text.
    pub async fn screen(&self, opts: &ScreenOpts) -> ScreenOutput {
        let term = self.terminal.lock().await;
        term.screen_content(opts)
    }

    /// Read screen content with style information.
    pub async fn screen_styled(&self, opts: &ScreenOpts) -> Vec<StyledLine> {
        let term = self.terminal.lock().await;
        term.screen_styled(opts)
    }

    /// Read screen content as HTML.
    pub async fn screen_html(&self, opts: &ScreenOpts) -> String {
        let term = self.terminal.lock().await;
        term.screen_html(opts)

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Check the PTY/session is still alive before writing; treat this error as 'session ended' and stop sending input
  2. Track child-process exit (or a session-closed signal) and close your input loop when it fires
  3. If you need the session, recreate it (spawn a new PTY session) instead of writing to the dead one
  4. Retry is pointless for this error — the channel never reopens; handle it as terminal in your error path

Example fix

// before
for key in keys { session.send_keys(key).await?; } // may fail if PTY exited
// after
if !session.is_alive().await { return Err(anyhow!("session ended before input was sent")); }
for key in keys { session.send_keys(key).await?; }
Defensive patterns

Strategy: try-catch

Validate before calling

async fn can_send(session: &Session) -> bool {
    !session.is_closed() // or equivalent liveness check exposed by the session
}

Try / catch

match session.send_bytes(input).await {
    Err(e) if e.to_string() == "PTY write channel closed" => {
        // session terminated; stop the input loop and finalize
        tracing::info!("pty ended, stopping input");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling send_bytes (directly or via send_keys) after the PTY process exited or the session was closed/dropped, causing the writer task that owns pty_write_rx to finish and drop the receiver.

Common situations: A long-running automation script that keeps typing after the shell/command exited; races where a `wait`/kill on the PTY process completes just before a final write; sending input to a session ID that was already terminated elsewhere.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/462a1ffa5a04e81e. Report an issue: GitHub.