openai / openai/codex-plugin-cc

codex-companion crashes with EAGAIN on concurrent sessions (readStdinIfPiped sync read)

Open
#247 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. Start a first codex-companion.mjs task that holds its stdin pipe open for several seconds (e.g., reading a large prompt).
  2. Spawn a second codex-companion.mjs task from the same parent shell / same process group while step 1 is still reading.
  3. The second one fails with EAGAIN: resource temporarily unavailable, read exit 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 unavailable errors 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.