Skip to content

fix(cli): Stop after cancelled ask_user_question#5218

Merged
doudouOUC merged 9 commits into
QwenLM:mainfrom
doudouOUC:codex/fix-ask-user-question-cancel
Jun 17, 2026
Merged

fix(cli): Stop after cancelled ask_user_question#5218
doudouOUC merged 9 commits into
QwenLM:mainfrom
doudouOUC:codex/fix-ask-user-question-cancel

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR makes ACP tool execution stop immediately after ask_user_question is cancelled, including nested Agent cases. It records skipped follow-up tool responses for calls that must not run, preserves the pending tool-response history needed for replay, waits for pending message rewrites before ending the turn, and propagates nested cancellation through the current Agent and sibling Agent calls in the same batch. It also keeps cancellation telemetry and hooks consistent by using the active Agent abort signal for cancelled Agent executions.

Why it's needed

Users reported that after the 5-minute ask_user_question timeout, the model continued executing. The root cause was that a cancelled question was represented as a tool error but the ACP turn loop continued feeding tool responses back to the model, and nested Agent cancellations did not consistently abort or suppress later Agent activity. This PR prevents the model from continuing without required user input and keeps daemon/ACP state, hooks, and replay records aligned with that stop.

Reviewer Test Plan

How to verify

Run cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts && npx vitest run src/acp-integration/session/SubAgentTracker.test.ts and expect both files to pass. Run npm run build && npm run typecheck from the repo root and expect exit 0. Reviewers can also simulate cancelling an ACP ask_user_question and confirm no later model follow-up or sibling Agent success hook runs after cancellation.

Evidence (Before & After)

N/A for UI evidence. Before this change, cancellation could be returned as a tool error and the turn could continue; after this change, cancellation ends the turn and records skipped tool responses for unexecuted calls.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Local macOS worktree using the repository npm workspace. Verification commands: cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts && npx vitest run src/acp-integration/session/SubAgentTracker.test.ts; npm run build && npm run typecheck.

Risk & Scope

  • Main risk or tradeoff: The daemon now intentionally stops the current ACP turn when user input is required but unavailable, so later tool calls from the same model response are skipped rather than executed.
  • Not validated / out of scope: Manual Windows and Linux validation was not run locally; full interactive ACP UI testing was not performed in this PR.
  • Breaking changes / migration notes: No breaking API changes or migrations are expected.

Linked Issues

N/A

中文说明

What this PR does

这个 PR 让 ACP 工具执行在 ask_user_question 被取消后立即停止,包括嵌套 Agent 场景。它会为不应继续运行的后续工具记录 skipped tool response,保留 replay 需要的待发送工具响应 history,在结束 turn 前等待 pending message rewrite,并把嵌套取消传播到当前 Agent 以及同一批次里的 sibling Agent。它还通过使用当前 Agent 的 active abort signal,让取消场景下的 telemetry 和 hook 语义保持一致。

Why it's needed

用户反馈 5 分钟 ask_user_question 超时后模型继续执行。根因是取消的问题被表示成工具错误后,ACP turn loop 仍然把工具响应继续交给模型,而且嵌套 Agent 取消没有稳定地中断或抑制后续 Agent 活动。这个 PR 防止缺少必要用户输入时模型继续执行,并让 daemon/ACP 状态、hooks 和 replay 记录与停止语义保持一致。

Reviewer Test Plan

How to verify

运行 cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts && npx vitest run src/acp-integration/session/SubAgentTracker.test.ts,预期两个测试文件都通过。从仓库根目录运行 npm run build && npm run typecheck,预期 exit 0。评审也可以模拟取消 ACP ask_user_question,确认取消后不会继续模型 follow-up,也不会触发 sibling Agent 的成功 hook。

Evidence (Before & After)

非 UI 改动,UI 证据为 N/A。变更前,取消可能作为工具错误返回并让 turn 继续;变更后,取消会结束当前 turn,并为未执行的调用记录 skipped tool response。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

本地 macOS worktree,使用仓库 npm workspace。验证命令:cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts && npx vitest run src/acp-integration/session/SubAgentTracker.test.tsnpm run build && npm run typecheck

Risk & Scope

  • Main risk or tradeoff: daemon 现在会在需要但无法获得用户输入时有意停止当前 ACP turn,因此同一模型响应里的后续工具调用会被跳过而不是执行。
  • Not validated / out of scope: 本地未进行 Windows 和 Linux 手动验证;本 PR 未执行完整交互式 ACP UI 测试。
  • Breaking changes / migration notes: 预期没有破坏性 API 变更或迁移要求。

Linked Issues

N/A

