This is the first of a series of design notes on long-horizon agents, prompted by a close read of BEACON (Zhejiang University, arXiv:2605.06078).
For an agent to hold up over a long task, we think two things have to be right:
- It keeps making progress toward the goal. That breaks into context compaction (what is safe to forget), milestone design (how you know a step is really done), and stall prevention (when it gets stuck, something has to notice).
- It can reflect and improve.
This post covers stall prevention, because that is the one that cost us the most.
The symptom
We had user reports, and saw it internally too: some very long tasks stop moving forward partway through.
Look at the logs and the agent is busy — reading files, searching, running commands, never idle. But half an hour later nothing has advanced.
Our first explanation was context loss: compaction dropped the record that a path had already been tried, so the agent went and tried it again. Reading the code and the logs together, that turned out to be only half the story.
We already had guards — three tiers of them
| Tier | Criterion | Thresholds |
|---|---|---|
| Exact repeat | Tool name + canonicalized args, byte-identical | LOOP_WARN=3 warn / LOOP_HARD=5 force-stop |
| Near-duplicate | Identical except volatile id / timestamp fields | NEAR_DUP_LOOP_WARN=6 / NEAR_DUP_LOOP_HARD=12 |
| Spin convergence | ≥2 compactions and ≥75% of the tool-loop budget burned | SPIN_CONVERGENCE_MIN_COMPACTIONS=2SPIN_CONVERGENCE_TOOL_LOOP_RATIO=0.75 |
The comment on that third tier reads, verbatim: Compound "may be spinning after context loss" signal. Someone had already seen this coming. So the problem was never that nothing was built — it was that what we built could not catch it.
The blind spot: all three are input-side detectors
The signature every tier keys on is tool name + canonicalized args. That answers exactly one question: are you doing the same thing twice?
But a capable model that is stuck does not repeat calls. It does this:
read file A → grep X → re-read A with a different line range
→ run a slightly different command → read file B → grep again …Every call has a distinct signature. All three tiers stay silent. And across that entire stretch, the number of verifiable state changes is zero: no file actually rewritten, no command producing a new result, nothing irreversible happening at all.
Loop detection is not stall detection. The first looks at inputs. The second has to look at outputs.
The paper supplies exactly that output-side definition
BEACON's in-segment reward:
r_t = R_ms · γ^(t_k − t) if the segment ends in a milestone
= 0 otherwiseA segment that does not terminate in a milestone earns exactly zero, regardless of how many actions it contained. Action count never enters the formula. That is the cleanest formalization of busy ≠ progressing we have seen.
The baseline layer is harsher still. The baseline is the group-average per-step return, so a segment that took 8 steps where the group averaged 5 has its entire advantage skewed negative, and the penalty scales with the overshoot. Spinning is not merely unrewarded; it is actively and proportionally penalized.
Figure 8 of the paper shows a failed trajectory where the last two actions receive an identical −2.20. That tail — the stretch after the last milestone that never reaches another one — is the mathematical shape of spinning. And it is common: trajectories that complete at least one subgoal but fail the task hold steady at 39–47% of samples.
One ablation rules out step-count triggers
| Partitioning | Score | vs. baseline (72.8) |
|---|---|---|
| Random 5-way split | 74.2 | +1.4 |
| Real milestones | 91.4 | +17.2 |
Splitting by arbitrary step counts is worth almost nothing. Splitting by real structure is worth a lot.
Now look back at our third-tier criterion — 75% of the tool-loop budget consumed. That is an arbitrary-step-count trigger. It asks how much you have burned, not what you have achieved. The right shape is:
✗ if steps > N → intervene
✓ if steps > N AND zero verified milestones → interveneThe second will not misfire on a legitimately long task that is progressing, because such a task hits milestones along the way. The first will.
There is also a positive feedback loop
Put the constants side by side. Compaction triggers at 82% of the context window. Spin convergence requires two compactions plus 75% of the loop budget before it fires.
spin → context fills → compaction fires → durable state summarized away
→ re-derive what was lost → spin moreThe spin detector infers spinning by observing that compaction happened repeatedly — but compaction is the very step that causes the amnesia. It is detecting a downstream symptom, and it has to wait for the loop to go around twice.
Worse, its intervention is to nudge the model to re-anchor on its durable state. If the durable state is precisely what was compacted away, there is nothing left to re-anchor on. This is a prompt-layer patch for a state-layer problem.
What we are adding: output-side stall detection
A fourth tier, built from two counters. Both are derived mechanically from tool observations the host already records; neither needs model judgment.
Steps since the last verified milestone. The simplest possible progress metric, used in the compound criterion above rather than alone.
Deduplicated new state changes. The more useful of the two:
- A file read whose content hash matches a previous read is not new information — re-reading the same file at a different line range leaves the hash unchanged.
- A command whose name, exit code, and output hash all match a previous run is not new information.
- A write where the after-hash equals the before-hash means nothing was actually written.
This counter targets exactly the case signature matching misses: every action different, information gain zero. The criterion is mechanical and needs no semantic understanding.
Then the pressure should be continuous rather than a single nudge:
surface the counter in context so the model can see it
→ force a plan revision (admit this path is dead)
→ ask the user
→ abort, but preserve the milestones already achievedThat last rung matters. If the run is going to stop, it should stop holding what it earned — which is the same 39–47% of partial progress the paper measured being thrown away.
What the paper does not give us
BEACON is a training method. It shapes gradients so the trained policy is less prone to wandering, but it has no runtime detection or intervention mechanism of its own. It supplies a definition of progress, not a controller. Thresholds, escalation ladders, and abort conditions are ours to design.
Its per-step baseline also needs a group-average segment length as reference. In production a given user task usually runs exactly once, so there is no group. The best available substitute is historical statistics over similar tasks, which is considerably noisier and carries none of the paper's variance-isolation guarantee.
The first move is to measure, not to fix
Before changing any decision logic, we want to instrument and answer a question we currently cannot: of the stalls happening in production, how many are amnesia-type and how many are no-gradient-type?
- Mostly accompanied by information gain at zero while call signatures all differ → no-gradient-type. The existing three tiers structurally cannot catch it, and output-side detection is the fix.
- Mostly accompanied by re-reading content that was already compacted away → amnesia-type. What needs fixing is what compaction preserves.
Those two conclusions call for completely different investments. Measuring first is cheaper than designing first, and the instrumentation is nearly free because the observations already exist.
Everything above depends on one notion this note has been leaning on without defining: a verified milestone. The next note is about that. Why a step being declared complete is not evidence that it is, which two halves already exist in the product without ever being connected, and one criterion the paper does not have: look at irreversibility, not importance.