Ground Truth

Accept The Work, Not The Claim: An Agent's Own Status Is Not Evidence

Mostafa DhouibMostafa Dhouib·10 min read
The short answer

A headless agent run that hits a rate limit can exit with a success status and a zero exit code, having written nothing at all. Every reliability feature that reads the agent's own account of what happened, retry, recovery, the status pane, then repeats that false claim instead of catching it. The only fix is an independent check between running and accepting.

Accept The Work, Not The Claim: An Agent's Own Status Is Not Evidence

The short answer. Run one widely used agent runner in headless mode, push it into a rate limit mid-run, and watch what comes back. It can return a success status and exit code 0, with no files written, no diff, no commit. Nothing failed loudly because nothing in the run treated a rate limit as a failure. That single behaviour breaks every reliability feature built on top of the run's own report. Retry sees success and does not retry. Recovery sees success and has nothing to recover. The dashboard shows green. The fix is not a better report. It is an independent check between running and accepting: files present, tests passing, a diff that exists.

We found this while building acceptance drills for an agent pipeline, not while debugging an outage. The drill was deliberately boring. Start a run that should produce a change, starve it of quota, and see what the orchestrator learns.

What it learned was that the work succeeded.

A zero exit code means the process ended, not that the job happened

An exit code is a contract between a process and whatever started it, and the contract is narrower than people assume. Zero means the process reached its end without raising an error it considered fatal. That is all it ever meant.

A rate limit, inside an agent runner, is usually not fatal. It is a condition the runner handles. It backs off, it truncates its plan, it writes a note about what it could not do, and then it finishes its turn in good order. Finishing in good order is exit code 0. The runner is not lying. It completed the conversation it was having. The conversation simply did not include editing any files.

This is worth separating from the other way a run can look fine and do nothing, where the model emits a tool call and the plumbing drops it. That one is a bug in the channel. This one is not a bug at all. It is the documented, reasonable behaviour of a component that was never asked to have an opinion about whether your task got done.

So the question is not why the runner reported success. The question is why your system believed it.

A rate limit is not fatal
the runner backs off, truncates its plan, writes a note about what it could not do, and finishes
Finishing in order is exit zero
zero means the process reached its end without raising something it considered fatal, which is all it ever meant
No files were written
no diff, no commit, and nothing in the run treated that as a failure
The runner never knew the task
it was never asked to have an opinion about whether your milestone got done
The failure point
A headless run returns a success status and exit code 0, having produced nothing at all.
This is not a bug in the channel, which is the other way a run can look fine and do nothing. It is documented, reasonable behaviour, which is why nothing catches it. The question is not why the runner reported success. The question is why your system believed it.
Provoked deliberately in a controlled workspace to characterise what the orchestrator sees, not reported from a client incident. Once is enough, because the failure is silent and it propagates.
FigureThe runner is not lying. It completed the conversation it was having, and that conversation did not include editing any files. A rate limit inside an agent runner is a condition it handles rather than an error it raises, and handling a condition and finishing the turn in good order is what exit code zero has always meant.

Reading the claim is the thing that makes it dangerous

Here is the part that took us longest to see. Every reliability feature we had built read the run's own account of itself. And each one, when fed a false success, did more damage than doing nothing would have.

Retry. A retry policy fires on failure. A false success is not a failure, so retry stays asleep. The one feature whose entire purpose is to recover from a transient problem is blind to the most common transient problem the runner has.

Recovery and resume. Resume logic asks the previous run what it got through, then continues from there. If the previous run claims it got through everything, resume concludes there is nothing to continue. Worse, it may mark the milestone complete and move the pipeline forward, which turns a recoverable gap into a permanent one.

Idempotency keys. Keyed on the claim of completion, an idempotency guard now actively prevents the work from happening. The second attempt is suppressed as a duplicate of an attempt that did nothing.

Status display. The pane goes green. A human looking at green does not go looking for an empty diff.

Alerting. Alerts are built on error rates and non-zero exits. Both stay flat. The system is in a state where the more monitoring you add on that axis, the more confident everyone becomes.

The shape here is general and worth naming. If a system's checks all read a subject's self-report, then the checks are not independent of the subject. They are a louder version of it. Adding more of them raises confidence without raising reliability, and that gap is exactly where a bad milestone gets accepted.

The check has to come from outside the run

An independent check is one that would give the same answer whether the run reported success, reported failure, or reported nothing at all. That rules out anything the agent wrote about itself, including structured output, a self-grade, or a final summary message.

What is left is the workspace and the repository. Three questions, in increasing strength.

Do the files exist? The cheapest check, and the one that catches the rate-limit case cold. The task named an artefact. Is the artefact on disk, non-empty, and newer than the run start? A run that wrote nothing fails here immediately, whatever it claimed.

Is there a diff? Stronger, because it catches the case where files exist but were never touched. Compare the tree against the commit the run started from. An empty diff on a task that was supposed to change code is a failed run, full stop. This check is nearly free and it is the single highest-value gate we add to agent pipelines.

Do the tests pass? Strongest, and the only one that speaks to correctness rather than existence. It also costs the most and takes the longest, so it belongs after the two cheap gates, not instead of them.

Run them in that order and most false successes die at the first gate in milliseconds.

Where the gate goes matters as much as what it checks

