Skip to content

Fix EAGAIN crash when stdin is a non-blocking handle with no data - #16

Merged
TheOnlyFusionCube merged 2 commits into
TheOnlyFusionCube:mainfrom
hessabi:fix/stdin-eagain-v2
Oct 25, 2026
Merged

TheOnlyFusionCube merged 2 commits into
TheOnlyFusionCube:mainfrom
hessabi:fix/stdin-eagain-v2

Conversation

@hessabi

@hessabi hessabi commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Re-file of the former #5, as requested in #4, rebased onto current main.

What was wrong: readStdinIfPiped() called fs.readFileSync(0) whenever stdin wasn't a TTY. Under Claude Code's Bash tool, which the devin-rescue subagent uses, fd 0 is a non-blocking handle with no data. The read threw EAGAIN and task failed before Devin ran, so /devin:rescue didn't work from Claude Code.

What changed

  • fd 0 is read only when fstat reports a FIFO or a regular file.
  • EAGAIN and ENXIO are treated as empty input. Other errors are rethrown.
  • fstat, read and isTTY are injectable for tests.

How tested

  • 10 unit tests cover the branches.
  • One end-to-end test uses no mocks:
    • It opens a real FIFO with O_NONBLOCK and an open writer that sends nothing.
    • It runs readStdinIfPiped in a child process with that FIFO as stdin and asserts the result is "".
    • It fails with EAGAIN: resource temporarily unavailable on current main, and passes with this change.
    • It's skipped on Windows, which has no mkfifo.
  • node --test: 78/78.

readStdinIfPiped called fs.readFileSync(0) whenever stdin was not a TTY.
Under Claude Code's Bash tool (used by the devin-rescue subagent), fd 0
is a non-blocking handle with no data, so the read threw EAGAIN and the
task command failed before Devin ran.

Now fd 0 is read only when fstat says it is a FIFO or regular file, and
EAGAIN/ENXIO are treated as empty input. Dependencies are injectable for
unit tests, and an end-to-end test reads from a real non-blocking FIFO
with an open, silent writer (tests/fs.test.mjs).

@TheOnlyFusionCube TheOnlyFusionCube left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EAGAIN guard addresses the original empty nonblocking-stdin crash, and preserving other I/O errors is good. Blocking issue: fs.readFileSync(0, 'utf8') may consume some FIFO bytes and then throw EAGAIN while the writer remains open. Catching EAGAIN around the entire read loses those bytes and returns an empty prompt. The added FIFO regression only checks an empty FIFO. Please use an incremental readSync loop that preserves successfully read chunks, ends on EOF or EAGAIN, and bounds input appropriately. Add a real FIFO test with bytes already written while the writer stays open, asserting the exact input is returned, plus a multi-chunk test. I ran a focused reproduction of this patch's reader on Node 22: the empty FIFO case passed, but a FIFO containing PROMPT DATA\n returned "" instead of the payload. The existing Node 18/20/22 CI Test matrix passed; this is a missing behavioral case, not a CI infrastructure failure. The HOL Scanner reports the same one high as main and is a separate issue.

Comment thread plugins/devin/scripts/lib/fs.mjs Outdated
throw e;
}
try {
return readImpl(0, "utf8");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: readFileSync may read partial FIFO data and then throw EAGAIN; the catch discards the already-read bytes. Verified with a real FIFO containing PROMPT DATA\n and an open writer: the function returned empty string. Replace with chunked reads that retain bytes up to EAGAIN/EOF and add a non-empty FIFO regression.

Read stdin with an incremental readSync loop so bytes already received from a non-blocking FIFO survive EAGAIN, decode once at the end, and throw if input exceeds an 8 MiB cap instead of truncating. Add real FIFO tests for pending data, multi-byte UTF-8 and multiple chunks.
@hessabi

hessabi commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, that was a real bug. readStdinIfPiped now reads fd 0 with an incremental fs.readSync loop. It keeps every chunk already read and stops on EOF or EAGAIN/ENXIO, so bytes already in a non-blocking FIFO are no longer dropped. Other errors still throw. It decodes once at the end, so multi-byte UTF-8 split across chunks stays intact. Input is capped at an exported MAX_STDIN_BYTES (8 MiB), and anything over the cap throws a clear error rather than being truncated.

Tests: new real mkfifo tests with an O_NONBLOCK reader and an open writer, covering PROMPT DATA\n already written, multi-byte UTF-8, and a multi-chunk payload (chunkSize 7), all asserting exact equality. The empty-FIFO test is kept. There are also unit tests for partial data followed by EAGAIN, chunk joining, and the cap. The FIFO data tests return "" on the previous commit and pass now. node --test passes 88/88.

@TheOnlyFusionCube TheOnlyFusionCube left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: the incremental readSync loop preserves partial FIFO bytes across EAGAIN/ENXIO, decodes UTF-8 once after concatenation, enforces an explicit 8 MiB cap with an overflow probe, and retains propagation of other I/O errors. Real nonblocking FIFO tests now cover empty, populated, UTF-8, and multichunk streams; CI Test passed on the new head. The HOL scanner failure is also present on main and is tracked separately; do not relax the check. Approved.

@TheOnlyFusionCube
TheOnlyFusionCube merged commit 83653b1 into TheOnlyFusionCube:main Oct 25, 2026
3 of 4 checks passed
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.

2 participants