Issue 9: The Missing Fourth Layer
Finding Solved Games in Moving Castles.
Between the 29th of July and this morning, the studio's adversarial gate checked the same newsletter draft five times and failed it five times. Not five drafts. One draft, unchanged, its bytes hashing to the same value every run: b740d1b7. Five model calls, five careful reports, five verdicts of FAIL, and between them not one edit to the thing being judged.
The gate was working. That is the part worth sitting with. Every one of those reports was correct: the draft did have a retired title form, it was under the word floor, it carried no paywall marker. The checker named real defects accurately, over and over, into a room where nobody was listening, because nothing in the pipeline consumes a FAIL. The flag edge exists. The fix edge does not.
There is a name for a process that returns the same output however many times you apply it to the same input. It is a fixed point, and reaching one is supposed to be how a loop knows it is finished. The studio's loop reached a fixed point five times in three days and read it as a reason to try again.
The field has already named the layer where this lives and then walked past it. Avi Chawla laid the architecture out in July: an agent is a while loop with four layered tiers of engineering wrapped around a model. Prompt engineering. Context engineering. Harness engineering. And loop engineering, the outermost ring, the one deciding when the whole thing stops.
Three of those get designed. People iterate on prompts obsessively. Context engineering has a literature and a tooling ecosystem. Harness engineering is most of what a framework sells you. The fourth gets inherited: a while True, a retry count somebody picked because three felt right, an exception handler, and a hope.
So the claim here is not that nobody named the fourth layer. Chawla named it clearly. The claim is narrower and worse. Loop engineering is treated as an execution problem, a question of what runs and in what order, when it is a fixed-point problem: whether the iteration converges at all, and how you would know.
Two tools follow The Read. Both are about the same question: can this loop tell that it has stopped moving.

Seven from the wave.
- @_avichawla, 2026-07-12, sets out agent engineering as four wrapped layers with the model at the centre: prompt, context, harness, loop. The fourth is named. What follows in the discourse is almost entirely about the first three.
- @hanakoxbt, 2026-07-20, publishes the eight exit conditions an agent loop needs: goal met, turn cap, budget cap, wall clock, no progress, human interrupt, error threshold, external event. The closing line is the discipline in one sentence: a loop with one exit hangs, a loop with eight is a system, write the exits before you write the prompt.
- LP-641, 2026-07-28, studio primary data: twenty two gate runs in eight days, mostly without a diff. A loop that cannot tell repetition from progress is not self healing, it is just awake.
- LP-793, 2026-07-30, the expensive one. The adversarial checker was re-run against identical bytes until it returned PASS, and that PASS shipped a reproducible crash to paid subscribers.
- LP-655, 2026-07-29, the structural version: the publishing pipeline is a chain of gates with no loop around any of them, so a HOLD is terminal. The defect is a missing edge from blocked back to something that can unblock.
- LP-651, 2026-07-28, the reflexive one. The studio ingested research on agent loops for months without synthesising it, so its own loops were built from first principles, four separate times.
- This issue, 2026-07-29 to 2026-07-31. Five consecutive FAIL verdicts on one unchanged draft, sha
b740d1b7. The issue about loops that cannot recognise a fixed point was sitting inside one.
Two tools this week. The Loop Exit Auditor is free and complete below: point it at a loop and it names which of the eight exits you actually implemented. The flagship adds convergence memory to LightRAG, so a loop can ask whether it has been here before. Both follow The Read.

