Files
qwen-code/docs/users
易良andQwen-Coder 26c3fb7a5a feat(core): add a convergence reminder for read-only exploration (#13601)
* feat(core): add a convergence reminder for read-only exploration

Share an advisory exploration allowance across core and daemon tool loops without adding a halt or changing tool permissions.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): keep halted tool batches out of the exploration budget

recordDaemonToolCalls fed the exploration budget at the top of its
per-call loop, before the per-turn cap and global-duplicate checks. A
batch that those checks halt is skipped whole and never executes, yet
its calls still counted toward the budget and still resolved every tool
name through the registry. Move the recording after both halt checks so
only calls the turn actually runs feed the budget, and a capped turn
performs no registry lookups.

Also complete the client-goal Config double with getMaxToolCallsPerTurn
and getToolRegistry, which the new budget call sites require.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmuy5teh2cj

* fix(core): roll back the exploration budget and turn cap on ModelFallback

A model fallback restarts the attempt from scratch exactly like a retry
(Turn clears pendingToolCalls for both), but the exploration budget's
rollback and LoopDetectionService's per-turn cap rollback both fired on
Retry only. The discarded attempt's read calls stayed counted, inflating
the phase counter by every failed hop of the fallback chain, so the
single reminder (and the cap) could fire before the executed evidence
reached the allowance — the same drift the daemon path avoids by
recording only the surviving batch.

Wire both rollbacks to ModelFallback beside Retry, and pin the core-side
budget wiring (record/commit/rollback/reset, the session-disable gate,
and the duplicate-call-id dedup) with client integration tests that
drive real tool-result continuations; each guard was verified to fail
under its targeted mutation.

Co-authored-by: Qwen-Coder <[email protected]>

* style(core): apply prettier to the new convergence-reminder tests

The Lint lane's whole-repo prettier check was short-circuited by the stale
freshness gate, so these lines were never formatted. Formatting only.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmv2f2dk80w

* fix(core): keep guard evidence across ModelFallback and close review gaps

The review-pr round on 69f2e313 filed one Critical and ten Suggestions;
this addresses the Critical fully, seven Suggestions, and declines two
with rationale.

Critical: the widened per-turn cap rollback cleared the loop detector's
accumulated guard evidence on ModelFallback. A production fallback can
only precede its own attempt's calls (the chain is gated on the failed
attempt yielding no candidate output), so there was nothing to roll back
— but the evidence clears were not inert: the chain is per-request, so a
capacity-limited primary can prefix every round-trip of one turn with
ModelFallback, zeroing the consecutive-identical streak, the
shell-inspection streak and the adaptive cap's repeat tracker on each
round-trip, silently disabling two always-on guards until the 1000-call
backstop. ModelFallback now returns early without touching accumulated
state; the Retry branch keeps its full rollback. Three new
loopDetectionService tests pin the reachable shape (per-round-trip
fallbacks with committed evidence) and redden under the widened branch.

Also: the client budget's lifecycle moves behind a shared
isAttemptRestartEvent predicate (R1-7); its block moves below the
statement the pre-existing comment documents (R1-10); a concurrent side
query no longer resets the running turn's exploration phase (R1-11);
lsp and tool_search classify as exploration reads without retyping
their Kind.Other registration (R1-2a); the unreachable model_fallback
fixture is replaced with a cross-round-trip shape (R1-1 note 3); reset()
gains a commit-floor unit test (R1-4); bridged tool_call args wiring is
pinned in both runtimes (R1-5); the design docs record the threshold
boundary, the subagent-loop exclusion, the budget-local rollback split,
the todo-write cadence consequence and the discovery-tool
classification, in both languages (R1-3, R1-2b); settings.md documents
the reminder at the maxToolCallsPerTurn row (R1-9).

Declined: insertion-site debug logging (R1-6) — the nearest reminder
peer (activeTodoReminder) logs nothing either, and the injected text
already persists in the transcript; reusing the executor's
ensureTool-resolved name for daemon classification (R1-8) — the
asymmetry is self-healing (ensureTool materialises factory targets into
the map the classifier reads), the only divergent shape found is inert
in fork registries, and the perf half is noise by the reviewer's own
measurement. Both are recorded as follow-up candidates.

Co-authored-by: Qwen-Coder <[email protected]>