A check that runs inside the agent's own process, as a final step in its instructions, is not independent. The agent that ran out of quota also ran out of quota before reaching its self-check. We have watched exactly that happen.

Put the gate in the orchestrator, after the run returns and before the result is recorded. The orchestrator holds two things the run does not: the starting state of the workspace, and the definition of what the milestone was supposed to produce. Those two facts are what make verification possible. The run has neither reliably.

Then make the gate's verdict, not the run's verdict, the thing everything downstream reads. Retry fires on the gate. Resume reads the gate. The status pane shows the gate. The run's own report becomes a diagnostic detail for a human reading logs, which is the only thing it was ever good for.

One more detail that is easy to miss. The gate needs to distinguish three outcomes, not two. Work verified. Work claimed but not present, which should retry. And work genuinely not attempted because of a known constraint, which should surface as blocked rather than failed, because retrying it immediately will hit the same constraint. Collapsing the last two into one bucket produces a pipeline that burns quota re-running something that cannot succeed yet.

Do the files exist
on disk, non empty, newer than the run start. The cheapest check and the one that catches the rate limit case cold
Is there a diff
against the commit the run started from. An empty diff on a task meant to change code is a failed run, and this is the single highest value gate
Do the tests pass
the only one that speaks to correctness rather than existence, and the one that costs real time, so it goes last
The gate's verdict is what everything reads
retry fires on the gate, resume reads the gate, the status pane shows the gate, and the run's own report becomes a log detail for a human
Three outcomes, not two
verified, claimed but not present which should retry, and genuinely blocked by a known constraint which should not
The orchestrator holds the starting state of the workspace and the definition of the milestone. The run reliably holds neither, which is why the boundary is where responsibility changes hands.
FigureAn independent check is one that gives the same answer whether the run reported success, reported failure or reported nothing, which rules out structured output, self grades and final summaries. Run these in this order and most false successes die at the first gate in milliseconds. The gate lives in the orchestrator, because a check written into the agent's own instructions is stopped by whatever stopped the agent.

Why this is not solved by a better runner

It is tempting to treat this as a defect to be reported and waited out. We think that is a mistake, for a structural reason.

The runner's job is to run a conversation with a model and a set of tools. Your job is to decide whether a milestone is done. Those are different jobs with different definitions of success, and the second one depends on information the runner does not have: what you wanted, what the repository looked like before, and what counts as acceptable. Even a perfectly honest runner that reported every rate limit as a failure would not tell you whether the change it made was the change you asked for.

So the independent check is not a workaround for a specific behaviour in a specific tool. It is the boundary where responsibility changes hands. Any system that accepts work from a component it does not control needs one, whether that component is an agent, a contractor, or a batch job from another team.

The honest limit

The rate-limit behaviour described here is a lab reproduction. We provoked it deliberately in a controlled workspace to characterise what the orchestrator sees. It is not a client incident report, and we are not claiming a figure for how often it occurs in normal operation. Our point is that once is enough, because the failure is silent and it propagates.

The gates have limits too. A file-exists check is trivially satisfied by an agent that writes a stub, and a diff check is satisfied by a diff that is wrong. Neither is a correctness check. Tests are closer, but tests only cover what someone wrote a test for, and an agent that can read your tests can sometimes satisfy them without solving the problem. Verification raises the floor. It does not establish the ceiling.

And the gate is itself code that can be wrong, or can be quietly disabled when it becomes inconvenient during a deadline. We have seen a verification step commented out with a note saying it was flaky, which is how a system returns to trusting the claim without anyone deciding to.

FAQ

Can an AI agent report success without doing anything? Yes. A headless run of one widely used agent runner that hits a rate limit mid-run can return a success status with exit code 0, having written no files and produced no diff. The runner handled the rate limit as a condition rather than an error, finished its turn cleanly, and exited normally. Nothing in that sequence is a bug, which is why nothing catches it.

Why does an exit code of zero not prove the work happened? An exit code says the process ended without an error it considered fatal. It says nothing about whether your task was completed, because the process does not know what your task was. Backing off from a rate limit, truncating a plan, or skipping a step are all ordinary completions from the process's point of view, and all of them exit zero.

What should I check instead of the agent's status? Three things, cheapest first. Do the expected files exist, are they non-empty, and are they newer than the run start. Is there a non-empty diff against the commit the run began from. Do the tests pass. The first two cost almost nothing and catch most silent failures. The third costs real time and belongs last.

Why does retry not fix a silently failed agent run? Because retry fires on failure, and a false success is not a failure. The run reports green, so the retry policy never triggers. The same applies to resume logic and idempotency guards, both of which read the previous run's claim of completion and conclude there is nothing left to do. Every feature that trusts the self-report repeats the false claim.

Where should the verification step live? In the orchestrator, after the run returns and before the result is recorded. A check written into the agent's own instructions is not independent, because whatever stopped the agent from doing the work may also stop it from reaching its own check. The orchestrator is the only place that holds both the starting state of the workspace and the definition of the milestone.

Does this mean agent output always needs human review? No. It means acceptance needs a check that does not read the agent's account of itself. That check can be fully automated, and should be, because an automated diff and test gate runs on every milestone while a human reviewer does not. Human review is for deciding whether the verified change was the right change, which is a separate question from whether anything changed at all.

Carrying a program like this one?

Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.

Bring us the program