Stop repeated invalid tool parameter loops in ACP - #6076
Conversation
|
Thanks for the PR! Template looks good ✓ — all required headings are present and filled in, including the linked issue (#6075). On direction: this is squarely aligned with the project. #6075 documents a real ACP-daemon correctness bug — a model can spin the daemon turn indefinitely by resending the same invalid tool parameters with fresh call IDs, bypassing the existing duplicate-call-id guard. The CLI already has always-on loop guards for similar scenarios, so parity in the daemon path is the right thing to ship. CHANGELOG has no direct prior reference, but the loop-detection area ( On approach: the scope is tight and minimal. Five files, ~230 lines, all pointed at the stated goal:
No drive-by refactors, no scope creep. I don't see a materially simpler path — the existing duplicate-call-id and turn-cap guards already cover adjacent cases, and this PR slots the missing piece in cleanly. Moving on to code review. 🔍 中文说明感谢 PR! 模板完整 ✓ — 所有必需章节都已填写,包括关联的 issue(#6075)。 方向:与项目完全对齐。#6075 记录了一个真实的 ACP daemon 正确性 bug —— 模型可以用新的 tool-call id 不断重发相同非法参数,绕过现有的 duplicate-call-id 防护,让 daemon turn 无限空转。CLI 在类似场景下已经有 always-on 的 loop guard,所以在 daemon 路径补齐对等能力是正确的方向。CHANGELOG 中没有直接相关的历史条目,但 loop-detection 基础设施( 方案:范围紧凑且最小化。5 个文件、约 230 行,全部围绕目标:
没有顺手的重构,也没有范围蔓延。我没有看到明显更简单的路径 —— 现有的 duplicate-call-id 和 turn-cap 守卫已经覆盖了相邻场景,本 PR 干净地补上了缺失的一块。 进入代码审查。🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent baseline (before reading the diff): the bug is "daemon resends the same invalid tool params with fresh IDs, bypassing duplicate-call-id guard." The minimal fix I would have proposed is:
The PR matches this baseline almost exactly — same state shape, same threshold, same reuse, same cap. The one nuance I hadn't anticipated is that the invalid-params counter should only fire when Findings:
No critical blockers. No AGENTS.md violations. Implementation reads like a maintainer wrote it. Real-Scenario TestingHonest caveat up front: this bug lives in the ACP daemon path — triggered only when a model sends repeated invalid tool parameters with fresh call IDs. It is not reachable from a normal interactive/non-interactive
Focused regression testThe test mocks Full related-suite runsBuild / typecheck / lintTmux before/after smoke (bundle starts, version surfaces)Both bundles start and exit cleanly. No CLI surface regression. Label presence in built artifact
The new enum value is compiled into core, consumed by the ACP agent chunk, and surfaced in the non-interactive CLI formatter with the correct user-facing label — exactly the three integration points the diff modifies. 中文说明代码审查独立基线(读 diff 之前):这个 bug 是「daemon 用新 id 不断重发相同非法参数,绕过 duplicate-call-id 守卫」。我会提出的最小修复是:
PR 几乎就是这个方案 —— 同样的状态形态、同样的阈值、同样的复用、同样的上限。一个我没想到的细节是:invalid-params 计数器只应在 结论:
无 critical blocker。无 AGENTS.md 违规。代码读起来像维护者写的。 真实场景测试先说实话:这个 bug 在 ACP daemon 路径 —— 只有模型用新 tool-call id 反复发送相同非法参数时才会触发。它无法通过普通 interactive/non-interactive
(测试输出见上文英文部分,已包含完整 capture-pane / vitest 输出。) — Qwen Code · qwen3.7-max |
|
Stepping back: this PR reads like a maintainer's fix for a bug they found in traces.
Approval guardrail check: Approving. ✅ 中文说明退一步看:这个 PR 读起来像是维护者修了一个自己在 trace 里发现的 bug。
Approval guardrail 检查: — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
DragonnZhang
left a comment
There was a problem hiding this comment.
Review Summary — PR #6076: Stop repeated invalid tool parameter loops in ACP
Scope reviewed: Loop detection logic in Session.ts, error-message handling in errors.ts/fileUtils.ts/write-file.ts, CacheSafeParams history copying in forkedAgent.ts, autonomous loop sentinel infrastructure, AcpFileSystemService local-read-root configuration.
Overall Assessment
The core loop-detection fix is well-structured. The DaemonToolLoopState approach with a Map<string, number> keyed by toolName\0error.message and a hard tool-call cap is a clean, minimal solution to the reported bug. The toolBuildSucceeded flag correctly scopes the invalid-params guard to tool.build() failures only (not execution errors), which is the right design choice.
Existing inline comments already cover the key concerns:
-
Stop-hook bypass (
Session.ts~line 2063) —#handleStopHookLoopcreates a freshDaemonToolLoopState, so a stop-hook continuation can re-enter the prompt loop with a clean loop-guard slate. This is the most significant concern. -
Error text bucketing (
Session.ts~line 282) — Using the exacterror.messagestring as a map key may under-count when validators embed user-controlled values (e.g., file paths) in error messages, causing the guard to miss genuine loops. -
Concurrent batch short-circuit (
Session.ts~line 3954) — In a concurrent batch,runBoundedcontinues scheduling calls afterloopDetectedis set. TheshouldSkipUnstartedcallback is only wired to abort signals, not to the loop state.
Verified as correct:
- Threshold logic: The
>=comparison with threshold 3 correctly stops at the 3rd repeated failure, matching the test expectations. - Cap logic:
recordDaemonToolCallscounts the full batch size upfront (conservative over-counting), which is acceptable for a safety cap. - Telemetry:
LoopType.INVALID_TOOL_PARAMS_STAGNATIONis properly added to the enum and toLOOP_TYPE_LABELSwith the correct always-on classification. - Error message handling: The
getErrorMessageenhancements (plain object support, cause unwrapping, 1000-char truncation) are well-implemented with thorough test coverage. - CacheSafeParams history copying:
copyHistoryContainersshallow-copiesContent[]andparts[]containers while sharing part objects by reference — correct for mutation isolation without deep cloning large history. - Test coverage: The focused regression test for the invalid-params loop is sound and verifies the fix.
PR scope note:
This PR is titled for the loop detection fix but bundles several additional changes (autonomous loop sentinels, AcpFileSystemService local-read-root configuration, subagent-result.ts tag stripping, error message formatting improvements, npx → cross-env script fix, serve fast-path bundle check in CI). These are well-structured but extend the PR's scope beyond what the title suggests. Consider splitting in the future for easier review and bisect.
What this PR does
The ACP daemon now tracks tool-call progress across each daemon turn and stops the turn when the model repeatedly sends invalid parameters for the same tool instead of correcting them. It also adds a hard per-turn tool-call cap for ACP daemon execution and records loop-detection telemetry when either guard fires.
Why it's needed
A model can repeatedly request a tool such as
ask_user_questionwith fresh call IDs but the same invalid argument shape. Because the IDs are fresh, duplicate call protection does not apply, and ACP daemon mode can keep sending validation errors back to the model until the session repeats the same failed request thousands of times. The daemon needs the same kind of always-on safety boundary that the CLI already has for tool-call loops.Reviewer Test Plan
How to verify
Confirm that an ACP prompt stops after the third repeated invalid parameter validation error when the provider uses fresh tool-call IDs. Also confirm that existing duplicate provider call ID loop behavior, non-interactive loop messaging, and the core per-turn tool-call cap behavior still work.
Evidence (Before & After)
Before this change, the focused ACP regression test reproduced the bug by continuing to a fourth model send after three repeated invalid
ask_user_questionparameter errors. After this change, the same test stops at three sends and logs the loop guard. Additional focused regression tests passed for duplicate provider call IDs, non-interactive loop messaging, and the core turn tool-call cap.Local verification passed: focused ACP session tests, focused non-interactive CLI loop detection test, focused core loop detection test,
npm run build && npm run typecheck,npm run lint, andgit diff --check.Tested on
Environment (optional)
Local macOS worktree using the repository npm scripts.
Risk & Scope
Linked Issues
Fixes #6075