fix(goal): persist iteration count across resume so MAX_GOAL_ITERATIONS bounds the whole session - #5000
Conversation
…NS bounds the whole session
On resume, restoreGoalFromHistory re-arms an unfinished /goal via registerGoalHook,
which always primed the store with iterations: 0. Since findGoalToRestore only
returned the condition, the running count recorded in the transcript was dropped,
so the MAX_GOAL_ITERATIONS safety cap was re-granted in full on every resume —
an unreachable goal could auto-loop another full budget after each /resume.
The count is already persisted (checking goal_status items carry iterations), so
the fix just reads it back:
- findGoalToRestore returns { condition, iterations } from the latest non-terminal
goal_status item (set items restore at 0).
- registerGoalHook accepts an optional initialIterations (default 0, clamped at 0).
- restoreGoalFromHistory threads the restored count through.
Resume re-arm stays passive — continuation timing is unchanged.
Closes QwenLM#4999
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No high-confidence issues found in the diff. One suggestion from the review that touches an unchanged file relied upon by this PR:
[Suggestion] checking items persist the stop-hook-loop counter, not the goal's cumulative counter
packages/cli/src/ui/hooks/useGeminiStream.ts:1373 writes iterations: value.iterationCount (per-recursion counter from StopHookLoop) instead of activeGoal.iterations (cumulative store counter). These are identical within a single top-level interaction but diverge if the user interrupts and sends a new message while the goal remains active — value.iterationCount resets to 1 while activeGoal.iterations keeps accumulating. On resume, the restored count would be lower than the true cumulative total, partially re-granting the MAX_GOAL_ITERATIONS budget.
Suggested fix: change line 1373 from iterations: value.iterationCount to iterations: activeGoal.iterations (the variable is already in scope two lines above).
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — qwen3.7-max via Qwen Code /review
Verification report — real-build testing at head
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — qwen3.7-max via Qwen Code /review
Local runtime verification (maintainer, macOS)Verdict: PASS — built both this PR ( Before (baseline
|
…NS bounds the whole session (#5000) * fix(goal): persist iteration count across resume so MAX_GOAL_ITERATIONS bounds the whole session On resume, restoreGoalFromHistory re-arms an unfinished /goal via registerGoalHook, which always primed the store with iterations: 0. Since findGoalToRestore only returned the condition, the running count recorded in the transcript was dropped, so the MAX_GOAL_ITERATIONS safety cap was re-granted in full on every resume — an unreachable goal could auto-loop another full budget after each /resume. The count is already persisted (checking goal_status items carry iterations), so the fix just reads it back: - findGoalToRestore returns { condition, iterations } from the latest non-terminal goal_status item (set items restore at 0). - registerGoalHook accepts an optional initialIterations (default 0, clamped at 0). - restoreGoalFromHistory threads the restored count through. Resume re-arm stays passive — continuation timing is unchanged. Closes #4999 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(goal): persist cumulative checking iterations * fix(goal): record checking status during continuations --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
What this PR does
Persists the
/goaliteration count across session resume so theMAX_GOAL_ITERATIONSsafety cap bounds the goal's whole session lifetime instead of being re-granted on every resume.A
/goalloop is driven by a session-scoped Stop hook: each not-met judge incrementsiterationsand blocks stopping, and the loop aborts onceiterationsreachesMAX_GOAL_ITERATIONS. On resume,restoreGoalFromHistoryre-arms an unfinished goal throughregisterGoalHook, which always primed the store withiterations: 0, andfindGoalToRestoreonly returned the condition — so the running count was dropped and the cap reset to a full fresh budget after each/resume,/branch, or resumed-session startup. An unreachable goal could therefore auto-loop another full budget every resume, making the runaway-loop guard effectively unbounded.The running count is already persisted (continuations write a
checkinggoal_statusitem carryingiterations), so the change just reads it back:findGoalToRestorenow returns{ condition, iterations }from the latest non-terminalgoal_statusitem (setitems predate any iteration, so they restore at 0).registerGoalHooktakes an optionalinitialIterations(default 0, clamped at 0) and primes the store with it.restoreGoalFromHistorythreads the restored count through.No persistence-format change. Resume re-arm stays passive — continuation timing is unchanged; only the starting iteration count is corrected.
Why it's needed
The
MAX_GOAL_ITERATIONScap exists to stop a goal that can never be satisfied from looping forever. Because the counter reset on resume, a long-running or unsatisfiable goal that spans a resume could keep consuming a full iteration budget indefinitely — the opposite of the cap's intent. Closes #4999.Reviewer Test Plan
How to verify
/goal the file /tmp/never-exists.txt contains the word DONE(never create it).qwen --resume(or/resume) the same session.Automated coverage:
cd packages/core && npx vitest run src/goals/goalHook.test.ts src/goals/goalLoop.integration.test.tscd packages/cli && npx vitest run src/ui/utils/restoreGoal.test.ts src/ui/commands/goalCommand.test.tsNew tests: iteration count carried from a
checkingitem; restored count landing in the active goal; negativeinitialIterationsclamped to 0; and a resumed near-cap goal hittingMAX_GOAL_ITERATIONSon the next not-met verdict (proving the cap is a true cross-resume bound).Evidence (Before & After)
Non-UI logic change. Behavior is covered by the unit tests above; no TUI surface changes.
Tested on