* style(core): apply prettier to the convergence-reminder files

Run Prettier reported style issues in 5 files touched by this PR
(docs/users/configuration/settings.md, client.test.ts,
loopDetectionService.ts, loopDetectionService.test.ts,
tool-exploration-budget.test.ts), which failed the Lint & Static job.
Formatting only, no behavior change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmv2xa8pg1q

* fix(core): isolate the exploration budget per interaction and close review gaps

* fix(core): commit the exploration floor on an in-round-trip phase reset

`record()`'s non-read branch zeroed `calls`/`reminded` without committing the
new floor, so a `rollback()` later in the same round-trip resurrected the
previous phase's `committedCalls`/`committedReminded`. The retried attempt was
then nagged after a single read, and the resurrected reminder latch was
re-committed by every later `Finished`, silencing the reminder for the rest of
the interaction. Commit the floor here, as `reset()` already does.

Also stop crediting a bridge-refused discovery target as a read: `tool_call`
unconditionally refuses `tool_search` and itself, so a bridged call to either
performs no read and must stay unclassified like a name the registry refuses.
A bridged `lsp` still classifies as read.

The strengthened rollback assertion is red on the previous code and green with
the commit; the new bridge case is red without the refusal guard.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmv41akk23g

* fix(core): drop the duplicate Goal-branch exploration budget reset

The Goal branch's toolExplorationBudget.reset() was the only budget
entrypoint with no ownership or side-query gate, contradicting the
invariant the design docs state for every other one. Goal is a member of
startsInteraction, so the interaction-start block already reset the budget
and claimed ownership for every non-side-query Goal submission, and nothing
records into the budget between the two points - making this a second no-op
reset. For a Goal submitted as a concurrent side query the
interaction-start reset is correctly skipped, but this one still fired and
zeroed the running turn's read-only phase, so that turn never converged.

Deleting the duplicate removes the ungated entrypoint entirely rather than
adding a second copy of the guard.

Also pin the Stop-hook restart reset, which no test exercised: the nested
restart is submitted with SendMessageType.Hook, which is not an
interaction-starting type, so that reset is the only thing clearing the
read-only phase across a hook restart.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmv45kw3b3o

* fix(core): drop the inert ModelFallback arm from checkAlwaysOnSafeties

The arm returned false with no mutation between it and the fall-through
`if (event.type !== LlmEventType.ToolCallRequest) return false;`, and
ModelFallback is not a ToolCallRequest, so it already returned false at the
same point with zero mutations. It could not change behaviour.

Delete it and re-point the two references that cited it as a detector-side
pin: the isAttemptRestartEvent docstring (now names the non-ToolCallRequest
early return that actually carries the no-op) and the client.test.ts fallback
comment. The three loopDetectionService.test.ts ModelFallback cases stay: the
invariant they assert (a per-round-trip fallback must not clear committed
streak evidence) is real and documented in the design doc, and they now pin it
at the tier that carries it instead of at dead code.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmv49v7nr3w

* refactor(core): delegate the exploration-budget phase reset to reset()

record()'s non-read branch was a byte-identical copy of reset()'s body,
with a comment asserting the equivalence instead of the code expressing
it. The two copies already diverged once: reset() committed the zeroed
floor while the in-round-trip copy did not, and that divergence was
R3-1, a Critical in which a rollback after a mid-round-trip phase change
resurrected the previous phase's count and its delivered-reminder latch.
Delegate so a future phase field cannot be added to one site only.

Addresses review finding R4-1.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmv585s4o5m

* test(core): cover the goal-permitted Stop-hook exploration-phase reset

The goal-permit branch restart reset at client.ts:5354 had zero test
coverage. Its !goalPermit sibling at :5463 is pinned by "gives a blocking
Stop hook continuation a fresh exploration phase", but no test installed a
goal runtime alongside a blocking Stop hook, so deleting this line changed
nothing any test observed. The nested restart is submitted with
type: SendMessageType.Hook, which is not an interaction-starting type, so
the in-branch reset is the only thing that clears the phase.

Extends the existing fixture rather than building a new one. Verified
discriminating: deleting client.ts:5354 turns this case red
(expected 2 to be +0) while the !goalPermit sibling stays green.

Addresses review finding R3-5.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmv585s4o5m

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-13 09:02:46 +00:00
..