app-server command/exec can hang after outputBytesCap is reached due to back-pressure

Open 💬 2 comments Opened Mar 7, 2026 by fitchmultz

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.rs
  • spawn_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.rs
  • spawn_process_output()
  • it stops reading from output_rx once cap_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:

  1. Start command/exec with a small outputBytesCap such as 5
  2. Use either buffered or streaming output
  3. Run a command that writes a large amount of stdout/stderr in multiple chunks
  4. Once the cap is reached, the app-server side stops draining output
  5. The bounded channel fills and the child can block on write, causing command/exec to 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.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