herdrdev/herdr · error
PTY actor was released before handoff quiesce
Error message
PTY actor was released before handoff quiesce
What it means
begin_handoff drains pending control commands and applies pending controls before quiescing the PTY for handoff; if the actor's state is already ActorState::Released at that point, it refuses to continue with ErrorKind::BrokenPipe. This indicates the PTY was released (peer gone/closed) before the handoff could start. It prevents handing off a dead actor.
Source
Thrown at src/pty/actor/unix.rs:603
};
let _ = reply.send(result);
}
PtyIoControlCommand::ReleaseAfterCommit(reply) => {
self.state = ActorState::Released;
self.pending_writes.clear();
let _ = reply.send(Ok(()));
return true;
}
PtyIoControlCommand::Shutdown => return true,
}
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(),View on GitHub (pinned to f457cff4f2)
Solutions
- Treat this as a benign terminal-close race: check whether the pane/process already exited and handle Released as 'nothing to hand off'
- Ensure handoff is initiated before the actor receives a Release control command (ordering of detach vs close)
- Reproduce with logs of control-command ordering to find who sent Release first, and fix the caller sequencing
- If the PTY legitimately died, propagate shutdown instead of retrying handoff
Example fix
// before
match actor.begin_handoff() {
Ok(()) => {},
Err(e) => return Err(e), // crashes detach flow on released actor
}
// after
match actor.begin_handoff() {
Ok(()) => {},
Err(e) if e.kind() == io::ErrorKind::BrokenPipe && actor.state() == ActorState::Released => {
// PTY already released; nothing to hand off
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
if actor.state() == ActorState::Released { /* skip handoff; session already closed */ } Try / catch
match actor.begin_handoff() {
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe && actor.state() == ActorState::Released => { /* benign race; finalize as closed */ }
other => other?,
} Prevention
- Initiate handoff before issuing Release on the actor
- Make close/handoff ordering explicit in the control command queue
- Treat Released-during-handoff as session end, not an error
When it happens
Trigger: Calling begin_handoff (via handle_control_command or the user-write drain path) after the actor has already transitioned to ActorState::Released, e.g. the PTY peer closed or a prior Release command was processed in the pre-quiesce drain.
Common situations: Detaching or migrating a session where the child process exited first, a race between pane close and detach/handoff, or replayed control commands where a release was already queued.
Related errors
- PTY closed while draining writes before handoff
- timed out draining PTY writes before handoff
- poll encountered invalid PTY actor fd
- poll encountered PTY fd error
- app response channel closed
AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28).
Data as JSON: /api/errors/0afcdc748ff48e54.
Report an issue: GitHub.