While working on the GitHub adapter, a gateway that lets AI agents create pull requests on GitHub, the source_state field first looked like a small technical detail.
It was not the operation itself, or the target. It was only a reference to the state the agent had seen before proposing a change.
But after working through the write path, this field started to look less like metadata and more like part of the safety model. A proposed change is not only defined by what it wants to do. It is also defined by the state in which that proposal made sense.
This is easy to miss.
An agent can read a repository, produce a reasonable change, and submit a clean intent. Nothing about that has to be wrong. But while the agent is planning, the repository can move. A human can push a fix. Another workflow can update the same file. A branch can advance.
In that case, the agent may still be reasoning correctly over the state it saw.
The problem is that this state no longer exists.
The reasoning was right, but the world shifted.
That is the stale state problem in agent workflows. And it is why I think agent workflows need state-bound intent.
The illusion of a static world
From the outside, even from the boundary's point of view, a stale request can look just like any other: the operation has the same name, the target path is still allowed, the input is still well formed.
But it is not. The proposal belonged to an older state of the repository, formed before the branch moved, before the file changed, before another workflow created a related result.
This is why stale state is not only a data freshness problem. For agent workflows, it becomes an admission problem: a decision about whether a proposed change is allowed to become a real effect. We call that decision point an MCP Boundary: the same pattern behind the GitHub adapter and the wider work we do on MCP gateways. The boundary should not only ask whether the operation is allowed on the target. It should also know whether the target is still in the state that made the operation reasonable.
The write happens now.
The reasoning happened before.
That gap is where the problem appears.
State binding
The intent should not float freely between the agent and the system that later creates impact. It should carry the state reference it depends on, a practice we call state binding: attaching a proposed effect to the exact state it was reasoned about.
For a GitHub adapter, this can be the branch head or a file hash. In other systems, it may be a ticket status, a data version, or a configuration version. The exact field is less important than the relation it creates.
The request is no longer only:
apply this change to this target.
It becomes:
apply this change to this target, if the target is still in the state this change was based on.
That small addition changes the meaning of the request. The boundary can now reject the drift instead of guessing through it. If the state still matches, the request continues through the normal checks. If the state moved, the request should stop before impact.
This does not mean the agent gets to declare that the state is safe. The state reference has to be checked by the boundary, or by the trusted system behind it. Otherwise it would only be another claim from the agent.
The agent can say what it saw.
The boundary has to verify whether that is still true.
This is not a new idea
The underlying idea is old. Git has base commits. Databases have version checks. HTTP has ETags. Security people know the broader class of problems around time of check and time of use.
So the point is not that agent systems discovered stale state. The point is that agents make the read-to-write gap wider and less predictable.
An agent reads, plans, calls tools, revises, retries, and only later submits a proposed effect. During that gap, the target system keeps moving. A change that was safe five minutes ago may be wrong now, not because the goal changed, but because the state around the goal changed.
That makes state-bound intent more than a backend detail. It becomes part of the boundary. Agent requests should not only be checked by operation, target, and policy. They should also be checked against the state that produced them.
When the state moved
When the agent submits an intent, the boundary compares the submitted state reference with the current target state.
If they match, the request can continue. It may still need review, be outside the allowed scope or be blocked for other reasons. But at least it is still attached to the state the agent used.
If they do not match, the request should stop before impact.
A stale request is not a malformed request. It is not an unauthorized request. It is not necessarily a forbidden goal. It is a request whose basis expired.
So the safe next action is not to retry the same write with slightly different arguments. It is to read the target again and submit a new intent based on the current state.
This is where boundary feedback matters. The system should say, in a structured way, that no impact happened because the state reference was stale:
{
"decision_status": "conflict",
"outcome_status": "no_impact",
"reason_code": "stale_state_reference",
"source_state": "sha:abc123",
"current_state": "sha:def456",
"required_next_action": "re_read_target_state",
"retryable": false
}
The important part is not the JSON format.
The important part is the next action.
The boundary is not saying that the goal is forbidden. It is saying that this request no longer belongs to the current state of the target.
Where the binding is less clear
Not every operation has a clean state reference. Some targets are easy to bind: a Git commit, a file hash, a database row version. Others are harder. A draft email may depend on context that is difficult to reduce to one stable token.
In those cases, the system should be honest. If the state cannot be bound well, the boundary may need to be stricter. That connects back to reach: the less precisely you can bind the state behind an effect, the more careful admission should become.
Why this matters for autonomous work
A human often notices when the world has moved. A developer sees that the branch changed. A support worker sees that the ticket was already closed.
Agents do not carry that situational judgement automatically. They can continue from an earlier observation with full confidence, even when it is already outdated, because the reasoning is still coherent relative to the old state.
That is exactly the issue.
Good reasoning over stale state can still produce bad impact.
State binding does not fix this by making the agent correct. It does not prove that the code is good, that an email is wise, or that a data update is meaningful. Tests, review, and engineering judgement remain necessary.
The claim is narrower. Without the state it was based on, the boundary cannot know whether it is admitting the request the agent actually reasoned about.
A request is not only an operation against a target.
It is an operation against a target, based on a state.
If that state changed, the request changed too.
No state binding, no safe admission.
Project: Impact Boundary Labs
Top comments (0)