The Cycle Counter - Capping a Two-Agent Loop Before It Runs Away

The problem

I ran twenty synthetic pull requests through a code-review setup where a Reviewer agent and a Fixer agent work a queue together, like two coworkers who each think the other one is holding up the release. Most of the twenty resolve inside one or two passes. Reviewer flags a bug, Fixer patches it, Reviewer confirms the patch, the PR closes, everybody moves on. I seeded a handful of the twenty differently on purpose, deliberately underspecified, built to be genuinely unresolvable by the pair without someone else weighing in.

Nothing in the graph connecting Reviewer and Fixer knows that some of these PRs are unresolvable. The edge between them carries no counter and no exit condition other than mutual agreement, so on the ambiguous PRs, the two agents just keep going. Reviewer restates the same ambiguity in slightly different language each round. Fixer answers it a different way each time, occasionally flapping between two candidate fixes that each subsequent Reviewer pass then rejects for having reintroduced the other one's problem. It looks, if you read the transcript, exactly like a couple arguing about whose turn it is to do the dishes, except the argument doesn't end when someone gets tired.

Here's the part that makes this more than an edge case to shrug off. Nothing here is a bug in the ordinary sense. Reviewer is doing exactly what a reviewer does: reading a diff, checking it against the reported issue, responding. Fixer is doing exactly what a fixer does: reading Reviewer's note, producing a patch. Both agents behave correctly on every single turn, and the pair as a whole still burns tokens, dollars, and wall-clock time on a PR that was never going to close through this channel, because nothing in the system distinguishes a productive cycle from a stuck one until someone happens to notice the cost.

I have a real-world number to put next to my synthetic one, and it's the strongest confirmation I've found for anything in this space. A postmortem from November 2025 describes a four-agent pipeline where two of the nodes, an Analyzer and a Verifier, traded requests with no cap and no termination check. It ran eleven days before anyone noticed the $47,000 in billing it had racked up. Same shape as my ReviewLoop pair, at production stakes instead of synthetic ones: two agents, one edge, no counter, no exit condition other than a mutual agreement that never arrived.

The pattern

What I built is a hard cap on the number of handoff cycles between two specific agents, a counter scoped to that one edge, that triggers automatic escalation to a human the moment it's breached. I tracked, per PR, dollars and time spent before resolution or runaway, the handoff-cycle count, and the percentage of PRs exceeding a threshold I set at three cycles without either resolving or escalating.

The mechanism itself is refreshingly ordinary once you see it work. The Reviewer-Fixer edge carries a counter now. On the fourth attempt, instead of allowing another Fixer pass, conditional routing sends the case to an escalation path that writes full context, the diff, the review history, every prior fix attempt, to a human-review queue. The ambiguous PRs still show up as ambiguous. What changes is that the system stops paying to discover that fact over and over. A two-agent conversation will happily run forever when nothing tells it when to stop, and once something does, the same conversation resolves on a predictable budget instead of running away.

The result on my twenty PRs was dollars and time spent per PR capped and predictable, with an escalation rate that matched the count of deliberately ambiguous PRs I'd seeded into the batch, and no PR exceeding the cycle threshold without a logged escalation. That's the outcome the cap is built to produce, and I want to be precise about what it actually proves. The ambiguous PRs didn't resolve any faster. What stopped was the unbounded cost of discovering, over and over, something a bounded number of cycles was already enough to reveal: this pair, on its own, isn't going to close this one.

Design considerations

A cycle cap forces termination, but it has no way to tell a genuinely unresolvable disagreement apart from one that simply needed a fourth cycle to close cleanly. Three was a judgment call I tuned to this specific Reviewer-Fixer pair, not a number derived from anything structural about the task, and getting it wrong in either direction costs something real. Set the cap too tight and you escalate cases a fifth cycle would have closed on its own, which turns a working loop into unnecessary human load. Set it too loose and you still burn real cost before the cap ever fires, which is the exact failure the cap exists to prevent, just delayed. There's no shortcut around tuning this against the actual rhythm of the two agents involved. A cap borrowed from a different pair, with a different typical resolution time, is a guess wearing the clothes of a measurement.

I also scoped this deliberately to a single edge between two named agents, and I think that scoping is the honest boundary of what this mechanism can do. It has no purchase on a decentralized negotiation network where several agents are proposing and countering each other's positions with no single edge to attach a counter to. A three-or-more-agent cycle, where ownership diffuses across several handoffs instead of bouncing between two fixed parties, needs a different kind of tracking, one that can see the whole cycle rather than one segment of it. Trying to bolt a single-edge counter onto that shape just hides the runaway behind a counter that's watching the wrong thing.

The other honest limit is what happens after escalation. A cap that routes to a human queue only works if a human is actually watching that queue. I've seen teams build the escalation path, feel good about having built it, and then let the queue back up unattended, which quietly turns a visible cap into a slower version of the same silent runaway it was meant to prevent. The cap converts an invisible cost problem into a visible staffing problem. That's real progress, but it's not the same as solving the problem, and treating it as solved the moment the counter exists is exactly how a team ends up with a very well-instrumented queue nobody is actually clearing.