{"record":{"id":"2a8a3a4cc1993f45","repo":"herdrdev/herdr","slug":"pty-closed-while-draining-writes-before-handoff","errorCode":null,"errorMessage":"PTY closed while draining writes before handoff","messagePattern":"PTY closed while draining writes before handoff","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/pty/actor/unix.rs","lineNumber":630,"sourceCode":"            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(),\n                true,\n                true,\n                timeout_ms,\n            )?;\n            if readiness.wake_ready {\n                fd::drain_wake_fd(self.wake_read_fd.as_raw_fd())?;\n            }\n            if readiness.pty_read_ready && !self.read_once() {\n                return Err(std::io::Error::new(\n                    std::io::ErrorKind::BrokenPipe,\n                    \"PTY closed while draining writes before handoff\",\n                ));\n            }\n            if readiness.pty_write_ready {\n                self.flush_pending_writes_once();\n            }\n        }\n        self.state = ActorState::Quiesced;\n        Ok(())\n    }\n\n    fn drain_pre_quiesce_commands(&mut self) {\n        while let Ok(PtyIoDataCommand::WriteUserInput(bytes)) = self.data_rx.try_recv() {\n            if self.state != ActorState::Released {\n                self.enqueue_write(bytes);\n            }\n        }","sourceCodeStart":612,"sourceCodeEnd":648,"githubUrl":"https://github.com/herdrdev/herdr/blob/f457cff4f2648eee85d176f8a41861241d4e8428/src/pty/actor/unix.rs#L612-L648","documentation":"While draining pending writes before handoff, begin_handoff polls the PTY; when the PTY read side becomes ready and read_once() returns false (EOF — the PTY closed), it aborts with ErrorKind::BrokenPipe. Continuing to hand off a closed PTY would lose the fact that the session ended.","triggerScenarios":"The child process exits (or the PTY master is closed elsewhere) while begin_handoff is still flushing pending writes; the poll reports pty_read_ready and read_once returns false, triggering the error.","commonSituations":"User pastes input and detaches immediately, but the child (e.g. a short-lived command) exits during the drain; races between process exit and detach/handoff.","solutions":["Handle it as a normal session-end race: complete the handoff path as 'session closed' rather than as an error","Verify the child's exit status to confirm the PTY closed intentionally","Ensure close/exit handling is idempotent so a handoff racing an exit is resolved deterministically","Add a small grace poll or subscribe to child-exit notification before starting handoff"],"exampleFix":"// before\nlet res = actor.begin_handoff();\nif res.is_err() { panic!(\"handoff failed\"); }\n\n// after\nmatch actor.begin_handoff() {\n    Ok(()) => {}\n    Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {\n        // PTY closed mid-drain; treat as session end, finalize exit state\n        session.mark_pty_closed();\n    }\n    Err(e) => return Err(e),\n}","handlingStrategy":"try-catch","validationCode":"if !actor.is_pty_open() { skip handoff and finalize session exit; }","typeGuard":null,"tryCatchPattern":"match actor.begin_handoff() {\n    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { reap_child(); session.mark_closed(); }\n    other => other?,\n}","preventionTips":["Check child liveness before starting handoff","Make exit handling idempotent to tolerate exit/handoff races","Propagate child exit status instead of retrying handoff"],"tags":["pty","unix","handoff","eof","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"}