{"record":{"id":"0afcdc748ff48e54","repo":"herdrdev/herdr","slug":"pty-actor-was-released-before-handoff-quiesce","errorCode":null,"errorMessage":"PTY actor was released before handoff quiesce","messagePattern":"PTY actor was released before handoff quiesce","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/pty/actor/unix.rs","lineNumber":603,"sourceCode":"                };\n                let _ = reply.send(result);\n            }\n            PtyIoControlCommand::ReleaseAfterCommit(reply) => {\n                self.state = ActorState::Released;\n                self.pending_writes.clear();\n                let _ = reply.send(Ok(()));\n                return true;\n            }\n            PtyIoControlCommand::Shutdown => return true,\n        }\n        false\n    }\n\n    fn begin_handoff(&mut self) -> std::io::Result<()> {\n        self.drain_pre_quiesce_commands();\n        self.apply_pending_controls();\n        if self.state == ActorState::Released {\n            return Err(std::io::Error::new(\n                std::io::ErrorKind::BrokenPipe,\n                \"PTY actor was released before handoff quiesce\",\n            ));\n        }\n        let deadline = Instant::now() + HANDOFF_DRAIN_TIMEOUT;\n        self.flush_pending_writes_once();\n        while !self.pending_writes.is_empty() {\n            let remaining = deadline.saturating_duration_since(Instant::now());\n            if remaining.is_zero() {\n                return Err(std::io::Error::new(\n                    std::io::ErrorKind::TimedOut,\n                    \"timed out draining PTY writes before handoff\",\n                ));\n            }\n            let timeout_ms = remaining.as_millis().min(i32::MAX as u128) as i32;\n            let readiness = fd::poll_pty_and_wake(\n                self.file.as_raw_fd(),\n                self.wake_read_fd.as_raw_fd(),","sourceCodeStart":585,"sourceCodeEnd":621,"githubUrl":"https://github.com/herdrdev/herdr/blob/f457cff4f2648eee85d176f8a41861241d4e8428/src/pty/actor/unix.rs#L585-L621","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nmatch actor.begin_handoff() {\n    Ok(()) => {},\n    Err(e) => return Err(e), // crashes detach flow on released actor\n}\n\n// after\nmatch actor.begin_handoff() {\n    Ok(()) => {},\n    Err(e) if e.kind() == io::ErrorKind::BrokenPipe && actor.state() == ActorState::Released => {\n        // PTY already released; nothing to hand off\n    }\n    Err(e) => return Err(e),\n}","handlingStrategy":"try-catch","validationCode":"if actor.state() == ActorState::Released { /* skip handoff; session already closed */ }","typeGuard":null,"tryCatchPattern":"match actor.begin_handoff() {\n    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe && actor.state() == ActorState::Released => { /* benign race; finalize as closed */ }\n    other => other?,\n}","preventionTips":["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"],"tags":["pty","unix","handoff","session-detach","broken-pipe"],"backgroundTag":"pipe-closed-during-handoff","analyzedSha":"f457cff4f2648eee85d176f8a41861241d4e8428","analyzedAt":"2026-08-28T15:41:09.197Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}