Files
qwen-code/docs/developers/development
ae7901552e fix(core): settle the loop reminder delivery at the model-acceptance boundary (#15559)
* fix(core): settle the loop reminder delivery at the model-acceptance boundary

takeLoopReminder() cleared the pending map and logged the 'reminded'
telemetry while the ToolResult request was only being assembled. Between
the take and turn.run(), the send still awaits microcompaction and
memory recall and yields pending goal events, so a consumer that stops
the generator there loses the streak's warning (dropPendingReminders
only unmarks still-pending keys) while telemetry credits a delivery the
model never saw.

The delivery is now only prepared at the take: the client commits it
when the first model-accepted event arrives and abandons it — restoring
the pending entries — in the send's finally when the request never
reached the model. A full reset drops a prepared delivery so a late
abandon cannot leak a stale warning into a new prompt. The agent-core
round loop keeps its take-time accounting with an explicit commit since
it has no consumer-visible suspension between the take and the model
call.

Fixes #15537

* fix(core): defer the agent-core loop reminder commit to model acceptance

* fix(core): drop the ineffective pre-round reminder abandons

The three abandonLoopReminderDelivery() calls at the abort/maxTurns/time
checks could not change anything: each is followed by a break out of the
round loop, the detector is method-local, and abandon only moves entries
between two private maps without logging. They made the take-site comment
and the maxTurns control read as if they were what keeps `reminded`
truthful, so drop them and say what the control actually pins — no round
crosses a model-acceptance boundary, so nothing commits.

Also, from the same review round:

- abandon's doc block named the wrong function. A retry drops the streak
  through resetToolCallCount(), which deletes the three hard-coded keys;
  dropPendingReminders() cannot reach entries already taken out of the
  pending map.
- client.test.ts asserted not.toHaveBeenCalled() immediately after
  mockClear(), which cannot fail. Compare the call count with the value
  captured before the send instead, so the "suspended before turn.run"
  premise is actually pinned.
- cover the multi-entry path: commit credits every entry of a delivery and
  abandon restores all of them, not just the first.
- telemetry.md and both languages of the loop-protection design doc now
  say `reminded` is logged at model acceptance, and the "sent, never
  accepted, credited on a later send" case is added to the enumeration of
  not-counted cases.

---------

Co-authored-by: suzhiguo7 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-02 00:43:23 +08:00
..