# Runaway agents: detecting repetition and bounding work

An agent can keep working without making progress. It might repeat a failing command, alternate between two unsuccessful approaches, or keep revising an already adequate answer because a hook refuses to let it stop. Preventing this requires several controls. A repetition detector recognizes a pattern; a budget limits expenditure whether or not the pattern is recognizable.

Consider a task to fix a test. The agent runs the same test three times, receives the same missing-dependency error, and changes nothing between runs. A repeated-call detector might intervene early. A twenty-step budget eventually stops the task even if the agent changes the command spelling on each attempt. Neither control establishes that the task succeeded. The final state must distinguish an answer from exhausted or interrupted work.

This page compares the pinned [Codex](../../snapshots/codex/8c076942cb70/chapter.md), [pi](../../snapshots/pi/482c1381a00d/chapter.md), [Goose](../../snapshots/goose/34a222e3f2b9/chapter.md), and [Sidekick](../../snapshots/sidekick/463d653124e1/chapter.md) implementations. Sidekick source links require repository access. Findings about missing detectors are limited to the inspected paths; no behavioral benchmark was run.

## What each limit actually measures

| Control | What it can stop | What it does not establish |
|---|---|---|
| Repeated-call detector | Identical or equivalent calls without useful change | That different calls are making progress |
| Model-step budget | Too many model/tool continuations | A bounded number of tools inside one batch |
| Token or spending budget | Excessive model usage | A short wall-clock duration or safe external effects |
| Tool timeout | One operation waiting too long | That its subprocess or remote side effect stopped |
| Overall deadline | Work continuing beyond an elapsed-time allowance | Whether an interrupted mutation completed |
| Stop-hook cap | A hook repeatedly rejecting completion | Correctness of the answer accepted after the cap |

These controls need separate counters. Compaction makes another model request fit; it does not replenish a task's spending allowance. Transport retry repeats a failed request; successful but useless tool calls need different feedback. A model's repetition-penalty sampling parameter is also different from a harness inspecting repeated actions.

## The four implementations

### Codex: continuation hooks and separate usage accounting

Codex's `run_turn` can continue for tools, queued input, compaction, or a blocking Stop hook. A hook that supplies a continuation prompt causes that prompt to enter history and the loop to continue. Subsequent hook calls receive `stop_hook_active`; the inspected branch does not implement a numeric repeated-hook cap. An empty blocking response produces a warning instead of an empty continuation. This is a concrete extension boundary where a host must understand how its hook eventually releases the task. [Stop continuation][C1] [Hook request][C2]

Configured rollout accounting is another boundary: `record_rollout_budget_usage` returns `SessionBudgetExceeded` when its shared budget reports exhaustion. This is usage enforcement, not a semantic detector of repeated commands, and its presence should not be read as a universal default step cap. [Budget enforcement][C3]

Transport recovery has its own policy. `handle_response_stream_error` normally uses a retry count, but an optional feature permits continued retries for eligible connection failures. A transport fallback can reset the bounded counter. The UI receives reconnecting feedback. A claim that every Codex run is bounded by the same retry count would therefore be inaccurate. No general identical-tool-call detector was established in this review. [Retry policy][C4]

### pi: an explicit continuation decision, with retries outside the core loop

In pi's reusable `runLoop`, `finishTurn` can return `end` or `continue`. Without an explicit decision, tool continuation, steering, and queued follow-ups determine whether another request runs. The inspected core loop has no fixed iteration counter or repeated-command detector. Applications can implement a bounded policy at this hook; extensions that continually return `continue` can also create unproductive work. [Core continuation][P1]

The coding application separately limits retryable failures through `_prepareRetry`, using configured `maxRetries`, exponential backoff, an abortable wait, and retry lifecycle events. It retains the failed attempt in raw history while omitting it from the next model projection. These retries do not count successful, repetitive actions as failures. They also demonstrate why a core `agent_end` event alone is insufficient to infer that the complete application has stopped trying. [Retry preparation][P2]

### Goose: turn limits, hook limits, and an inactive repetition configuration

Goose's `MaxTurnsOperation` counts assistant blocks since the current kickoff and yields a message asking whether the user wants to continue once the configured allowance is reached. Adjacent assistant messages form one block, so this is not simply a count of tool calls or emitted events. The model receives budget information after half the allowance has been used. Session configuration overrides global `GOOSE_MAX_TURNS`; the inspected agent fallback is 1,000. [Counting][G1] [Enforcement and feedback][G2] [Configuration][G3]