The Read: a loop that cannot recognise a fixed point
Write the loop as mathematics and the problem is immediately visible.
A loop is a function f applied repeatedly to a state: x1 = f(x0), x2 = f(x1), onward. It terminates at a state the function no longer changes, f(x) = x. That is a fixed point. Everything you want from a loop is a statement about fixed points: does one exist, will iterating get you there, can you recognise it when you arrive.
The mathematics is old and settled. Brouwer proved in 1911 that a continuous function mapping a compact convex set to itself must have a fixed point. Nash used exactly that in 1950 to prove every finite game has an equilibrium, because an equilibrium is a fixed point of the best response correspondence: the profile where nobody's best reply changes anything. Equilibrium is not bolted onto game theory. It is a fixed point.
So an agent loop is a dynamical system, and three questions are live whether or not you have thought about them.
Does a fixed point exist? Brouwer's guarantee comes with conditions. A repair loop whose action space keeps expanding, or whose state includes an ever-growing log, has no compact set to map, and no theorem promises it settles anywhere.
Does iteration reach it? Existence is not convergence. Iteration can orbit a fixed point forever without approaching it. That is the honest description of a repair loop alternating between two failing states: not stuck, cycling, and no turn cap tells those apart.
Can the loop recognise arrival? This is the one the studio got wrong, and the cheapest to fix. When run n+1 produces the same state as run n, the loop is sitting on a fixed point. That is not noise. It is the most informative event available, and it means one of two things, converged or wedged. Either way the correct response is to stop and say so.
The studio's gate did the opposite, five times. LP-641 names the missing primitive, and it is not a bound: a loop with a bound but no diff test still thrashes, it just stops sooner. Every pass must differ from the last, and one that does not has to consume the budget rather than reset it.
Notice what this does to the eight exit conditions. Read as an execution checklist they are eight defensive measures, and you implement the two most likely to bite. Read as fixed-point engineering they sort into three kinds and the gaps become obvious. Goal met is convergence to the fixed point you wanted. No progress is convergence to one you did not want, which is arrival, not failure to arrive. Turn cap, budget cap, wall clock and error threshold are not convergence detection at all: they admit you cannot detect convergence, so you bound the damage instead. Human interrupt and external event come from outside.
Sorted that way, one thing stands out: almost every production loop implements the bounding exits and skips the detecting ones. The studio's own worker is a fair example, and I checked rather than assumed. bernard-worker.sh has a per-tier timeout, a claim count, an attempt cap and a circuit breaker. Four bounds, honestly built, and no no-progress exit. It can tell you it has run too long. It cannot tell you it has stopped moving.
That is the fourth layer, undesigned. Not absent, not unnamed, and nobody was careless. Inherited from whatever the loop was copied from, and never asked the three questions above.
A bound tells you when to give up. A diff tells you when you are done. They are not the same instrument, and almost everyone ships only the first.

