app-server command/exec can hang after outputBytesCap is reached due to back-pressure
What version of Codex CLI is running?
codex-cli 0.111.0
What subscription do you have?
Pro
Which model were you using?
_No response_
What platform is your computer?
Darwin 25.3.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
iTerm.app 3.6.8
What issue are you seeing?
command/exec in the app-server can stop draining stdout/stderr as soon as outputBytesCap is reached. After that, the lower-level pipe reader still forwards output into a bounded mpsc::channel(128). Once that channel fills, stdout/stderr back-pressure can block the child process and prevent command/exec from finishing.
I do not have a user-facing error message from a released binary to paste here, because I found this while validating a code-level bug report against the current source tree. The problematic behavior is in:
codex-rs/app-server/src/command_exec.rsspawn_process_output()
Specifically, it stops reading once:
if cap_reached {
break;
}
That appears unsafe because the producer side in codex-rs/utils/pty/src/pipe.rs still sends into a bounded channel.
This appears distinct from, but possibly related to, #7852. That issue discusses indefinite hangs involving orphaned child processes / pipe EOF behavior. This report is specifically about app-server output capping stopping the drain loop too early.
What steps can reproduce the bug?
I do not have a stable end-user CLI repro from a released build yet. The reproduction I have is at the app-server/code level.
The problematic flow is:
codex-rs/app-server/src/command_exec.rsspawn_process_output()- it stops reading from
output_rxoncecap_reached == true
Meanwhile the producer side is still active:
codex-rs/utils/pty/src/pipe.rs- output is forwarded through a bounded
mpsc::channel::<Vec<u8>>(128)
A practical reproduction shape is:
- Start
command/execwith a smalloutputBytesCapsuch as5 - Use either buffered or streaming output
- Run a command that writes a large amount of stdout/stderr in multiple chunks
- Once the cap is reached, the app-server side stops draining output
- The bounded channel fills and the child can block on write, causing
command/execto hang or fail to complete promptly
Minimal code-level repro sketch:
let (output_tx, output_rx) = mpsc::channel(1);
let handle = spawn_process_output(SpawnProcessOutputParams {
connection_id: ConnectionId(1),
process_id: Some("proc-1".to_string()),
output_rx,
stdio_timeout_rx,
outgoing,
stream: CommandExecOutputStream::Stdout,
stream_output: false,
output_bytes_cap: Some(5),
});
// First chunk reaches the cap.
output_tx.send(b"abcdef".to_vec()).await?;
// Without continued draining, a later send can block once the bounded channel fills.
output_tx.send(b"ghijkl".to_vec()).await?;
I have a local fix and regression tests that demonstrate the intended behavior for both:
- buffered output
- streaming output
What is the expected behavior?
command/exec should continue draining stdout/stderr after the output cap is reached, while only buffering or emitting the capped prefix.
Expected behavior:
- capped output is still returned/emitted correctly
- post-cap bytes are discarded
- the child process can still finish normally
- no deadlock/back-pressure hang occurs
This matches the behavior already used in the core exec path, which continues reading to EOF after capping specifically to avoid back-
pressure.
Additional information
Root cause
In codex-rs/app-server/src/command_exec.rs, spawn_process_output() currently stops reading after the cap is reached:
if cap_reached {
break;
}
That is unsafe because the lower-level pipe reader in codex-rs/utils/pty/src/pipe.rs keeps sending into a bounded channel. Once the
receiver stops draining, the channel can fill and block producers.
Why this looks like a regression
The core exec path already handles this safely in codex-rs/core/src/exec.rs by continuing to read to EOF after capping specifically to
avoid back-pressure.
Validation
I have a local fix with regression coverage for both:
- buffered path
- streaming path
Local checks passed on the fix branch:
- just fmt
- just fix -p codex-app-server
- cargo test -p codex-app-server
If this direction matches maintainer expectations, I can open an invited PR.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