Stop ACP tool execution after ask_user_question is cancelled, including nested Agent cancellation paths.

Co-authored-by: Qwen-Coder <[email protected]>
@doudouOUC
doudouOUC marked this pull request as ready for review June 17, 2026 02:53
Copilot AI review requested due to automatic review settings June 17, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Resolve the Session.ts conflict after upstream moved the mid-turn queue drain method into acp-bridge shared types.

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

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No review findings. Downgraded from Approve to Comment: CI still running.

Well-structured fix: the RunToolResult wrapper cleanly propagates cancellation through all 4 tool-call sites (main, retry, background, subagent loops). The recordSkippedToolCall + appendSkippedAfter pattern correctly records skipped tool calls so the model sees them on resume. The activeToolAbortSignal fix (vs parent abortSignal) is the right scoping. Comprehensive test coverage. ✅ — qwen3-coder via Qwen Code /review

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review timed out after 85 minutes. See workflow logs.

Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
@doudouOUC
doudouOUC requested a review from wenshao June 17, 2026 06:20
doudouOUC and others added 3 commits June 17, 2026 14:20
Address PR QwenLM#5218 review feedback by sending a failed ACP tool update for tool calls skipped after ask_user_question cancellation.

Co-authored-by: Qwen-Coder <[email protected]>
Integrate the remote PR branch updates before pushing the review feedback fix.

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI still running.

Additional finding (unchanged code, not in diff): firePreToolUseHook (line 3602) and firePostToolUseHook (line 3722, success path) pass the original abortSignal instead of activeToolAbortSignal. For Agent tools where a nested ask_user_question was cancelled, activeToolAbortSignal is aborted while abortSignal may not be. The PostToolUseFailure hook at line 3860 correctly uses activeToolAbortSignal, but the success-path hooks don't. Low practical impact today since the success hook is guarded by !aborted, but worth aligning for consistency.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.test.ts

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Request Changes to Comment: self-PR. 3 Critical findings and 2 Suggestions below as inline comments.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/SubAgentTracker.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
@doudouOUC
doudouOUC requested a review from DragonnZhang June 17, 2026 11:36
@doudouOUC doudouOUC self-assigned this Jun 17, 2026
Comment thread packages/cli/src/acp-integration/session/SubAgentTracker.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
@wenshao

wenshao commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR @doudouOUC!

Template looks good ✓

On direction: this fixes a real user-facing bug — after the 5-minute ask_user_question timeout, the model keeps executing without the user's answer, which is a broken experience. Claude Code's CHANGELOG has several related cancellation/subagent-stop fixes, confirming this area is squarely within scope. Clear direction alignment.

On approach: the scope feels right for what it's solving. The cancellation signal needs to propagate through four distinct layers (top-level prompt loop → sequential tool batches → concurrent Agent siblings → nested sub-agent questions), and the diff addresses each with a dedicated mechanism (abort controllers, RunToolResult wrapper, onAskUserQuestionCancel callback, #preserveCancelledAskUserQuestionToolRun). The test count is high (~1000 lines) but each test covers a distinct propagation path — that's justified for a fix that touches this many concurrency boundaries. No unrelated changes or drive-by refactors spotted.

One question worth thinking about: the onAskUserQuestionCancel callback is threaded through SubAgentTracker constructor and then into runTool/runBounded as an optional parameter. This works, but it adds a new optional parameter at three levels. An alternative would be an AbortController with a sentinel reason (like the existing USER_CANCEL_ABORT_REASON) shared between the tracker and the batch runner, checked at the same points. That might reduce the parameter surface. Not a blocker — just a thought for the author to consider.

Moving on to code review. 🔍

中文说明

感谢 @doudouOUC 的贡献!

模板完整 ✓

方向:这修复了一个真实的用户问题——ask_user_question 5 分钟超时后模型继续执行,用户体验是断裂的。Claude Code 的 CHANGELOG 有多条类似的取消/子代理停止修复记录,说明这个方向完全在项目范围内。方向对齐,没有问题。

