- Status: Accepted
- Date: 2026-06-22
Before this decision, neenee wrapped its autonomous loop in two arbitrary execution caps:
- Per-turn round cap.
MAX_TOOL_ROUNDS = 32incrates/neenee-agent/src/lib.rshard-paused a turn that called more than 32 distinct tool rounds, withSOFT_TOOL_ROUND_LIMIT = 26injecting a hidden "convergence nudge" a few rounds earlier (agent.rs). /loopiteration cap.1..=50incrates/neenee-cli/src/main.rsbounded the outer autonomous loop. The model self-evaluated completion by emitting[NEENEE_GOAL_COMPLETE], but if it never did, the loop ran out at 50 turns and reportedexhausted.
Both caps pre-existed any survey of how peer agents handle the same problem.
A direct comparison of the two open implementations the user asked us to
study — codex (codex-rs/core/src/session/turn.rs) and claude-code
(src/query.ts) — produced a clear, converging answer:
| Concern | codex | claude-code |
|---|---|---|
| Agentic turn loop | loop {}, no per-iteration cap (turn.rs:207) |
while (true), no per-iteration cap (query.ts:307) |
| Iteration counter | none — there is no max_turns/iteration_limit |
optional maxTurns, unset on the main thread; sub-agents cap at 200 (forkSubagent.ts:65) |
| Termination | model emits end_turn: true with no tool_calls |
model emits an assistant message with no tool_use blocks |
| Runaway backstop | context-window pressure → auto-compaction, then continue (turn.rs:304–323) |
same model, plus an explicit taskBudget token cap |
/loop command |
does not exist | exists, but is a cron scheduler (/loop 5m <prompt>), not an iteration driver |
Both treat the agentic loop as the default execution model: every ordinary prompt already runs open-ended until the model itself stops calling tools. Neither publishes a per-iteration counter as a user-facing knob.
neenee's two-layer cap system diverged from both. Worse, it produced observable pain:
- The 32-round cap tripped on legitimate refactor turns that needed to read,
edit, and verify several files in sequence. Users had to
/loopto keep going, which started a fresh turn with a noisy control prompt. - The 50-iteration cap turned into a budget the model would quietly exhaust
on a hard sub-task, ending with
Loop exhausted its 50 iteration budget. The user had no in-loop signal that the model was making progress vs. stuck — the cap was the signal, and it was the wrong one. - Coupling between
/goaland/loop(/looprefused to start without an active goal) created two-state management friction that codex and claude-code simply do not have.
The compaction backstop that replaces a hard cap was already in place
(compaction_max_chars, mid-turn pruning via MidTurnCompactionGate,
auto-compaction on context overflow). The cap was redundant safety theatre.
-
Remove the per-turn round cap. Delete
MAX_TOOL_ROUNDS,SOFT_TOOL_ROUND_LIMIT, andCONVERGENCE_NUDGE. The streaming and non-streaming turn loops (Agent::run_streaming_with_events,Agent::run_with_events) run until the model emits a final assistant message with no tool call, or until one of: user interrupt (CancellationToken), the repeated-call guard (MAX_REPEATED_TOOL_CALLS = 3), a provider/tool error, or context overflow after compaction. TheHarnessError::TurnLimitReachedvariant and theAgentResponse::TurnPausedevent are removed. -
Remove the
/loopiteration cap.start_goal_loopno longer takes amax_iterationsparameter; it runs until the completion marker fires, the user runs/loop stoporEsc, a newer request supersedes it, or an error aborts. The"exhausted"terminal status is no longer produced. -
Decouple
/loopfrom/goal./loop <objective>sets a fresh goal from the objective text and starts the loop in one step./loop(no args) keeps the existing behaviour of starting on the active goal. The pure-numeric legacy form/loop <N>is rejected with a migration hint. -
Keep durable checkpoints.
LoopCheckpoint.max_iterationsis preserved on the wire for backward compatibility, withUNCAPPED_ITERATIONS = usize::MAXas the new sentinel.LoopCheckpoint::resume_iterationcontinues to accept legacy finite-budget checkpoints (their cap is ignored on resume) so pre-ADR-0009 snapshots resume cleanly. -
Rely on compaction + user interrupt as the runaway backstop, matching codex's explicit trust comment at
turn.rs:304.
-
Keep the caps, raise the numbers (e.g.
MAX_TOOL_ROUNDS = 200,/loop 1..=500). Rejected: any finite cap is arbitrary, produces the same "budget exhausted" failure mode at a slower cadence, and diverges from the two reference implementations. The cap is the wrong shape for the problem; compaction is the right shape. -
Replace
/loopwith claude-code's cron semantics (/loop 5m <prompt>schedules the prompt on a wall-clock interval). Rejected for this ADR: that is a different feature (long-running monitoring), not a replacement for the in-session autonomous loop. Can be added later under a different command name. -
Drop
/loopentirely and make every prompt an uncapped agentic loop (the strict codex model). Rejected: neenee's goal/checklist accounting and durable resume are genuinely useful, and/loopis the natural entry point for them. Removing the cap and decoupling from/goalcaptures the relevant best practice without throwing away the durable infra. -
Keep the convergence nudge, drop the hard cap. Rejected: the nudge exists only to give the model a wind-down period before a hard wall; with no hard wall, the nudge is just an interruption. The model already receives "produce a final text answer when done" guidance in the system prompt.
Positive:
- Long refactor / multi-file verification turns no longer trip a 32-round cliff.
/loopruns until the model genuinely completes or the user interrupts; no more "Loop exhausted its 50 iteration budget" on hard sub-tasks./loop <objective>collapses two commands into one for the common case.- Two fewer public-API variants to maintain (
TurnLimitReached,TurnPaused) and one less notice severity (NoticeSeverity::TurnLimit), shrinking the harness-to-UI event surface.
Negative:
- A genuinely stuck model that makes distinct but unproductive tool calls
can run longer before the user notices. Mitigation: the activity bar
shows
turn N · round M · <status>live, the user can interrupt at any time, and the repeated-call guard still catches the common case. /loop's uncapped duration puts more weight on context compaction behaving well. The mitigation is the same as codex's: compaction has been in place and observed to hold the transcript well under the context limit.
Migration:
- The pure-numeric
/loop <N>form is rejected with a message pointing to the new syntax. Non-numeric forms (/loop resume,/loop status,/loop stop, and now/loop <objective>) are unchanged or extended. - Existing durable checkpoints with a finite
max_iterations(legacy pre-ADR-0009 snapshots) still resume — the cap is ignored on resume. - The
TurnLimitReachederror andTurnPausedevent are gone; the TUI notice severityTurnLimitis gone. The transcript no longer renders amber "Turn paused after N tool rounds" notices.
crates/neenee-agent/src/lib.rs—MAX_REPEATED_TOOL_CALLS(the surviving guardrail), with comments documenting the uncapped-loop policy.crates/neenee-agent/src/agent.rs— the streaming and non-streaming turn loops, now without round caps.crates/neenee-agent/src/orchestration.rs::start_goal_loop— the uncapped outer loop.crates/neenee-store/src/session.rs::UNCAPPED_ITERATIONS— the wire sentinel for uncapped checkpoints.crates/neenee-cli/src/main.rs— the rewritten/loopparser.- Peer implementations surveyed:
codex-rs/core/src/session/turn.rs:207,304–323,368andsrc/query.ts:307,829–835,1704–1712. - Harness architecture — the surviving safety surface.
- Turns and rounds — what ends a turn under the new model.