Hmbown/CodeWhale · error · io::Error

terminal input pump did not pause before child terminal…

Error message

terminal input pump did not pause before child terminal handoff

What it means

pause_terminal_input_for_child_terminal sets a `paused` flag and waits (bounded by TERMINAL_INPUT_CHILD_PAUSE_TIMEOUT) for the input-pump thread to acknowledge via `paused_ack`. If the ack never arrives before the deadline, it rolls both flags back and returns ErrorKind::TimedOut, refusing the child-terminal handoff so the pump is never left in a half-paused state.

Solutions

  1. Verify the terminal input pump thread is alive and processing gates before handing off to a child terminal.
  2. Increase TERMINAL_INPUT_CHILD_PAUSE_TIMEOUT if the platform legitimately needs longer to ack.
  3. Retry the handoff after confirming pump health.
Defensive patterns

Strategy: fallback

Validate before calling

// before handoff
if !terminal_pump_is_alive() { restart_pump()?; }

Try / catch

match pause_terminal_input_for_child_terminal(&gate) {
    Err(e) if e.kind() == io::ErrorKind::TimedOut => abort_handoff_gracefully(),
    Ok(guard) => run_child_terminal(guard),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling pause_terminal_input_for_child_terminal when the pump thread is stuck, busy, or dead and does not observe `paused` within the timeout (tested by the refusal test).

Common situations: Pump thread blocked on a read that never yields, thread already exited, extreme system load delaying the poll loop past the deadline.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/fc3b42d3f06aca47. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tui/ui/terminal_input.rs:122

/// not run the child, because that is exactly the keystroke-splitting state
/// this guards against. A process with no pump published (tests, non-TUI
/// callers) has nothing to pause and succeeds with an inert guard, matching
/// [`TerminalInputPump::pause_for_child_terminal`]'s `handle.is_none()` case.
pub(crate) fn pause_terminal_input_for_child() -> io::Result<ChildTerminalInputPause> {
    let Some(gate) = CHILD_TERMINAL_GATE
        .lock()
        .ok()
        .and_then(|gate| gate.clone())
    else {
        return Ok(ChildTerminalInputPause { gate: None });
    };
    gate.paused.store(true, Ordering::Release);
    let deadline = Instant::now() + TERMINAL_INPUT_CHILD_PAUSE_TIMEOUT;
    while !gate.paused_ack.load(Ordering::Acquire) {
        if Instant::now() >= deadline {
            gate.paused_ack.store(false, Ordering::Release);
            gate.paused.store(false, Ordering::Release);
            return Err(io::Error::new(
                io::ErrorKind::TimedOut,
                "terminal input pump did not pause before child terminal handoff",
            ));
        }
        // Blocking-call convention (#6149): a bounded retry, capped by
        // `TERMINAL_INPUT_CHILD_PAUSE_TIMEOUT`, in a synchronous API whose
        // caller is about to block this very thread on a foreground editor
        // for as long as the user keeps it open. `tokio::time` is not
        // reachable from here and would not change what the thread does.
        thread::sleep(TERMINAL_INPUT_CHILD_PAUSE_POLL_INTERVAL);
    }
    Ok(ChildTerminalInputPause { gate: Some(gate) })
}

impl Drop for ChildTerminalInputPause {
    fn drop(&mut self) {
        if let Some(gate) = self.gate.take() {
            gate.paused_ack.store(false, Ordering::Release);

View on GitHub (pinned to 73e0f67d83)