Skip to content

feat: configurable maxIterPerTurn per-turn iteration cap (#2037)#2046

Merged
esengine merged 3 commits into
esengine:mainfrom
zhangyapu1:feat/configurable-max-iter
May 28, 2026
Merged

feat: configurable maxIterPerTurn per-turn iteration cap (#2037)#2046
esengine merged 3 commits into
esengine:mainfrom
zhangyapu1:feat/configurable-max-iter

Conversation

@zhangyapu1

@zhangyapu1 zhangyapu1 commented May 27, 2026

Copy link
Copy Markdown

Summary

Fixes the "no iteration limit" vulnerability from #2037 (BUG-028). The main loop was an infinite for loop with no hard cap — a runaway model could consume unlimited API budget.

Changes

  1. src/config.ts — Added maxIterPerTurn?: number to ReasonixConfig + loadMaxIterPerTurn() loader (config > env > default 50)
  2. src/loop.ts — Added maxIterPerTurn to CacheFirstLoopOptions and CacheFirstLoop instance. DEFAULT_MAX_ITER_PER_TURN = 50. Loop checks iter >= this.maxIterPerTurn before each model call and emits a warning + forces summary.
  3. src/cli/ui/App.tsx — Passes loadMaxIterPerTurn() when constructing loop
  4. src/cli/commands/desktop.ts — Same
  5. src/cli/commands/run.ts — Same
  6. src/cli/commands/acp.ts — Same
  7. src/i18n/EN.ts + zh-CN.ts + JA.ts — Warning message for iteration cap
  8. src/i18n/types.ts — Added iterLimitReached to loop type
  9. tests/loop.test.ts — Added tests verifying default and custom cap

Usage

// .reasonix/config.json
{
  "maxIterPerTurn": 15
}

Or via env: REASONIX_MAX_ITER=15

Default is 50. 50 = observed ceiling (30) × ~1.7 safety margin. Tighter cap risks truncating legitimate multi-file refactors. Power users can override via config or env.

Verification

  • npx vitest run tests/loop.test.ts — 70/70 passed
  • npm run verify — all 3805 tests passed

The main loop had no hard iteration limit — a runaway model could consume unlimited API budget. Added maxIterPerTurn config (default 9) with env override REASONIX_MAX_ITER. Plumbed through all loop construction sites (App, desktop, run, acp). Updated i18n strings.
@zhangyapu1 zhangyapu1 force-pushed the feat/configurable-max-iter branch from ae3563a to 99313ef Compare May 27, 2026 12:37
@esengine

Copy link
Copy Markdown
Owner

Thanks — capping the iteration loop is the right call, and the test that drives a cycling tool with unique args is a good way to assert the cap fires before the storm-breaker does.

Two concerns on the defaults before I merge:

  1. 9 is too low for a real workload. A typical deep-research or refactor turn in Reasonix hits 15-30 tool calls before producing the final answer (read → grep → read → edit → run → fix → re-run). Capping at 9 will force a premature summary on a lot of legitimate flows and confuse users who weren't seeing infinite loops in the first place. Suggest defaulting to 50 (still cheap insurance) and leaving the override path for power users.

  2. The env-var lookup inside the loop is unnecessary. parsePositiveIntEnv(process.env.REASONIX_MAX_ITER) ?? this.maxIterPerTurn runs every iteration. loadMaxIterPerTurn() already resolves config > env > default at construction time — this.maxIterPerTurn is the resolved number. Just compare iter >= this.maxIterPerTurn directly.

Once those two are addressed I'll merge.

…add iterLimitReached type

- Change DEFAULT_MAX_ITER_PER_TURN from 9 to 50 (real workloads need 15-30 calls)
- Remove redundant parsePositiveIntEnv() inside the loop; use this.maxIterPerTurn directly
  (loadMaxIterPerTurn() already resolves config > env > default at construction time)
- Add iterLimitReached to i18n types (fixes CI build failure)
- Move cap check before model call to avoid wasting an API request on the cap-hit iter
- Add test for custom maxIterPerTurn option

@esengine esengine left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direction is right — closes a real gap (BUG-028 was on my radar too).

A few inconsistencies to clean up before I merge:

  1. src/loop.ts field doc says default 9, code is 50. The class field comment reads Config > env > default (9) but DEFAULT_MAX_ITER_PER_TURN = 50 and loadMaxIterPerTurn falls back to 50 too. Please reconcile to 50 (or whatever final number we land on — see #3).

  2. PR description is stale. It says Changed DEFAULT_MAX_ITER_PER_TURN from 100 to 9 and Default is 9 in the usage block, but the code is 50. Please update so the description matches what reviewers will actually merge.

  3. Default value — is 50 the right number? A defensive cap is good, but 50 tool calls per turn is on the high side; in practice most legitimate turns finish well under 30. I'm fine with 50 as a starting point, but call out in the description why you picked 50 over (say) 30 so it's a deliberate choice we can revisit later.

CI is green and the test coverage looks good. Once 1 + 2 are fixed and 3 has a one-line rationale I'm happy to merge.

@esengine esengine merged commit 5c3ba53 into esengine:main May 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants