A timeout was reported as a parsing error
A build agent killed by its time budget was reported as producing unparseable output, sending self-repair after the wrong problem twice; the fix names the true failure and gives the reporting component its first tests.
Development track
Most people building with agents today work on a laptop, one session at a time, watching it happen. We are trying to scale past that: a pipeline where work is designed, reviewed, built, and verified by agents running under a control plane, whether or not a human is awake to watch.
Scaling changes what breaks. This is a story about a failure that was not in the code being built, not in the model, and not in the infrastructure. It was in the sentence the system used to describe the failure. That sentence cost us two rounds of self-repair aimed at the wrong target.
What happened
The job was a large one: an OAuth 2.1¹ authorisation server for a hosted Model Context Protocol² endpoint, across the application programming interface and the web app. The design passed review. The build went out.
The build stage failed, twice, with the same report: the build agent's output was "unparseable."
Our pipeline has a self-repair rule, and it fired the way it was designed to. It diagnosed a parsing defect, rewrote the parsing code, passed its test suites, redeployed itself, verified its own fingerprint, and resumed the work, unaided, no human involved. Then it did that a second time. The build kept failing the same way, the repair budget ran out, and the chain escalated and stopped rather than retry forever. That last part, at least, worked exactly as intended.
The timeline
The three attempts ran as follows:
The same numbers in text form:
- First build: stage budget 3,600 seconds; killed at 3,300 seconds with
about 400,000 bytes of patch already written; recorded as "Output unparseable."
- Second build: stage budget 7,200 seconds; killed at 6,900 seconds with
about 813,000 bytes across 110 files already written; recorded as "Output unparseable."
- Seeded continuation: finished in 45 minutes; recorded as success, agent
exit 0.
Figure 1. Both builds died just under their budget, still working. Nothing in either run was a parsing problem.
What was actually wrong
The build agent was healthy. It was working the whole time. The build was simply enormous, and a time budget killed it mid-work, once with a patch of roughly 400,000 bytes already written, and later, under a longer budget, with a patch of roughly 813,000 bytes across 110 files.
Here is the mechanism. The agent writes its result only when it completes. A killed run leaves an empty result file. The component that reads that file (the result builder) wrapped every failure in a single error label. Empty file, corrupt file, crashed agent, timed-out agent: all of it came out as "output unparseable." We reproduced it locally with the unmodified builder: an empty result file plus a timeout exit code produced the identical failure string the live system had recorded.
So the self-repair was not stupid. It was obedient. It fixed the problem the error message described, and the error message was lying. And the reason nobody caught it sooner is a plain one: the test suites the repair trusted contained zero tests of the result builder itself. The one component whose job is to tell the truth about every other component was the one component nobody had tested.
What we changed
Small, once seen:
- The builder now names the true condition. An absent or empty result is
reported as what it was: a timeout (with the exit code), a bare non-zero exit, or a silent zero exit. Only genuinely corrupt output is called unparseable.
- Six new regression tests run the real builder against fixture outputs:
timeout kill, crash with no output, corrupt output, wrapper failure, a healthy run, and a timeout on the second agent runtime. That is the builder's first test coverage. All six suites pass.
- The build itself was finished the way large work has to be finished in a
system with stage budgets: as a seeded continuation. The timed-out attempt's work became the starting point, and the continuation completed in 45 minutes with a clean exit, its final state verified in direct line of descent from the project's main branch.
The error vocabulary, before and after:
The same labels in text form:
- Agent killed by the time budget: before, "Output unparseable"; after,
timed out, with the exit code.
- Agent exited non-zero, no result: before, "Output unparseable"; after,
bare non-zero exit.
- Agent exited zero, no result: before, "Output unparseable"; after, silent
zero exit.
- Result file present but corrupt: "Unparseable" before and after, the only
case that keeps the word.
What it cost
The direct cost was time and repair effort spent on the wrong problem. The two killed builds consumed 3,300 seconds and 6,900 seconds of stage time before they were stopped. Self-repair then ran twice: it rewrote the parsing code, passed its test suites, redeployed itself, and verified its own fingerprint, and none of that work addressed the time budget that was the actual cause. When the repair budget ran out, the chain escalated and stopped, and the build still had to be finished by a seeded continuation that took another 45 minutes.
The less visible cost is trust in the record. Every later decision in the chain was made from a label that was wrong, so the escalation looked like a parsing problem that had resisted two fixes, when the underlying build had been healthy throughout.
What we take from it
Error labels are an interface, and other systems program against them. A human reading "unparseable" asks a question. A self-repair system reading "unparseable" starts editing the parser. The more autonomy you grant the pipeline, the more its error vocabulary has to be exact, because nobody is standing there to smell that the label is wrong.
The same choice appears in much smaller setups. A nightly script that reports every failure as "bad output" sends whoever maintains it, human or agent, to the output format first. If the real cause was a timeout, a lost permission, or a missing file, the report has to say so, in words specific enough that the next step is the right one.
Test the observer. Every system has a component that reports on the rest: the builder, the monitor, the dashboard, the evaluation. If that component is wrong, every downstream decision is wrong with confidence. It deserves the first tests, not the last.
Self-repair inherits the blind spots of what it trusts. Our repair loop did everything right against the evidence it had. Autonomy does not remove the need for judgement about where the evidence is thin. It raises the price of getting that wrong, because the loop will act on it at machine speed, in the middle of the night, twice.
Correctness is a property of the boring parts. The glamorous failure modes of agent systems are hallucination and runaway behaviour. This one was a string constant and a missing test file. The amounts our software will represent, and the freshness its data has to have, mean the boring parts are exactly where we cannot afford to be casual.
References
- "The OAuth 2.1 Authorization Framework" — IETF OAuth Working Group Internet-Draft
- Model Context Protocol specification, "Overview" — modelcontextprotocol.io