{"record":{"id":"b87a8d0f7ae0d484","repo":"herdrdev/herdr","slug":"timed-out-draining-pty-writes-before-handoff","errorCode":null,"errorMessage":"timed out draining PTY writes before handoff","messagePattern":"timed out draining PTY writes before handoff","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/pty/actor/unix.rs","lineNumber":613,"sourceCode":"        }\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(),\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,","sourceCodeStart":595,"sourceCodeEnd":631,"githubUrl":"https://github.com/herdrdev/herdr/blob/f457cff4f2648eee85d176f8a41861241d4e8428/src/pty/actor/unix.rs#L595-L631","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nactor.begin_handoff()?; // may time out on huge pending paste\n\n// after: drain or abort pending input before handoff\nif actor.pending_writes_len() > LARGE_WRITE_THRESHOLD {\n    actor.discard_pending_writes_for_handoff(); // explicit data-loss decision\n}\nactor.begin_handoff()?;","handlingStrategy":"retry","validationCode":"if actor.pending_writes_len() > 0 { wait_for_drain(grace_period) before begin_handoff; }","typeGuard":null,"tryCatchPattern":"match actor.begin_handoff() {\n    Err(e) if e.kind() == std::io::ErrorKind::TimedOut => retry_after_backoff_or_discard_pending(),\n    other => other?,\n}","preventionTips":["Avoid detaching immediately after huge pastes","Keep pending-write queues shallow by chunking large input","Watch child process health before handoff"],"tags":["pty","unix","handoff","timeout","write-queue"],"backgroundTag":"write-drain-timeout","analyzedSha":"f457cff4f2648eee85d176f8a41861241d4e8428","analyzedAt":"2026-08-28T15:41:09.197Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}