async fn run_proc(
args: &Args,
command: &str,
mps: Option<&WpsClient>,
buffered: Arc<Mutex<Vec<u8>>>,
) -> Result<i32>Expand description
Spawn the bash child and stream its lines. PTY by default with a pipe fallback if PTY allocation fails (G6 in docs/specs/SPEC_LIVE_LOG_PTY_REWORK_2026_05_16.md).
Why PTY: when the child sees its stdout as a pipe, glibc switches
libc stdio from line-buffered to block-buffered (~4 KB). External
programs (grep, npm, cargo, python, …) accumulate output in the
stdio buffer until flush, defeating the live-log overlay’s premise.
PTY makes isatty(STDOUT_FILENO) == 1 so glibc stays line-buffered.
CRITICAL ConPTY lifetime contract (Windows): pair.master MUST
stay alive across child.wait(). Dropping master during child
startup tears down the pseudoconsole anchor and produces
STATUS_DLL_INIT_FAILED (the β.A wedge). See retro §4.2.
HEADLESS PTY contract (Windows): ConPTY queries the terminal at
startup with \x1b[6n (DSR — request cursor position) and blocks
waiting for a \x1b[r;cR response. A headless PTY (no real terminal
behind it) never answers — the child never proceeds — so
child.wait() would never return. We write a synthetic \x1b[1;1R
response via the master writer, and the reader strips the query from
the output. This is what xterm.js does for VS Code’s agent-mode
terminal; here we do the minimum subset for non-interactive bash -c.
Verified via agentmux-pty-repro V2 before this landed.
Unix PTYs never ask (checked with bash -c 3.2 and 5.x on macOS: no
\x1b[6n in the output, and the command exits without a reply). There
the reply would only be echoed back by the line discipline as the
printable text ^[[1;1R at the start of every result, which no escape
stripping can recognise, and left in the child’s stdin. So it is sent on
Windows only.
The PTY is PTY_COLS wide, not the classic 80: width-aware programs
(tables, help text, column listings) fold their output at the terminal
width with real line breaks, which no preview can undo.