Goose also contains a `RepetitionInspector` that compares a tool's name and argument value with the previous call. However, ordinary agent construction registers it with `None`, which disables the repetition threshold. Its inspection method checks temporary copies; the existence of this class does not establish an active, accumulating default repetition detector. [Inspector implementation][G4] [Registration][G5]

`StopHookOperation` has a distinct block cap, defaulting to eight. It counts recorded denial markers in the current kickoff window and eventually yields an explicit warning. Recipe retries have another maximum and can replace the attempted conversation suffix while retaining their attempt counter on the kickoff message. Goal and grind continuations also insert prompts. These mechanisms explain why inspecting only the inference function misses important reasons an agent keeps going. [Hook cap][G6] [Defaults][G7] [Recipe and goal continuation][G8]

### Sidekick: explicit iteration exhaustion within an event-driven system

Sidekick's native `run_agent_loop` defaults to 25 iterations and retries empty responses at most twice within that loop. The bounded loop checks cancellation at iteration boundaries. Exhaustion emits a typed error result and returns a failed outcome with reason `iteration_limit`; it is not silently reported as an answer. No general semantic or identical-call loop detector was established in this path. [Defaults][S1] [Loop boundary][S2] [Empty-response handling][S3] [Exhaustion][S4]

The scope of the counter matters. Each hidden deliberation pass invokes its own loop and gets its own allowance, separate from the eventual synthesis. Workflow retries or resumed attempts are also distinct from one invocation's iteration counter. A worker execution timeout is not necessarily an overall deadline: the workflow heartbeat refreshes it. Thus “25 iterations” must not be presented as a total conversation, multi-agent, or wall-clock budget. [Deliberation scopes][S5] [Heartbeat][S6]

## Model-based supervision and progress assessment

A **progress supervisor** is a separate model invocation that evaluates the worker's recent trajectory against the task objective. It can decide whether to continue, redirect the worker, or stop. A repeated-command detector asks whether actions look alike; a supervisor can ask whether different actions are advancing the task. Its judgment is still fallible and needs evidence.

No built-in periodic model-based progress supervisor was established in the inspected paths of these four snapshots. There are related mechanisms and useful integration points:

| Harness | Closest inspected mechanism | Boundary to keep clear |
| --- | --- | --- |
| Codex | Stop hooks can reject completion and supply a continuation prompt. [Stop handling][C1] | An attempted-stop check does not periodically inspect an agent that continues making tool calls. A custom judge behind a hook would be additional integration. |
| pi | `finishTurn` receives the completed assistant message, tool results, and current context; it can return `continue` or `end`. [Callback contract][P3] | The callback provides a place to consult a separate model, but does not itself implement a model-based evaluator. Feedback would also need to enter the next request through the application's context or steering path. |
| Goose | Stop hooks can block completion and turn their reason into feedback to the agent. [Feedback and hook boundary][G9] | This is completion review, distinct from a periodic progress check during ongoing tool use. |
| Sidekick | Hidden draft, critic, and optional fact-checker passes feed a later revision. The critic runs as a separately configured agent. [Deliberation][S7] | This is staged answer review. The critic receives the task and draft, without the drafter's tool traffic, so it lacks that evidence for diagnosing repeated failed actions. [Review context][S8] |

The distinction is between **reviewing the answer**, **deciding whether completion is acceptable**, and **supervising progress during execution**. All can produce useful guidance, but they observe different evidence at different times. See [multi-agent delegation](multiagent.md) for ownership, communication, cancellation, and workspace boundaries when a supervisor coordinates a worker.

### Pattern: a stronger supervisor over a bounded worker

The following is a proposed architecture, not a claim that these snapshots ship it. Use a lower-cost worker for routine execution and a stronger model for selected supervisory checks. “Stronger” should mean better performance on your progress-assessment evaluations; a larger or more expensive model is not automatically a better judge.

1. **Run a bounded segment.** Let the worker complete a small allowance of turns, or trigger review after repeated failures, conflicting results, or an attempted completion. Check at a defined execution boundary; do not silently replay active mutations.
2. **Provide evidence.** Send the objective and acceptance criteria, recent calls and results, actual file or artifact changes, prior guidance, and remaining budget. Include a compact longer-term record so an alternating cycle cannot hide outside the recent window. The worker's claim that it made progress is only one input.
3. **Request a structured decision.** Require a decision, supporting evidence, and a concrete next action or stopping reason. Keep the supervisor's review context separate from the worker's execution context; tool output remains evidence rather than authority to rewrite the task.
4. **Apply the decision through the harness.** Associate it with the reviewed run and context revision. If new user input or completed work makes it stale, reconcile or review again. Inject guidance at the next steering boundary, or record an explicit terminal outcome.

