herdrdev/herdr · error
timed out draining PTY writes before handoff
Error message
timed out draining PTY writes before handoff
What it means
During PTY handoff, begin_handoff must flush all pending user writes to the PTY fd before quiescing. It polls the fd until pending_writes is empty or HANDOFF_DRAIN_TIMEOUT elapses; if the deadline passes with writes still pending, it returns ErrorKind::TimedOut. This guards against losing user keystrokes/commands during a handoff.
Source
Thrown at src/pty/actor/unix.rs:613
}
false
}
fn begin_handoff(&mut self) -> std::io::Result<()> {
self.drain_pre_quiesce_commands();
self.apply_pending_controls();
if self.state == ActorState::Released {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"PTY actor was released before handoff quiesce",
));
}
let deadline = Instant::now() + HANDOFF_DRAIN_TIMEOUT;
self.flush_pending_writes_once();
while !self.pending_writes.is_empty() {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"timed out draining PTY writes before handoff",
));
}
let timeout_ms = remaining.as_millis().min(i32::MAX as u128) as i32;
let readiness = fd::poll_pty_and_wake(
self.file.as_raw_fd(),
self.wake_read_fd.as_raw_fd(),
true,
true,
timeout_ms,
)?;
if readiness.wake_ready {
fd::drain_wake_fd(self.wake_read_fd.as_raw_fd())?;
}
if readiness.pty_read_ready && !self.read_once() {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,View on GitHub (pinned to f457cff4f2)
Solutions
- Retry the handoff/detach after the child catches up, or reduce pending input before detaching
- Increase HANDOFF_DRAIN_TIMEOUT if large pastes are a supported workflow
- Check whether the child process is hung (not reading stdin) and kill/restart it before handing off
- Split very large pastes into smaller chunks so the pending-write queue stays shallow
Example fix
// before
actor.begin_handoff()?; // may time out on huge pending paste
// after: drain or abort pending input before handoff
if actor.pending_writes_len() > LARGE_WRITE_THRESHOLD {
actor.discard_pending_writes_for_handoff(); // explicit data-loss decision
}
actor.begin_handoff()?; Defensive patterns
Strategy: retry
Validate before calling
if actor.pending_writes_len() > 0 { wait_for_drain(grace_period) before begin_handoff; } Try / catch
match actor.begin_handoff() {
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => retry_after_backoff_or_discard_pending(),
other => other?,
} Prevention
- Avoid detaching immediately after huge pastes
- Keep pending-write queues shallow by chunking large input
- Watch child process health before handoff
When it happens
Trigger: A large volume of pending writes (e.g. pasted megabytes of text) combined with a stalled or slow PTY consumer, so pending_writes never empties within HANDOFF_DRAIN_TIMEOUT while polling in begin_handoff.
Common situations: Detaching or migrating a session immediately after pasting a huge buffer into a program that isn't reading stdin; a hung child process; slow network-backed PTY consumers under load.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- PTY actor was released before handoff quiesce
- PTY closed while draining writes before handoff
- poll encountered invalid PTY actor fd
- poll encountered PTY fd error
- timed out reading api request
AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28).
Data as JSON: /api/errors/b87a8d0f7ae0d484.
Report an issue: GitHub.