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
- Check the PTY/session is still alive before writing; treat this error as 'session ended' and stop sending input
- Track child-process exit (or a session-closed signal) and close your input loop when it fires
- If you need the session, recreate it (spawn a new PTY session) instead of writing to the dead one
- 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
- Stop writing input as soon as the PTY child process exits; join the wait handle with the input loop
- Use a select! over child-exit and input channels so writes are cancelled on exit
- Treat this error as terminal — never retry send_bytes after channel closure
- Drain pending input only while the session reports alive
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
- Task panicked: {}
- blocking pool pre-warm stalled after {started} of {n} thread
- bridge spawn
- User question session ended unexpectedly (coordinator channe
- git {shown} failed in {}: {}
AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31).
Data as JSON: /api/errors/462a1ffa5a04e81e.
Report an issue: GitHub.