The Ruler Was Shorter Than the Line

The count came back 65536.

It was meant to confirm what everyone already knew. A review job had been dying with spawn E2BIG, Linux’s way of saying an argument is too long, and the fix was written. All that was left was to count the failing prompt and watch it come in over the limit. It came in at exactly half.

What was failing

Every morning a timer on my automation box asks a second model family to review whatever my build agents committed the day before. For each repository the tool builds a prompt (instructions, the changed files, and the diff itself when it is small enough) and hands it to OpenAI’s codex command line as its last argument.

Linux caps a single argument at 128 KiB, and the edge is exact: a string of 131,071 characters goes through, and at 131,072 the spawn throws before the program starts. The tool did have a guard, a constant named INLINE_DIFF_BYTE_LIMIT set to 120,000. It compared that against the diff’s .length, which in JavaScript counts UTF-16 code units, not bytes, and it measured the diff before the instructions were wrapped around it. A test repository with a 118,150-character diff, comfortably under the guard, produced a 190,442-byte prompt and failed instantly.

A diff over the guard was left out of the prompt entirely, with an instruction to read it from the repository, so the biggest changes were safe. The ones that killed the review sat just under the line.

It showed up in 11 of the 62 digests the job had written, across five repositories. On the last morning it took the whole run down three times: each failure exited 1, each exit earned an automatic re-run, and each re-run reviewed all eighteen repositories again to fail on the same two.

The confirmation

The repair was small. codex exec reads its prompt from standard input when the argument is -, and standard input has no such cap. Before trusting it, the session making the fix wanted to see the problem. The tool has a --dry-run flag that prints the prompt instead of sending it, so it piped that into wc -c.

65536.

Twenty minutes earlier, a planning session had run the same measurement on three repositories and got 42,760, 65,536 and 27,313.

Why it could only say that

The dry run wrote the prompt to standard output and then called process.exit. A Linux pipe holds 64 KiB. When the prompt is bigger, the first 65,536 bytes go into the pipe and the rest waits inside the Node process for the reader to make room. process.exit does not wait for it. The process ends with the remainder still queued, and wc counts what arrived.

I reproduced it this morning with a script that writes 154,000 bytes and exits. Piped six times: 65536 six times. Piped into a reader that sleeps before reading: 65536. Redirected to a file: 154,000.

So the instrument had a ceiling, and the ceiling was half the threshold it was asked about. A prompt under 64 KiB is counted correctly, and such a prompt never fails. Every prompt over 64 KiB, which includes every prompt that can fail, is reported as the same number, and that number is safely under the limit. The measurement had no way to return yes.

It is one bug twice: a fixed-size opening in the plumbing between processes, 128 KiB on the way into a program and 64 KiB on the way out, and a large string that does not fit through. The tool for looking at the first was standing behind the second.

What gave it away

Only the roundness. The fixing session read the number and answered in three seconds: “Exactly 65536 bytes means the dry-run output got cut off at the pipe buffer, not that the prompt is small.” It redirected to a file and got 154,000. The planning session had flagged the same reading as a separate bug in its follow-ups.

That was luck, not method. Anyone who has written systems code knows 65,536 on sight. A cutoff on some unremarkable number, because a header went out first or the buffer was sized differently, would have read as a size, and a size under the limit sends you looking for the cause somewhere else.

The dry run’s fix is one line: process.exitCode = outcome.exitCode instead of process.exit(...), which lets Node drain the pipe before it leaves. The review that had died on that prompt ran to the end and exited 0, and both runs since have reviewed every repository they were given.

Piped into wc -c after the fix, the same dry run printed 154000. It is the first answer it has ever given that could have been yes.


← all writing