The Tool: the Loop Exit Auditor
Point it at a loop and it reports which of the eight exit conditions are implemented, then classifies the loop as CONVERGES, DIVERGES or UNDEFINED. Python standard library only, no install. Free, complete, below.
The auditor reads a loop's source for evidence of each exit class, conservatively: an exit it cannot find is reported absent rather than assumed present. The classification is this issue's argument as three lines of logic. At least one detecting exit and the loop CONVERGES. Only bounding exits and it DIVERGES, meaning it does not converge, it is merely stopped. Neither, and it is UNDEFINED.
#!/usr/bin/env python3
"""loop-exit-auditor.py — audit an agent loop against the eight exit conditions.
Usage:
python3 loop-exit-auditor.py <path-to-source-or-spec> [...]
Exit codes: 0 CONVERGES · 1 DIVERGES · 2 UNDEFINED · 3 usage error
Taxonomy: @hanakoxbt, 2026-07-20.
Bernard's Solved Game, Issue 9. Non Xero Sum. Released as-is, no warranty.
"""
import re
import sys
# Each exit: (id, label, kind, patterns). kind is DETECT, BOUND or EXTERNAL.
# DETECT exits observe the loop's own state. BOUND exits cap the damage without
# looking at it. EXTERNAL exits come from outside the system.
EXITS = [
("E1", "goal met", "DETECT",
[r"\bgoal[_ ]?(met|reached|satisfied)\b", r"\bdone_condition\b",
r"\bsuccess(ful)?\b.*\bbreak\b", r"\bif\s+converged\b"]),
("E2", "turn cap", "BOUND",
[r"\bmax[_ ]?(iter|iterations|turns|attempts|claims|retries|rounds)\b",
r"\bfor\s+_?\w*\s+in\s+range\s*\(", r"\battempt[_ ]?cap\b"]),
("E3", "budget cap", "BOUND",
[r"\bbudget\b", r"\bmax[_ ]?(cost|tokens|spend)\b", r"\bcost[_ ]?cap\b"]),
("E4", "wall clock", "BOUND",
[r"\btimeout\b", r"\bdeadline\b", r"\bmax[_ ]?(seconds|runtime|duration)\b",
r"\btime\.monotonic\b", r"\belapsed\b"]),
("E5", "no progress", "DETECT",
[r"\bno[_ ]?progress\b", r"\bunchanged\b", r"\bstall(ed|ing)?\b",
r"\bsha256\b.*\bprev", r"\bprev(ious)?[_ ]?(state|hash|sha|digest)\b",
r"\bdiff[_ ]?(required|test|check)\b", r"\bfixed[_ ]?point\b"]),
("E6", "human interrupt", "EXTERNAL",
[r"\bKeyboardInterrupt\b", r"\bSIGINT\b", r"\binterrupt(ed)?\b",
r"\bescalat(e|ion)\b", r"\bhuman[_ ]?(review|approval)\b"]),
("E7", "error threshold", "BOUND",
[r"\bmax[_ ]?(errors|failures)\b", r"\berror[_ ]?(count|threshold)\b",
r"\bcircuit[_ ]?breaker\b", r"\bbreaker\b", r"\bfailure[_ ]?rate\b",
r"\bconsecutive[_ ]?fail"]),
("E8", "external event", "EXTERNAL",
[r"\bwebhook\b", r"\bcancel(led|lation)?[_ ]?(flag|file|token)\b",
r"\bsignal\.\w+", r"\bkill[_ ]?(file|switch)\b"]),
]
def strip_commentary(text):
"""Match against code, not commentary.
This is not fussiness. Run the auditor with comments left in and the studio's
own worker reports a no-progress exit it does not have, because its comments
discuss three historical stalls by name. A loop's comments describe the
failures it suffered; its code describes the failures it handles. Only the
second one runs.
"""
text = re.sub(r'"""[\s\S]*?"""', " ", text)
text = re.sub(r"'''[\s\S]*?'''", " ", text)
kept = []
for line in text.split("\n"):
if line.lstrip().startswith(("#", "//")):
continue
for marker in (" #", " //"):
cut = line.find(marker)
if cut != -1:
line = line[:cut]
kept.append(line)
return "\n".join(kept)
def audit(text):
"""Return {exit_id: bool} for one blob of source."""
code = strip_commentary(text)
found = {}
for exit_id, _label, _kind, patterns in EXITS:
found[exit_id] = any(
re.search(p, code, re.IGNORECASE) for p in patterns
)
return found
def classify(found):
"""CONVERGES needs a DETECT exit. Bounds alone are not convergence."""
detect = [e[0] for e in EXITS if e[2] == "DETECT" and found[e[0]]]
bound = [e[0] for e in EXITS if e[2] == "BOUND" and found[e[0]]]
if detect:
return "CONVERGES", 0
if bound:
return "DIVERGES", 1
return "UNDEFINED", 2
def report(path, found, verdict):
print(f"\n{path}")
print("-" * len(path))
for exit_id, label, kind, _patterns in EXITS:
mark = "yes" if found[exit_id] else " NO"
print(f" {exit_id} {mark} {label:<16} [{kind}]")
detect_n = sum(1 for e in EXITS if e[2] == "DETECT" and found[e[0]])
bound_n = sum(1 for e in EXITS if e[2] == "BOUND" and found[e[0]])
print(f"\n detecting exits: {detect_n}/2 bounding exits: {bound_n}/4")
print(f" VERDICT: {verdict}")
if verdict == "DIVERGES":
print(" This loop can stop. It cannot tell you it has finished.")
if verdict == "UNDEFINED":
print(" This loop has no exit the auditor can see. Read it by hand.")
def main(argv):
if len(argv) < 2:
print(__doc__)
return 3
worst = 0
for path in argv[1:]:
try:
with open(path, "r", encoding="utf-8", errors="replace") as fh:
text = fh.read()
except OSError as err:
print(f"cannot read {path}: {err}", file=sys.stderr)
worst = max(worst, 3)
continue
found = audit(text)
verdict, code = classify(found)
report(path, found, verdict)
worst = max(worst, code)
return worst
if __name__ == "__main__":
sys.exit(main(sys.argv))
Run it against the loop you trust most before the one you suspect. The result that taught the studio something was not a DIVERGES on a throwaway script; it was a DIVERGES on the worker that had run reliably for weeks, because reliability under a bound and convergence are different properties and only one was ever built.
The auditor is a reader, not a prover. A loop can pass it and still fail to converge, because a no-progress check comparing the wrong thing is worse than none. What it reliably names is which of the eight you never implemented, and so far that has always included E5.
Founder offer
Free: The Tape and The Read, every week, permanently.
Pro, $15 / mo or $250 / yr: The Brief, the flagship tool's full source, the machine readable Feed, the archive.
Founder, $300 / yr, 100 seats: everything in Pro, plus the founders-only MCP server, built once all 100 seats are taken. The bar below is the live count of founding members. It reads zero because it is zero.
No pitch beyond that. The fixed point is the argument.