qwen-code/docs/design/web-shell-loop-detection-turn-error.md
ytahdn 30134200a9
fix(web-shell): surface loop detection turn errors (#8853)
* fix(web-shell): surface loop detection turn errors

* fix(cli): expect loop-detected turn error in invalid-params stop test (#8853)

* fix(cli): reject loop-detected stops only for foreground ACP turns

Stop-hook continuations are shared with cron and background-notification
turns, which must keep their pre-loop-error graceful end-turn handling;
only the foreground prompt chain now rejects a loop-detected stop.

Also folds in review feedback: drop the unreachable repeated-failure
branch from the continuation, extract the cancellation-precedence
helper, defer and freshness-guard the bounded-refresh turn-error replay,
skip the phantom forward-failed compensation for structured turn errors,
key the structured code gate on structuredness, share the retryability
predicate in Web Shell, and keep the turn_complete error signal alive
for loop-detected turns.

* fix(acp-bridge): harden loop-detected turn errors per review feedback

Address round-3 review findings:

- The bounded-refresh append guard no longer compares the in-memory
  turn_error against the last journaled event of any kind. The published
  event is stored on the entry at broadcast time; any newer turn terminal
  clears it, and only turn-content events journaled after it block the
  append (queue/config bookkeeping no longer hides the error on refresh).
  The staleness check reads the in-flight journal via a new
  liveJournalSnapshot() accessor instead of flattening the replay window.
- Channel turns keep the graceful end-turn handling like cron and
  background-notification turns, so their collected text is still
  delivered when loop protection stops them.
- Loop-detected rejections drain the cron/notification queues in
  prompt()'s finally, preserving the pre-rejection drain invariant for
  queued automatic work.
- Tests pin each behavior: queued-bookkeeping refresh append, stale-error
  no-append, prompt_cancelled absence, foreground stop-hook rejection,
  background-notification graceful default, and the user-facing message
  constant.

* fix(cli): keep channel-prompt turns graceful on loop-detected stops

Channel tasks prompted through CHANNEL_PROMPT_META_KEY (DaemonChannelBridge /
AcpBridge) carry no channelDelivery meta, so the loop-protection exemption
keyed on channelDelivery missed them: a per-turn tool-call cap trip rejected
the turn with LOOP_DETECTED, the channel bridge never emitted promptComplete,
and the collected response text was lost as a failed task. Extend the
graceful end-turn exemption to channel-prompt-meta turns, mirroring the
repeated-failure guard forcing those turns already receive.

* fix(acp-bridge): keep loop turn error on refresh after idle bookkeeping (#8853)

* fix(acp-bridge): harden loop-protection turn state per review feedback (#8853)

* fix(acp): gate channel-turn classification and harden loop-error refresh replay (#8853)

Close two trust-gate gaps in the channel-turn classification introduced
for loop-detected turn errors: the `qwen.daemon.channelDelivery` sibling
key was not gated like `qwen.channel.prompt` at the standalone ACP
boundary, and on the daemon-hosted path a client-forged
`qwen.channel.prompt` survived the serve route and bridge admission strip
to be re-injected for trusted parents. Both keys are now stripped from
untrusted callers and honored only from trusted context (private parent
state, or the channel-worker prompt authorization validated by the daemon
prompt route).

Also harden the refresh-append guard for pending turn errors: the
synthetic `history_truncated` journal marker and idle latest-wins
`session_update` snapshots no longer defeat the append, and a queued
terminal that folds newer turn content supersedes the stale error before
the fold erases the evidence. Web Shell now derives turn_complete's error
from the same backward walk as the retry decision.

* fix(acp): close loop-protection bypasses in delivery classification and retry re-arm (#8853)

* fix(acp): cover idle workspace fan-out events in the refresh-append allowlist (#8853)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-14 06:14:06 +00:00

2.6 KiB

Web Shell loop-detection turn errors

Problem

ACP loop protection currently records unstarted tool calls as failures and then completes the prompt with stopReason: end_turn. Web Shell therefore presents the internal tool skip text as the only explanation and treats the turn as successful.

Design

When a foreground ACP prompt is stopped by loop protection, preserve completed and skipped tool results as today, then reject that prompt with a structured ACP request error. The bridge publishes the existing turn_error terminal with errorKind: loop_detected and the detector's loopType. Cancellation continues to take precedence when it races the loop stop.

Web Shell renders loop_detected from the structured kind, using localized plain language: the model repeated tool use or reached a safety limit, only the current turn stopped, and the user can continue with a more specific instruction. No client matches the internal English tool error.

Skipped tools keep their existing failed terminal update and error details so they cannot remain pending and their display behavior does not change. The additional turn_error provides the user-facing explanation for the stopped turn.

The session remains alive and the per-turn loop state is recreated for the next prompt. Cron, background-notification, channel-classified, and goal turns keep their existing non-interactive handling: only interactive foreground prompts reject. Channel classification comes from the authenticated channel-prompt marker alone; the caller-requested delivery meta still schedules the delivery but keeps the foreground rejection, so it cannot opt a turn out of loop protection. Goal turns bypass the bridge entirely, so rejecting one would settle it as failed and pause the goal without publishing any turn_error; they resolve end_turn like the other automatic turn types. A loop-detected rejection still drains the cron/notification queues, preserving the invariant that a loop-stopped turn never strands queued automatic work.

When Web Shell reloads a live session from paginated persisted history, the bridge appends the current in-memory turn_error to that replay. This keeps the terminal error visible across a page refresh while the session remains idle; newer turn content — including automatic turns the rejection itself drains — supersedes it by design.

Compatibility

turn_error already terminates prompts and returns the UI to idle. Adding a known error kind and optional metadata is backward-compatible: older clients show the daemon message, while updated clients show localized guidance.