| Supervisor decision | Harness behavior |
| --- | --- |
| Continue | Grant another bounded segment within the remaining overall allowance. |
| Redirect | Deliver a specific change of approach and a condition for checking whether it helped. |
| Stop as complete | Verify available acceptance checks and record the evidence for completion. |
| Stop as blocked | Report unfinished work, the blocker, and any uncertain side effects. |

For the missing-dependency example, useful guidance is: “The last three test runs failed before any tests executed. Inspect dependency installation before editing the parser again. Resume testing once collection succeeds.” This gives the worker a diagnosis, an alternative action, and an observable checkpoint. “Try harder” supplies none of them.

A stronger supervisor can examine less evidence less often while the worker handles routine steps, but the added calls cost tokens and latency. Measure whether interventions improve task outcomes enough to justify that cost. Review too often and the worker may be interrupted before an approach can succeed; review too rarely and the allowance may be spent before redirection helps. A supervisor can also share the worker's mistaken assumptions or miss facts omitted from its summary.

Keep hard budgets outside both models. Charge worker, supervisor, child-agent, and retry usage to the task; cap reviews and repeated redirects. A supervisor cannot renew its own allowance or bypass tool authorization. Define an explicit outcome for judge timeout, malformed decisions, and unavailable evidence rather than treating those as unlimited permission to continue. Stopping future work does not undo an operation already performed.

### Test the supervisor's decisions

Start with controlled traces: an identical failure repeated, an alternating two-strategy cycle, legitimate polling, slow but measurable progress, and a worker claiming success despite a failing acceptance check. Also test stale guidance after user steering and repeated disagreement between supervisor and worker. Assert that redirection reaches the correct run and exhausted budgets still stop it.

Then compare the same task set with and without supervision, holding worker configuration and total allowance constant. Record success, unnecessary stops, useful redirects, cost, and elapsed time, including supervisor usage. This evaluates the stronger-model pattern instead of assuming it works. See [testing and evaluation](testing.md) for the distinction between runtime contracts and real-model task outcomes.

## A bounded continuation design

For a new harness, first define a durable task budget, then allocate child and retry allowances from it. Record consumption across process restarts. Keep a separate short-window progress detector: normalize tool arguments, include relevant result/error signatures, and distinguish deliberate polling from repeated failed mutations. Treat its verdict as a reason to pause or change strategy, not proof that a task is impossible.

For the failing-test example, feedback might say: “The last three runs produced the same missing-dependency error and no file change. Inspect the environment before running this test again.” If the global allowance expires anyway, report unfinished work, consumed limits, and any uncertain side effects. Allow an explicit continuation decision without pretending that it erases prior spending.

Finally, cancellation must reach model requests, queued tools, active executors, and retry waits. A useful regression trace covers a repeating tool, an alternating two-step cycle, a permanently blocking stop hook, a child exhausting its allocation, and a restart near the limit. Test the counters and terminal outcomes rather than relying on the model to obey a reminder. See [steering and cancellation](steering.md) and [persistence and recovery](persistence.md) for the surrounding contracts.

[C1]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L718-L772
[C2]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/hook_runtime.rs#L392-L468
[C3]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/agent/control/budget.rs#L11-L17
[C4]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/responses_retry.rs#L55-L174
[P1]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L159-L321
[P2]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L3790-L3848
[P3]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/types.ts#L134-L158
[G1]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/operation.rs#L32-L72
[G2]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_maxturns.rs#L14-L68
[G3]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/agent.rs#L556-L569
[G4]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/tool_monitor.rs#L41-L133
[G5]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/agent.rs#L348-L357
[G6]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_stop_hook.rs#L85-L120
[G7]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/agent.rs#L52-L53
[G8]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_retry.rs#L177-L279
[G9]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_stop_hook.rs#L19-L120
[S1]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L57-L65
[S2]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L397-L448
[S3]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L623-L641
[S4]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L866-L890
[S5]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/deliberation.py#L1-L36
[S6]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/workflows/agent_conversation_workflow.py#L154-L193
[S7]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/deliberation.py#L200-L315
[S8]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/context_builder_service.py#L595-L615
