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

  1. Retry the handoff/detach after the child catches up, or reduce pending input before detaching
  2. Increase HANDOFF_DRAIN_TIMEOUT if large pastes are a supported workflow
  3. Check whether the child process is hung (not reading stdin) and kill/restart it before handing off
  4. 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

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

Related errors


AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28). Data as JSON: /api/errors/b87a8d0f7ae0d484. Report an issue: GitHub.