Two AI agents can undo each other’s fixes. The race was in a file write.
Published
Recovery workers · race diagram
Check first is not write protection
In the temporary-state replay, a competing writer publishes after the cleanup helper's last absence check. Exclusive creation preserves that record; an overwrite-capable write would replace it. Conceptual ordering, not a historical production trace or a whole-system transaction.
- OBSERVE Cleanup sees no record The first check finds no resolution for the exact hold-file bytes.
- PREPARE Quarantine + request kickstart These state changes happen before the final resolution create; a later refusal does not undo them.
- CHECK Last check sees absence The observation does not reserve the filename.
- OTHER WRITER Recovery record appears The test publishes the competing record immediately after that check.
- BRANCH x refuses · w would overwrite FileExistsError leaves the existing bytes unchanged; this is not complete-file publication.
- VERIFY Compare the winner's bytes Temporary-state regression only; no claim of universal idempotence or unattended reliability.
One helper had already recorded a genuine recovery. Another replaced that record with its own cleanup result. The file still said “resolved,” but it no longer preserved the resolution it was supposed to represent.
That happened in my content automation on October 16. It is the less cinematic version of “two AI agents undoing each other’s fixes”: competing automation helpers and recovery workers writing the same resolution record. It is not evidence of two language models arguing, rewriting each other’s reasoning or deleting commits.
The useful question was not which model needed a better instruction. It was: what stops a later writer from replacing a result that another worker has already published? Checking that the file doesn’t exist is not enough.
What the record was protecting
My blog-factory build log describes a first-failure hold: a durable stop that requires matching recovery evidence, not just time passing or another cycle starting.
A separate resolution file—often called a sidecar—connects that hold to a recovery receipt. Its identity is tied to a SHA-256 digest of the exact hold-file bytes. A receipt for a different hold must not resolve this one.
“Same hold” here means the same bytes, not merely similar JSON. Changing whitespace can change the file digest even when the parsed values look identical. The script also retains a quarantine copy and checks that its bytes hash to the original hold’s digest.
That binding answers an identity question. It does not, by itself, prove that a recovery succeeded or that everything described by a receipt is true. Those claims still need their own checks.
The incident had a classification error too
The retained October 16 incident record describes a cleanup helper treating a controller failure as an incomplete seed-related hold. A genuine whole-cycle recovery had already completed; the later cleanup replaced its resolution sidecar with a quarantine resolution.
The distinction matters. A seed-related lane_failure needs the failed input binding, evidence location and cycle identity for its recovery path. If required identity is missing, that is an incomplete—or “skeleton”—hold.
A controller_failure does not require that seed binding. Its absence is not evidence that the hold is broken. Calling every missing field a defect gave the cleanup helper authority over work that belonged to another recovery path.
The current predicate limits skeleton handling to the relevant seed-binding kind with incomplete required identity. It leaves controller failures, cycle errors and unknown kinds to their own recovery paths. There is also a narrow legacy case for a hold with no kind and none of the recovery identity fields; that is not permission to clear every unfamiliar record.
The other protection is just as important: an existing resolution record must survive this helper byte-for-byte. An early existence check avoids starting the quarantine when the record is already there. But another writer can arrive after that check.
The race, in order
The temporary-state regression makes that last window deterministic. It exercises the actual current script, with a test hook that publishes a competing record at the dangerous point. It does not launch two language models or rely on getting lucky with process timing.
Here is the order the test forces:
- The cleanup helper reads an eligible incomplete hold. It derives the hold-file digest and checks the matching resolution path. No file is there.
- It performs the quarantine work. The current sequence preserves the hold copy, writes an incident receipt, removes the active hold and requests a supervisor kickstart. These actions happen before the final resolution write.
- Its last existence check observes no resolution file. That observation is true at the instant it is made.
- The competing writer publishes its resolution. The test inserts the expected record immediately after that last check has observed absence, while the helper still receives the answer “not present.”
- The cleanup helper tries to create its own record at the same path. Opening with
wwould truncate the competing record and replace its bytes. Opening withxinstead raisesFileExistsError; the handler leaves the competing record unchanged.
This is time-of-check to time-of-use, or TOCTOU. The check describes an earlier instant. It does not reserve the filename until the write.
Adding another exists() check only moves the gap. The protection has to be enforced by the create operation itself.
The small code change—and its boundary
Python’s w mode creates a file if needed but truncates one that already exists. Its x mode requests exclusive creation and fails if that path already exists.
This generic example shows the pattern, not an instruction to run my recovery helper:
import json
from pathlib import Path
def create_record_if_absent(path: Path, record: dict) -> bool:
try:
with path.open("x", encoding="utf-8") as handle:
json.dump(record, handle)
except FileExistsError:
return False # A record exists; this attempt did not replace it.
return True
A False result is not “recovery succeeded.” It means this attempt did not create the file. The caller still needs to establish what the existing record means and whether it belongs to the right hold. Other I/O errors are not converted into a successful result.
There is a larger boundary too. Exclusive creation protects this create attempt from clobbering an existing file. It is not an atomic transaction across the whole recovery sequence. In the tested path, quarantine and a kickstart request have already happened before the final create. Refusing that create does not roll those actions back.
Nor does x make the complete JSON appear all at once to readers. A crash can leave an incomplete file, and a reader can encounter it while it is being written. Complete-file publication, durability, validation and coordination across other state changes need their own design. Another writer using an overwrite-capable operation can still replace the record later.
What the red and green runs actually prove
The retained article-preparation run of the current script passes all nine temporary-state tests. Those cover eligible skeleton handling, repeated invocation, the narrow legacy case, complete seed-related identity, non-seed kinds, existing-record preservation, malformed input, absent input, partial identity and the injected race. Some tests exercise several cases.
For the comparison, the preparation pass made a deliberately unsafe synthetic copy of the current source, changing only the final resolution open mode from x to w. Against that copy, the targeted race test fails: the competing writer’s bytes no longer match the expected record.
That is a current-copy counterfactual, not recovered old deployed source. It demonstrates the overwrite mechanism and why the current exclusive create matters. It does not establish the exact historical instruction sequence or scheduling interleaving of the October 16 incident.
The real incident and the controlled comparison answer different questions: the private incident record documents a genuine resolution being replaced; the temporary-state regression isolates the check-then-write window. I don’t want to blend them into a cleaner history than the evidence supports.
These runs are not evidence of natural unattended reliability, a fleet-wide deployment or universal idempotence. Repeating an isolated test successfully does not establish how every future recovery will behave.
The checklist I’d use on another agent workflow
You don’t need my pipeline to look for this failure. Before letting multiple helpers repair shared state, I would ask:
- Who can write the same record? Include scheduled cleanup, retries and recovery workers—not just the agent currently visible in chat.
- Is a field actually required for this record’s kind? Missing seed identity and a non-seed failure are different conditions. Don’t clear the latter just because it lacks the former’s fields.
- What exact evidence identifies the work? If the contract binds file bytes, preserve and compare those bytes. Don’t silently substitute reserialized JSON or a friendly label.
- What happens when another writer wins after the last check? Use a no-replace operation where the record must be preserved; don’t mistake
exists()for write protection. Audit the other writers too. - Does the test force the dangerous ordering? In isolated temporary state, put the competing record between the check and create, then assert its bytes remain unchanged. A passing exit code is not that assertion.
- What does the result leave unproved? Check partial writes, crashes and other state transitions separately. Keep regression evidence distinct from installation and later natural-operation evidence.
This is why I separate the model, agent and tool layers in my AI setup. The model can propose a sensible plan while the tool operation still permits the wrong state transition. My Hermes updater story has the related lesson: a green test can enforce an unsafe assumption if the assertion asks the wrong question.
Here, the useful assertion was small and concrete: the competing resolution record must retain its exact bytes. The improvement was not another request for the helpers to cooperate. It was making this writer refuse to replace the other writer’s record.
Sources and evidence limits
The incident account comes from the retained private October 16 repair record. The mechanism and test claims come from the actual current autoclear source, its temporary-state tests, and the retained paired comparison logs reviewed for this article on October 17, 2026, EDT. Those operational artifacts are not public downloads. Private paths, machine identities and record hashes are omitted; the illustration is a conceptual race diagram, not a production trace.
For the public language contract, see Python’s open() modes and FileExistsError. Those references explain the operation; they do not independently verify my incident. Nothing here calls for running an autoclear tool against live holds.