openai / openai/codex-plugin-cc
codex-companion crashes with EAGAIN on concurrent sessions (readStdinIfPiped sync read)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Summary
When two codex-companion sessions run concurrently on the same machine, one of them can crash immediately with:
EAGAIN: resource temporarily unavailable, read
The crash happens in scripts/lib/fs.mjs::readStdinIfPiped before the companion's task even begins. It looks like a hard CLI failure to upstream callers, not a transient condition.
Root cause
readStdinIfPiped does a synchronous read of fd 0:
https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/fs.mjs#L35-L40
export function readStdinIfPiped() {
if (process.stdin.isTTY) {
return "";
}
return fs.readFileSync(0, "utf8");
}
fs.readFileSync(0, ...) calls the read(2) syscall directly. If the inherited stdin fd is marked O_NONBLOCK — which can happen when a sibling Node process in the same session group has put it in non-blocking mode — the syscall returns -1/EAGAIN instead of blocking. readFileSync treats EAGAIN as a hard error and throws; there's no retry or blocking-mode toggle.
On macOS, fd flags are inherited across fork(2)/exec(2), so one companion invocation's fd state can poison a sibling invocation's stdin pipe. Observed in the wild when two plugin-hosted review pipelines each spawn node codex-companion.mjs task in parallel.
Reproduction
Non-trivial to reproduce deterministically because it's a race, but the setup is:
- Start a first
codex-companion.mjs taskthat holds its stdin pipe open for several seconds (e.g., reading a large prompt). - Spawn a second
codex-companion.mjs taskfrom the same parent shell / same process group while step 1 is still reading. - The second one fails with
EAGAIN: resource temporarily unavailable, readexit 1.
A much more reliable proof is to manually set the fd flag before running:
node -e '
const fs = require("fs");
try { require("tty").ReadStream.prototype.setBlocking?.call(process.stdin, false); } catch {}
console.log(fs.readFileSync(0, "utf8").length);
' < /etc/hosts
Impact
- Appears to be the source of sporadic
CLI unavailableerrors in plugin pipelines that parallelise codex invocations (observed across a review-harness run: ~20% failure rate when two companion sessions run against unrelated projects simultaneously). - Failures are opaque downstream: stderr is often swallowed by wrappers, so callers see only a non-zero exit and
BUILTIN_FALLBACK-style degradation, not the underlying EAGAIN. - The companion is otherwise healthy — the task would have run fine if the read had blocked.
Proposed fix
Minimal sync-compatible fix: force blocking mode on fd 0 before reading, and retry on EAGAIN as a safety net. Synchronous because callers (buildPromptInput) are sync.
export function readStdinIfPiped() {
if (process.stdin.isTTY) {
return "";
}
- return fs.readFileSync(0, "utf8");
+ // Defend against EAGAIN on stdin when the fd is inherited in non-blocking
+ // mode from a concurrent sibling process (observed when multiple
+ // codex-companion sessions run in parallel — fd flags propagate across
+ // fork/exec and a raw readFileSync throws instead of blocking).
+ try {
+ if (process.stdin._handle && typeof process.stdin._handle.setBlocking === "function") {
+ process.stdin._handle.setBlocking(true);
+ }
+ } catch {
+ // setBlocking unsupported on this fd type; retry loop below will cover it.
+ }
+
+ const maxAttempts = 8; // ~2.5s total wall clock at worst case
+ let delayMs = 10;
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
+ try {
+ return fs.readFileSync(0, "utf8");
+ } catch (err) {
+ if (err && err.code === "EAGAIN" && attempt < maxAttempts - 1) {
+ const deadline = Date.now() + delayMs;
+ while (Date.now() < deadline) {
+ // brief busy-wait; EAGAIN usually clears within milliseconds
+ }
+ delayMs = Math.min(delayMs * 2, 500);
+ continue;
+ }
+ throw err;
+ }
+ }
+ return "";
}
A cleaner long-term fix is to convert readStdinIfPiped to use an async for await over process.stdin and make the caller chain async — but that's a wider refactor (it propagates through buildPromptInput and every call site). The sync patch above is a safe drop-in that eliminates the observed failure class without changing the public surface.
Happy to open a PR against either approach if useful.
Environment
- Observed on macOS 14 (Sonoma / Sequoia), Node 20 / 22.
- Companion invocation form:
printf '%s' "$prompt" | node codex-companion.mjs task [--effort high].
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read scripts/lib/fs.mjs, especially readStdinIfPiped, then trace its synchronous use through buildPromptInput. Run the provided non-blocking-stdin reproduction and test concurrent codex-companion task invocations. Done means piped input no longer fails with EAGAIN while normal stdin behavior and the synchronous caller remain intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100