Skip to content

fix: close stdin on streaming Port paths#38

Merged
joshrotenberg merged 1 commit intomainfrom
fix/port-stdin-hang
Apr 10, 2026
Merged

fix: close stdin on streaming Port paths#38
joshrotenberg merged 1 commit intomainfrom
fix/port-stdin-hang

Conversation

@joshrotenberg
Copy link
Copy Markdown
Collaborator

Summary

  • Fix Port-based streaming (Exec.stream/2, ExecResume.stream/2) hanging indefinitely against codex-cli ≥ 0.118 because the Port inherited stdin from the parent and Codex reads stdin at startup.
  • Extract the shell-wrap-and-close-stdin mechanism from Command.run_with_closed_stdin into a shared Command.shell_cmd_args/3 helper so execute and streaming paths share one implementation.
  • Streaming opts out of the 2>&1 stderr merge that the execute path uses, so stderr flows to the parent's stderr without contaminating NDJSON on stdout.

Closes #37. Complements #34 which fixed the same root cause in command.ex (the non-streaming execute path) but didn't touch the streaming paths.

Test plan

  • mix test — 217 tests pass, no regressions
  • Live verification against codex-cli 0.119.0:
    Calling Exec.stream/2 ...
    Stream completed in 13094ms with 4 events.
    Event types: ["thread.started", "turn.started", "item.completed", "turn.completed"]
    SUCCESS: saw turn.completed
    
    Previously this call hung until the internal after 300_000 safety timeout fired with zero events.
  • The Reading additional input from stdin... message from Codex no longer appears, confirming stdin is actually closed.

Background

Discovered while building a GenAgent backend adapter on top of this package. The GenAgent Codex backend currently uses the non-streaming exec_json path (which was already fixed by #34), so this fix doesn't unblock anything immediately for that consumer — but any future streaming consumer would have hit the same wall. Getting the streaming path into the same state as execute makes the package's surface consistent.

There is a sibling issue on claude_wrapper_ex (#42) with the same latent root cause — the Claude CLI doesn't currently trigger it, but the code path is structurally identical. Fix PR to follow there.

The non-streaming execute path was fixed in #34, but the streaming
paths in Exec.stream/2 and ExecResume.stream/2 still opened a raw
Port with stdin inherited from the parent. Codex CLI 0.118+ reads
stdin at startup and blocks forever on the open-but-empty pipe,
causing streams to hang until the internal 300s safety timeout.

Extract the shell-wrapping logic from Command.run_with_closed_stdin
into a shared Command.shell_cmd_args/3 helper that both execute and
streaming paths now use. Streaming opts out of the 2>&1 stderr merge
so stderr flows to the parent without contaminating NDJSON on stdout.

Verified against codex-cli 0.119.0: Exec.stream/2 now completes in
~13s with the expected thread.started / turn.started / item.completed
/ turn.completed events instead of hanging on the safety timeout.

Closes #37
@joshrotenberg joshrotenberg merged commit add5f29 into main Apr 10, 2026
2 checks passed
This was referenced Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port-based streaming hangs on Codex 0.119.0 — stdin never closed

1 participant