方案:范围与解决的问题匹配。取消信号需要穿透四个不同层级(顶层 prompt 循环 → 顺序工具批次 → 并发 Agent 兄弟 → 嵌套子代理问题),diff 里每一层都有对应机制(abort controller、RunToolResult 包装、onAskUserQuestionCancel 回调、#preserveCancelledAskUserQuestionToolRun)。测试量虽然大(约 1000 行),但每个测试覆盖一条不同的传播路径——对于涉及这么多并发边界的修复来说是合理的。没有看到无关改动或顺手重构。

一个值得思考的问题:onAskUserQuestionCancel 回调穿过 SubAgentTracker 构造函数,再作为可选参数传入 runTool/runBounded,一共三层。另一种做法是共享一个带 sentinel reason 的 AbortController(类似已有的 USER_CANCEL_ABORT_REASON),在同样的检查点判断。这样可以减少参数传递。不是阻塞项,只是供作者考虑。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the cancellation propagation logic across Session.ts and SubAgentTracker.ts. The abort signal plumbing (parent → batch controller → per-tool controller → nested agent) is correct with proper cleanup in finally blocks. The RunToolResult wrapper and skipped-tool recording preserve replay history correctly.

No new issues beyond what existing inline comments already cover. Build passes, CI green across all platforms.

— qwen3.7-plus via Qwen Code /review

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Reviewed the diff against my independent proposal. The problem requires cancelling the turn at four distinct concurrency boundaries (top-level prompt loop, sequential batches, concurrent Agent batches, nested sub-agent questions), and the PR handles each with a dedicated mechanism. The RunToolResult wrapper is the cleanest way to thread the stop signal through the existing return types without refactoring runToolCalls into a generator or event emitter.

No critical blockers found. Specific observations:

  • Abort controller layering is correct. The per-batch batchAbortController wraps the parent abortSignal, and the per-Agent agentToolAbortController wraps the tool-level signal. Cleanup happens in finally blocks and removeEventListener calls — no leaked listeners.
  • recordSkippedToolCall is resilient. It catches client notification failures (e.g. disconnected ACP client) but still records the skipped response in history. The test preserves skipped tool responses when skipped tool updates fail covers this explicitly.
  • Hook semantics are consistent. PostToolUse success hooks are suppressed for cancelled/aborted tools, PostToolUseFailure fires with isInterrupt: true — matches the core path in coreToolScheduler.ts.
  • SubAgentTracker onAskUserQuestionCancel ordering is correct. The callback fires before event.respond() in the success path (parent learns about cancel first) and is wrapped in try/catch in the error path. The new test verifies ordering via mock.invocationCallOrder.
  • Sequential batch path doesn't pass onStopAfterUserQuestionCancel — intentionally, because the outer for loop in runToolCalls checks r.stopAfterUserQuestionCancel after each tool and skips the rest. Correct.

One minor note: the onAskUserQuestionCancel callback threads through three levels (SubAgentTracker constructor → runTool parameter → runBounded parameter). It works, but a shared AbortController with a sentinel reason might be slightly cleaner. Already flagged in Stage 1 — not a blocker.

Tests

Unit tests: 173/173 passed (Session.test.ts: 145, SubAgentTracker.test.ts: 28). The new tests cover all cancellation propagation paths: direct cancel, sequential skip, concurrent batch abort, nested agent cancel, sibling abort, unstarted skip, hook suppression, interrupt labeling, and rewrite-wait-before-end.

Build: ✅ exit 0 (lint warnings are pre-existing in vscode-ide-companion, unrelated to this PR)

Typecheck: ✅ exit 0 across all packages

Real-Scenario Testing

Basic smoke test — npm run dev -- -p 'Say hello in one word' before (main) and after (PR branch). Both produce identical output, confirming no regression in the normal execution path.

Before (installed build — main branch)

runner@runnervm1li68:~/work/qwen-code/qwen-code$ npm run dev -- -p 'Say hello in one word' 2>&1 | tail -20

> @qwen-code/[email protected] dev
> node scripts/dev.js -p Say hello in one word

DEV is set to true, but the React DevTools server is not running. Start it with:

$ npx react-devtools

Hello!
runner@runnervm1li68:~/work/qwen-code/qwen-code$

After (this PR)

runner@runnervm1li68:~/work/qwen-code/qwen-code/.qwen/worktrees/triage$ npm run dev -- -p 'Say hello in one word' 2>&1 | tail -20

> @qwen-code/[email protected] dev
> node scripts/dev.js -p Say hello in one word

DEV is set to true, but the React DevTools server is not running. Start it with:

$ npx react-devtools

Hello!
runner@runnervm1li68:~/work/qwen-code/qwen-code/.qwen/worktrees/triage$

Limitation: the specific bug (ask_user_question 5-minute timeout → model continues executing) requires an ACP client to cancel the permission request, which isn't reproducible in a simple tmux prompt. The 173 unit tests thoroughly cover this cancellation path at every concurrency boundary.

中文说明

代码审查

对照我的独立方案审查了 diff。问题需要在四个不同的并发边界(顶层 prompt 循环、顺序批次、并发 Agent 批次、嵌套子代理问题)取消 turn,PR 在每一层都有对应机制。RunToolResult 包装是在不重构 runToolCalls 为 generator 或 event emitter 的前提下,传递停止信号的最干净方式。

没有发现关键阻塞项。具体观察:

  • Abort controller 分层正确。 批次级 batchAbortController 包装父级 abortSignal,Agent 级 agentToolAbortController 包装工具级信号。清理在 finallyremoveEventListener 中完成——没有泄漏的 listener。
  • recordSkippedToolCall 有韧性。 捕获客户端通知失败(如断连的 ACP 客户端),但仍然在 history 中记录 skipped 响应。
  • Hook 语义一致。 取消/中止的工具不触发 PostToolUse 成功 hook,PostToolUseFailureisInterrupt: true——与 core 路径一致。
  • SubAgentTracker onAskUserQuestionCancel 顺序正确。 成功路径中回调在 event.respond() 之前触发;错误路径中有 try/catch 保护。
  • 顺序批次路径不传 onStopAfterUserQuestionCancel——有意为之,因为外层 for 循环在每个工具后检查 r.stopAfterUserQuestionCancel 并跳过剩余工具。正确。

测试

单元测试: 173/173 通过。新测试覆盖所有取消传播路径。

构建: ✅ exit 0(lint 警告是 vscode-ide-companion 的已有问题,与本 PR 无关)

类型检查: ✅ exit 0

真实场景测试

基本冒烟测试——前后对比 npm run dev -- -p 'Say hello in one word',输出一致,无回归。

限制: 特定 bug(ask_user_question 5 分钟超时 → 模型继续执行)需要 ACP 客户端取消权限请求,无法在简单 tmux 提示中复现。173 个单元测试已全面覆盖此取消路径。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection

This PR fixes a real, user-reported bug: after the 5-minute ask_user_question timeout, the model keeps running without the user's answer. The fix is well-scoped — every change traces back to propagating a cancellation signal through the ACP session's concurrency layers.

Going back to my independent proposal: I would have used a shared AbortController with a sentinel reason, checked at the same boundaries. The PR takes a slightly different path — a RunToolResult wrapper plus an onAskUserQuestionCancel callback — which is more explicit about why the turn is stopping (not just "aborted" but specifically "user question was cancelled"). That explicitness is a net positive for maintainability. The #preserveCancelledAskUserQuestionToolRun helper, which I wouldn't have thought to add, ensures replay correctness — a detail that matters for daemon sessions.

The test suite (173 tests, ~1000 lines of additions) is large, but each test isolates a distinct propagation path: direct cancel, sequential skip, concurrent batch abort, nested agent, sibling abort, unstarted skip, hook suppression, interrupt labeling, rewrite-wait, cron stop. For a fix that touches this many concurrency boundaries, the coverage is insurance, not bloat.

Build, typecheck, and smoke test all pass. No critical blockers in code review.

The one reservation from Stage 1 stands — the callback threading through three constructor/parameter levels is slightly more surface area than a pure abort-controller approach would need. But it's a stylistic choice, not a correctness issue, and the author clearly thought about it (the callback name is descriptive, the parameter is optional at every level).

Verdict: this is a solid, well-tested fix for a real bug. Ready to ship. ✅

中文说明

反思

这个 PR 修复了一个真实的、用户反馈的 bug:ask_user_question 5 分钟超时后,模型在没有用户回答的情况下继续执行。修复范围聚焦——每处改动都可以追溯到在 ACP session 的并发层之间传播取消信号。

回到我的独立方案:我会用一个共享的 AbortController 加 sentinel reason,在同样的边界检查。PR 走了稍微不同的路径——RunToolResult 包装加 onAskUserQuestionCancel 回调——更明确地表达 turn 停止的原因(不只是"aborted",而是"user question 被取消了")。这种明确性对可维护性是正面的。#preserveCancelledAskUserQuestionToolRun 是我不会想到要加的,它保证了 replay 正确性——对 daemon session 很重要的细节。

测试套件(173 个测试,约 1000 行新增)量大,但每个测试隔离一条传播路径。对于涉及这么多并发边界的修复,这些覆盖是保险而非膨胀。

构建、类型检查、冒烟测试全部通过。代码审查无关键阻塞项。

Stage 1 的保留意见依然成立——回调穿过三层构造/参数,比纯 abort-controller 方案多了一些参数传递。但这是风格选择,不是正确性问题,而且作者显然考虑过了(回调名有描述性,每层参数都是可选的)。

结论:这是一个扎实的、经过充分测试的真实 bug 修复。可以合入。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@doudouOUC
doudouOUC merged commit 3ff7e7f into QwenLM:main Jun 17, 2026
24 checks passed
@doudouOUC
doudouOUC deleted the codex/fix-ask-user-question-cancel branch June 17, 2026 13:28
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review timed out after 85 minutes. See workflow logs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants