I run a small team of LLM agents that coordinate through markdown files. Several roles run on Codex CLI 0.165.0, which runs their tool calls in code mode. They read task items, review findings, plans, and design documents, and then decide what to do next.
On October 24 I checked whether those reads were complete. Of 33 Codex sessions working on one goal that day, 31 had at least one tool output cut short, 129 cuts in all. Among the cut outputs were a reviewer's findings and the work plan the agent was executing. The agents went on to make decisions from what was left.
I had seen this once in another project, so I knew what to look for. Seeing it across almost every session of a team that had been running for weeks still shocked me.
What the cut looks like
When an output is too long, the call doesn't fail. The output starts with a line like Warning: truncated output (original token count: 15382), keeps the start and the end, drops the middle, and leaves a marker:
...stage 4 the same), the stage's units, slots, and candidates…4107 tokens truncated…tained set the policy keeps, ...
The model gets all of that back as the result of cat. If it reads past the warning and the marker, the file looks complete, minus the middle of the document, which is usually where the detail is.
The first fix
Codex has a config value, tool_output_token_limit, that limits tool output. It was at its default of 10,000 tokens, and my largest document is about 39,000. I raised it to 40,000, the point where almost everything still cut was process dumps and test JSON.
There is also a per-call budget, max_output_tokens, which the model sets itself. My other project, codex-workflows, handles that with a strict rule: measure files with wc -c before reading them. For a team that reads all day I didn't want an extra step on every read. So I explained how the budgets work and asked the model to set ones that fit what each call returns. How to read was left to the model.
Six days later
Outputs were still being cut: 11% of tool outputs, against 13% the week before.
I replayed one of the cut reads: a director opening a discussion and pulling in its references. Every cut output stopped at about 10,000 tokens, even when the model had asked for 18,000 or 26,000.
The cause turned out to be a third limit. In code mode, the model's commands run inside a JavaScript exec cell, and the cell has its own budget for everything it returns, set by a first line like // @exec: {"max_output_tokens": 40000}. Without that line it is 10,000, however much the commands inside asked for. Across 9,538 calls from five days of logs, the model wrote that line 137 times. The ceiling I had raised was never the limit. It is a known issue, openai/codex#33402, open since July.
My instruction already mentioned the pragma. I rewrote it twice to explain the two budgets more plainly, the second time saying outright that the smaller one applies. Over four replays, 19 of 24 calls were cut, and the model wrote the pragma zero times.
What worked
I stopped asking the model to choose. The instruction now gives one fixed value for every call, and the reason for it:
Start every `exec` call with the first line `// @exec: {"max_output_tokens": 40000}` and give every `exec_command` `max_output_tokens: 40000`. Output past the smaller of the two budgets loses its middle, marked `…N tokens truncated…`, and you miss whatever detail that middle held; without the first line that budget is 10000. Base a judgment only on text you received, reading again any cut part it depends on.
In three replays every one of the 16 calls carried both budgets. Three were still cut, each a batch of eight or more files that added up past 40,000, and each time the model went back and read the missing part of the document in ranges.
I had left the budget to the model's judgment because the right value depends on the call. In practice it kept leaving the one budget that mattered at its default, no matter how I explained it.
If you run Codex in a loop, count the markers in your session logs. They live under ~/.codex/sessions/, and older ones are compressed as .jsonl.zst, so this reads both:
find ~/.codex/sessions -name '*.jsonl*' -exec zstd -dcqf {} + \
| grep -oE '…[0-9]+ tokens truncated…' | wc -l
Top comments (9)
The "31 of 33 sessions had a truncated read and the agents decided anyway" number is the scary part — not that truncation happens, but that it's silent and the model treats the start+end of a file as the whole thing. We hit the same class of bug with agents coordinating through markdown and landed on a slightly different mitigation: instead of tuning the three budgets to fit the biggest document, we made the read tool fail loudly when it truncates, returning an error the agent has to handle (re-read in ranges) rather than a warning it can read past. Truncation-as-error beat truncation-as-warning for us because models reliably ignore the warning line once there's a plausible-looking body after it. The
wc -cbefore read is the right instinct too; we front-loaded it into the tool itself so the model can't skip it. One question — did you ever see an agent notice the…N tokens truncated…marker and self-correct without you prompting it? In our logs that basically never happened, which is what convinced me warnings aren't enough.Never on its own. That's why it went unnoticed for so long the first time. That was in another project, a classification system where an agent read a lot of material and sorted it by a set of rules. The results followed the rules less closely than they should have, and I kept digging until I found the cut reads.
I didn't make truncation an error because these agents don't do one fixed job, and I don't have enough cases yet to know what an agent should do after each kind of error. The team works this way in general: start nondeterministic, then move whatever settles into deterministic code. So it's useful to hear that the error approach holds up in practice for you.
Prompting the model to pass
// @exec: {"max_output_tokens": 40000}breaks down because token budgets belong at the harness boundary. When the context window fills up or multi-step tool loops chain together, prompt instructions get dropped. Even with the 40k pragma hardcoded, middle-truncation with exit code 0 remains dangerous because the model receives valid text with an inline marker and continues as if the document were whole. Moving the contract into the tool wrapper by throwing an immediate exit code or returning byte ranges likeread_file(path, offset, limit)prevents the agent from running on half a plan. Raising the buffer to 40k buys temporary space, but any 60k test log or schema dump will trigger the exact same silent truncation.Agreed that this belongs in the harness. The 10,000-token default on the outer exec cell is what openai/codex#33402 is about, and I'd rather see it fixed in Codex than have every prompt carry a pragma.
For now, only tool_output_token_limit can be set from outside, in config.toml or with -c on codex exec. The other two budgets are set per call by the model, and the smallest of the three wins, which is why raising the ceiling alone didn't help.
On this team the work is routine enough that 40k covers most reads, and when a batched read goes past it, the agent rereads the cut part in ranges.
A verdict on a file that was only half read is a verdict on the half that was read. Make the judge log the last line it reached before it decides, and refuse a verdict whose range doesn't cover the file. One number per file turns a silent gap into a visible one.
Right, a verdict only covers what was read. Logging the last line would catch a cut that drops the end of a file. Codex drops the middle instead, so the agent still reaches the last line and the read looks complete.
What I've settled on for now is making the cut rarer and cheap to repair: the same 40,000-token budget on every call, and when a read still gets cut, the agent rereads only the missing range.
Judging a file it never finished reading is a fun way to get confident wrong reviews.
Very fun. 129 times in one day.
129 times in one day makes the confident part hard to argue with.