Skip to content

feat(telemetry): Track tool execution outcomes - #8180

Merged
wenshao merged 29 commits into
QwenLM:mainfrom
doudouOUC:codex/8179-tool-execution-status
Aug 3, 2026
Merged

feat(telemetry): Track tool execution outcomes#8180
wenshao merged 29 commits into
QwenLM:mainfrom
doudouOUC:codex/8179-tool-execution-status

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR introduces an execution-specific outcome alongside the existing terminal tool-call status. The terminal status continues to describe the final result of the whole call, while executionStatus records whether invocation.execute() was entered and whether it succeeded, failed, or was cancelled.

Core scheduler and ACP tool calls now use the same state contract, freeze the execution outcome when execution settles, and preserve that result through hooks, result persistence, recording, image bridging, and batch post-processing. Validation failures, permission denial, PreToolUse blocking, the host invocation guard introduced by #8176, duplicate calls, and other synthetic pre-execution responses are explicitly marked as not started. Cancellation that arrives while failure hooks settle now wins consistently in both Core and ACP, while an approved plan is redacted from model history before a post-processing cancellation is finalized.

Tool-call telemetry is normalized once before fan-out to UI telemetry, chat recording, QwenLogger, OTLP logs, and metrics. The change adds call IDs, execution status, consistent cancellation and error classification, a low-cardinality execution-outcome counter, and execution child spans that exist only for attempted executions. It preserves the terminal status dimension on qwen-code.tool.call.count established by #8176 and does not otherwise change that metric schema. QwenLogger receives terminal and execution outcomes but does not receive MCP server names or function arguments. User-facing TUI and stream-JSON protocols remain compatible.

Why it's needed

The terminal result cannot distinguish a tool implementation that failed after execution started from validation, permission, hook, or post-processing failures that prevented or followed execution. This makes the observed tool failure rate misleading and obscures whether the executable tool boundary is healthy.

The additional execution axis provides a stable execution failure-rate denominator while excluding cancellations, calls that never started, and historical events whose execution state is unavailable. It also makes Core and ACP failure handling observable under the same contract.

Reviewer Test Plan

How to verify

  1. Verify that validation failure, an unregistered tool, permission rejection, a blocking pre-tool hook, and host invocation-guard denial finish as terminal errors with execution not started, while explicit permission cancellation and cancellation during the host guard finish as cancelled with execution not started.
  2. Verify that successful execution, returned tool errors, synchronous or asynchronous exceptions, timeouts, and mid-execution cancellation produce the expected independent terminal and execution outcomes.
  3. Verify that post-execution hook failures, cancellation during failure hooks, post-processing cancellation, approved-plan history redaction, and batch-hook replacement preserve the execution result frozen when execution settled.
  4. Verify that every normalized built-in telemetry event contains a call ID and a known execution status, cancellation has no call-level error fields, recognized MCP failures and timeouts retain their classifications, QwenLogger excludes MCP server metadata, and the new counter contains only execution-status and tool-source event dimensions plus any globally opted-in common metric attributes.
  5. Verify that each attempted execution produces exactly one execution span, pre-execution failures produce none, cancelled parent and derived spans remain UNSET, and TUI and stream-JSON output do not expose the new internal fields.

Automated verification on the latest PR head completed with 466 focused Core tests and 506 ACP session tests passing. The repository build, typecheck, full lint, and diff whitespace checks also passed.

Evidence (Before & After)

N/A — this change does not alter user-visible TUI or stream-JSON output.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Node.js v22.22.3, npm 10.9.8, sandbox disabled for local unit, build, lint, and typecheck verification.

Risk & Scope

  • Main risk or tradeoff: Tool execution crosses cancellation, permission, host-guard, hook, timeout, recording, and span lifecycle boundaries, so incorrect arbitration could produce duplicate terminal records or misclassified outcomes. Cancellation during post-processing intentionally replaces the model-visible response with a cancellation notice even when tool side effects already completed; this preserves cancellation semantics but can hide a successful tool output from the model. Approved plan contents are redacted before that replacement so cancellation cannot retain the full plan in model history. Race-focused regression coverage exercises these boundaries. This cross-package Core/CLI feature touches core infrastructure and requires maintainer review.
  • Not validated / out of scope: A live model-backed TUI telemetry session was not run because it requires external credentials and network access. Mixed PostToolBatch replacement-span attribution remains a separate follow-up because it requires a broader span-lifecycle change. Speculation, direct /fork execution, MCP-internal retries, provisional subagent-result reconciliation, shell exit or signal metadata, OpenTelemetry gen_ai.execute_tool.duration alignment, generic retryability, and historical-data migration are also out of scope.
  • Breaking changes / migration notes: No user-facing TUI or stream-JSON protocol break is expected, but embedders should account for source and behavior changes. Public response/event fields remain optional and JSONL recordings gain an optional executionStatus; CancelledToolCall.tool and .invocation can now be absent for cancellation before resolution. Per-tool execution failures delivered through CoreToolScheduler.schedule(): Promise<void> now complete through update/completion callbacks instead of rejecting the scheduling promise, while scheduler-level queue/setup failures can still reject; handleConfirmationResponse() terminalizes confirmation-flow errors and then rethrows. Permission transport failures are classified as unhandled_exception rather than user denial, recognized MCP execution failures use mcp_tool_error, and post-processing cancellation replaces the model-visible tool response. The terminal metric schema from feat(core): Normalize tool-call terminal telemetry #8176 is preserved. The execution counter has only execution_status and tool_type event dimensions, while globally configured common attributes such as opt-in session.id may also be present. Dashboards should cut over by deployment time or service.version and should not mix historical events into the execution-failure SLI.

Linked Issues

Closes #8179

中文说明

本 PR 的作用

本 PR 在现有工具调用终态之外新增独立的执行结果。终态继续描述整个调用的最终结果,executionStatus 则记录是否进入了 invocation.execute(),以及执行成功、失败还是被取消。

Core 调度器和 ACP 工具调用现在使用同一套状态约定,在执行 settle 时冻结执行结果,并在 hook、结果持久化、recording、图片桥接和批次后处理期间保持该结果不变。校验失败、权限拒绝、PreToolUse 阻断、#8176 引入的 host invocation guard、重复调用以及其他执行前 synthetic response 都会被显式标记为未开始执行。失败 hook settle 期间到达的取消现在会在 Core 和 ACP 中一致地胜出;批准的计划会先从模型历史中脱敏,再完成后处理取消的终态化。

工具调用遥测在扇出到 UI telemetry、聊天记录、QwenLogger、OTLP 日志和指标之前统一归一化一次。本变更新增 call ID、执行状态、一致的取消与错误分类、低基数执行结果计数器,以及仅在实际尝试执行时存在的 execution 子 Span。它保留 #8176qwen-code.tool.call.count 上建立的终态 status 维度,不再修改该指标的其他 schema。QwenLogger 接收终态和执行结果,但不接收 MCP server 名称或函数参数。用户可见的 TUI 和 stream-JSON 协议保持兼容。

为什么需要

终态无法区分工具实现在开始执行后失败,与校验、权限、hook 或后处理导致的执行前或执行后失败。这会使观测到的工具失败率失真,也无法判断真正的工具执行边界是否健康。

新增执行轴后,可以建立稳定的执行失败率分母,并排除取消、从未开始执行的调用,以及缺少执行状态的历史事件;同时 Core 与 ACP 的失败处理也能按照同一约定观测。

Reviewer 测试计划

如何验证

  1. 验证校验失败、工具未注册、权限拒绝、阻断型 PreToolUse hook 和 host invocation guard 拒绝最终为终态错误且执行未开始;显式权限取消以及 host guard 等待期间的取消最终为已取消且执行未开始。
  2. 验证执行成功、工具返回错误、同步或异步异常、超时和执行中取消会生成预期的独立终态与执行状态。
  3. 验证执行后 hook 失败、失败 hook 期间取消、后处理取消、批准计划历史脱敏和批次 hook 重建响应时,会保留执行 settle 时冻结的结果。
  4. 验证所有内建归一化遥测事件都包含 call ID 和已知执行状态,取消事件不包含调用级错误字段,已识别 MCP 故障和超时保持正确分类,QwenLogger 不包含 MCP server 元数据,并且新计数器只包含执行状态与工具来源事件维度以及全局显式启用的公共指标属性。
  5. 验证每次实际尝试执行恰好生成一个 execution Span,执行前失败不生成 execution Span,取消的父 Span 和派生 Span 均保持 UNSET,并且 TUI 与 stream-JSON 输出不会泄露新增内部字段。

基于最新 PR head 的自动化验证结果为 466 个 Core 定向测试和 506 个 ACP session 测试通过;仓库 build、typecheck、完整 lint 和 diff 空白检查也全部通过。

证据(变更前后)

N/A — 本变更不修改用户可见的 TUI 或 stream-JSON 输出。

测试平台

操作系统 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

Node.js v22.22.3、npm 10.9.8;本地单元测试、build、lint 和 typecheck 验证未启用 sandbox。

风险与范围

  • 主要风险或权衡:工具执行跨越取消、权限、host guard、hook、超时、recording 和 Span 生命周期边界,错误的状态仲裁可能产生重复终态记录或错误分类。后处理阶段发生取消时,即使工具副作用已经完成,也会按设计使用取消提示替换模型可见响应;这保持了取消语义,但可能让模型看不到成功的工具输出。批准的计划内容会先完成脱敏再进行替换,避免取消路径把完整计划保留在模型历史中。针对竞态的回归覆盖验证了这些边界。该跨 Core/CLI 的功能涉及核心基础设施,需要 maintainer 审核。
  • 未验证 / 范围外:由于需要外部凭据和网络访问,未运行真实模型驱动的 TUI 遥测会话。混合 PostToolBatch 场景下的 replacement span 归属需要更广的 Span 生命周期调整,留作独立后续。Speculation、直接 /fork 执行、MCP 内部重试、子代理 provisional result 协调、Shell exit 或 signal 元数据、OpenTelemetry gen_ai.execute_tool.duration 对齐、通用 retryability 和历史数据迁移也不在本次范围内。
  • 破坏性变更 / 迁移说明:预期没有用户可见的 TUI 或 stream-JSON 协议破坏,但嵌入方需要关注源码与行为变化。公共 response/event 字段保持可选,JSONL recording 新增可选的 executionStatus;对于工具解析前取消,CancelledToolCall.tool.invocation 现在可能不存在。通过 CoreToolScheduler.schedule(): Promise<void> 执行的单工具失败现在经 update/completion callback 完成,不再拒绝调度 Promise,但调度器级队列或初始化故障仍可拒绝;handleConfirmationResponse() 会先终态化确认流程错误再重新抛出。权限传输故障归类为 unhandled_exception 而不是用户拒绝,已识别 MCP 执行故障使用 mcp_tool_error,后处理取消会替换模型可见的工具响应。feat(core): Normalize tool-call terminal telemetry #8176 建立的终态指标 schema 保持不变。execution counter 只有 execution_statustool_type 事件维度,但也可能包含全局配置的公共属性,例如显式启用的 session.id。面板应按部署时间或 service.version 切换,并且不应把历史事件混入执行失败率 SLI。

关联 Issue

Closes #8179

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Jul 31, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

E2E / behavioral verification report

Baseline

  • Global qwen executable: version 0.21.1.
  • A live model-backed TUI telemetry run was not performed because it requires external credentials and network access; no live-output evidence is claimed.

Automated verification

  • Core lifecycle, MCP classification, telemetry normalization, metrics, QwenLogger, and span suites: 810 passed.
  • CLI/ACP lifecycle, recording, TUI bridge, duplicate/skipped response, and non-interactive protocol suites: 798 passed, 1 skipped.
  • npm run lint: passed.
  • npm run build: passed.
  • npm run typecheck: passed.
  • git diff --check: passed.

Behavioral results

  • Pre-execution rejection and explicit permission cancellation retain executionStatus=not_started.
  • Successful, failed, timed-out, and cancelled executions freeze the execution result at the execute() boundary.
  • Post-execution hooks and PostToolBatch response replacement preserve the frozen execution status, including historical missing values.
  • Built-in tool-call telemetry includes a call ID and known execution status; cancellations do not carry call-level error fields.
  • Stream-JSON success and error envelopes do not expose either executionStatus or execution_status.
  • TUI rendering is unchanged; the new value is copied only into recording and telemetry metadata.

Out of scope

Speculation, direct /fork execution, MCP-internal retries, provisional subagent-result reconciliation, shell exit/signal diagnostics, standard gen_ai.execute_tool.duration alignment, and historical-data migration were not exercised because they are intentionally excluded from this change.

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code did not run conflict resolution for this request.

PR #8180 is draft.

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

1 similar comment
@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code did not run conflict resolution for this request.

PR #8180 is draft.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the codex/8179-tool-execution-status branch from 60542f1 to 78b294d Compare July 31, 2026 15:31
@doudouOUC
doudouOUC marked this pull request as ready for review July 31, 2026 15:34
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code did not run conflict resolution for this request.

PR #8180 does not currently have merge conflicts with main.

@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

🔄 Qwen Triage is runningwatch live progress. Stage results will post in this thread as they complete.

🔄 Qwen Triage 正在运行 —— 查看实时进度。各阶段结果完成后会更新在本线程。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Re-run triggered by @wenshao — gate re-check on the current head afd349ca.

  • Template: complete ✓
  • Problem: real and specified in feat(telemetry): Distinguish tool execution outcomes from terminal call status #8179 (self-reported by the PR author, already labeled as such). This is an observability gap, not a user-facing bug: the terminal status conflates pre-execution denials (validation, permission, hook, invocation guard) with failures returned during execution, so the observed tool failure rate is misleading and the health of the executable tool boundary cannot be measured. The maintainer's Aug 2 real-environment verification shows the gap concretely: on main, tool_call events carry no call_id/execution_status and the qwen-code.tool.execution span is anonymous.
  • Direction: aligned. Trustworthy tool telemetry is core mission, and the two-axis design explicitly preserves the qwen-code.tool.call.count schema established by feat(core): Normalize tool-call terminal telemetry #8176. No direct execution-axis reference found in the Claude Code CHANGELOG, but the area is plainly relevant.
  • Size: core paths touched. Production logic ≈ 3,011 lines (coreToolScheduler.ts 1,208; ACP Session.ts 1,007; telemetry modules ≈ 700; mcp-tool.ts 156), tests ≈ 4,458 lines, docs ≈ 141. feat type → no hard block; 500+ core production lines → maintainer-awareness flag — that awareness has been thoroughly exercised (a dozen review rounds plus a full real-environment verification). The 1000+ line advisory is noted too; splitting a fully verified, deeply reviewed state contract at this point would be more disruptive than useful, so informational only.
  • Approach: scope matches the stated goal. The bulk of the diff is cancellation/race arbitration around the execution boundary — that is precisely what makes the two-axis contract trustworthy, not scope creep. The open scope questions (guarded-await helper, session-dispose span wiring, warn-level sink logging) were raised by @wenshao and declined with reasons in autofix round 11; those deferrals are maintainer call items, not gate blockers.
  • Risk: packages/cli/src/acp-integration/** matches this repo's revert-correlated high-risk paths → full review depth and CI evidence before any approval. Both are done below.

Moving on to code review. 🔍

中文说明

@wenshao 触发的重跑——在当前 head afd349ca 上复查各道门。

  • 模板:完整 ✓
  • 问题:真实存在,feat(telemetry): Distinguish tool execution outcomes from terminal call status #8179 有明确描述(作者自报,已打相应标签)。这是可观测性缺口而非用户可见 bug:终态 status 把执行前拒绝(校验、权限、hook、invocation guard)与执行中的失败混为一谈,导致观测到的工具失败率失真,也无法衡量可执行工具边界的健康度。维护者 8 月 2 日的真实环境验证具体展示了该缺口:main 上的 tool_call 事件没有 call_id/execution_statusqwen-code.tool.execution span 是匿名的。
  • 方向:对齐。可靠的工具遥测是核心职责,双轴设计明确保留了 feat(core): Normalize tool-call terminal telemetry #8176 建立的 qwen-code.tool.call.count schema。Claude Code CHANGELOG 中没有执行轴的直接参照,但该领域显然相关。
  • 规模:触及核心路径。生产逻辑约 3,011 行coreToolScheduler.ts 1,208、ACP Session.ts 1,007、遥测模块约 700、mcp-tool.ts 156),测试约 4,458 行,文档约 141。feat 类型 → 不硬拦;核心路径 500+ 生产行 → 需维护者关注——这一关注已经充分落实(十余轮评审加完整真实环境验证)。1000+ 行大 PR 提示同样记录在案;此时拆分一个已完整验证、深度评审过的状态契约只会更添乱,仅作信息记录。
  • 方案:范围与目标匹配。diff 的大头是执行边界周围的取消/竞态仲裁——这正是让双轴契约可信的部分,不是范围蔓延。尚存的范围问题(guarded-await 辅助函数、会话销毁时的 span 收尾、warn 级 sink 日志)已由 @wenshao 提出,并在 autofix 第 11 轮带理由拒绝延后;这些属于维护者裁量项,不是门槛阻塞项。
  • 风险packages/cli/src/acp-integration/** 命中本仓库与 revert 相关的高风险路径 → 批准前需完整 review 深度与 CI 证据,下文均已覆盖。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at afd349ca705415bf40c213787848b00ecf7ae0c4 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Code review at afd349ca

This is a re-run after 11 autofix rounds and multiple maintainer reviews, so the review focuses on whether the current head actually landed what it claims. I read the full production diff; no critical blockers found.

The two must-fix items from @wenshao's Aug 2 review are verifiably fixed here:

  • Cancel-message divergence (issue pre-release: fix ci #1) — both cancel-message sites in coreToolScheduler.ts's catch block now gate on the executionThrew flag: execution that threw under abort reports 'User cancelled tool execution.' (matching ACP), while only the true post-settle path keeps "The tool had already completed; its output was discarded." Verified in the diff, including the post-failure-hook re-check.
  • Module cycle (issue Where is the config saved? #2)StructuredToolError moved into tool-error.ts; priorReadEnforcement.ts re-exports it for back-compat; mcp-tool.ts imports from tool-error.js directly. Dependency direction is now one-way. Verified.

The three declined items (#3 guarded-await helper, #4 session-dispose span wiring, #5 warn-level sink logging) have reasoned write-ups in round 11 and are already named as follow-ups in the design doc itself. They are maintainer-judgment calls, not gate items. The one factual claim I checked independently — #6, the generate-notices.js churn — holds: main's two ~88-char collectDependencies(...) lines exceed the 80-char width, so the project's Prettier toolchain re-applies the reformat on any commit touching the file. Formatting noise, nothing more.

Cross-checks: the automated /review (round 2, v0.21.3) on this exact head reported zero findings. My own pass over coreToolScheduler.ts, Session.ts, mcp-tool.ts, and the telemetry modules agrees — and two details are worth calling out as genuinely good: withPostToolBatchStop only carries executionStatus into the replacement response when the replaced call had one (a batch stop cannot invent an outcome the tool never produced), and the MCP createParentAbortRace change is a real bug fix beyond telemetry (the SDK's abort rejection shares the -32001 timeout code, so freezing the parent outcome before forwarding is what makes EXECUTION_TIMEOUT classification trustworthy).

The settlement/freeze/cancel flow that the whole contract rests on:

sequenceDiagram
    participant P1 as CoreToolScheduler
    participant P2 as Tool invocation
    participant P3 as Hooks
    participant P4 as Telemetry fan-out
    P1->>P2: enter execute() - open execution span, mark executing
    P2-->>P1: settle - success, tool error, or throw
    P1->>P1: freeze execution outcome at settle
    P1->>P3: PostToolUse or PostToolUseFailure
    alt parent abort arrives after settle
        P1->>P1: cancellation wins - terminal cancelled, frozen outcome kept
    end
    P1->>P4: one normalized event, one execution span per attempt
Loading
Files changed (21 groups covering all 36 files)
File What changed
docs/design/2026-07-31-tool-execution-status.md New design doc - valid terminal/execution pairs, normalization rules, dashboard cutover guidance
docs/developers/development/telemetry.md Documents the new event fields, counter, and span attributes next to the existing schema
packages/cli/src/acp-integration/session/Session.ts ACP half of the contract - explicit status at every terminal path, abort checks after each await, fault-tolerant emits
packages/cli/src/acp-integration/session/Session.test.ts Race-shaped tests for the ACP paths (+1,576 lines)
packages/cli/src/acp-integration/session/SubAgentTracker.ts + test Emit failures swallowed to debug log; abort-driven cancels no longer fire onPermissionCancel
packages/cli/src/nonInteractiveCli.ts + test Synthetic pre-execution responses marked not_started
packages/cli/src/ui/hooks/useGeminiStream.ts + test Passes executionStatus through result recording
packages/cli/src/ui/hooks/useReactToolScheduler.ts + useToolScheduler.test.ts mapToDisplay handles cancelled calls whose tool/invocation never resolved
packages/core/src/core/coreToolScheduler.ts Core half of the contract - outcome frozen at settle, PostToolBatch span deferral, queue drain hardening
packages/core/src/core/coreToolScheduler.test.ts Outcome matrix and cancellation-race coverage (+1,721 lines)
packages/core/src/core/turn.ts + tests ToolExecutionStatus type; optional executionStatus on ToolCallResponseInfo (back-compat)
packages/core/src/hooks/types.ts Doc comment for the PostToolBatch serialized response fields
packages/core/src/telemetry/constants.ts + index.ts Failure-kind vocabulary centralized; new counter exported
packages/core/src/telemetry/loggers.ts + test normalizeToolCallEvent exported (unknown when absent, error fields deleted on non-errors); per-sink isolation via runToolTelemetrySink
packages/core/src/telemetry/metrics.ts + test New low-cardinality qwen-code.tool.execution.count counter (execution_status + tool_type only)
packages/core/src/telemetry/qwen-logger/qwen-logger.ts + test Gains call_id and execution_status; deliberately no MCP server name or args
packages/core/src/telemetry/session-tracing.ts + test Execution spans gain identity (tool name, call id) and status; span start is fail-safe to NOOP
packages/core/src/telemetry/log-to-span-processor.ts + test Cancelled tool-call events stay UNSET even when legacy error fields exist
packages/core/src/telemetry/types.ts ToolCallEvent gains call_id and optional execution_status
packages/core/src/tools/mcp-tool.ts + test Parent-abort race fix; request-timeout classification to EXECUTION_TIMEOUT
packages/core/src/tools/tool-error.ts + priorReadEnforcement.ts StructuredToolError relocated, cycle broken (issue #2)

The 36th file, packages/vscode-ide-companion/scripts/generate-notices.js, is Prettier-forced formatting churn (see findings above).

Test evidence — the PR's own CI (this run executed no PR code)

Per triage rules this is an unattended run: no PR build, test, or script was executed here. The evidence is the PR's own CI on the reviewed commit, fetched via API, plus the named live-behavior signals below.

All checks on afd349ca are complete — nothing pending, nothing failing:

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
SDK Java matrix (ubuntu 11/17/21, macOS 21, windows 21) ✅ success
review-pr ✅ success
Integration Tests (CLI, No Sandbox) ⚪ skipped
Test (macos-latest / windows-latest, Node 22.x) ⚪ skipped

The Integration Tests (CLI, No Sandbox) skip is the one check a human should be aware of — the automated /review flagged the same: that suite did not run in CI and its assertions are covered by unit tests only. All other gates are green.

Live-behavior signal (named, per 2b-bis): the central claim here is behavioural — the outcome contract holding under cancellation races — and a green suite alone does not pin it. Two stronger signals exist, and I'm naming both rather than papering over the gap:

  1. @wenshao's real-environment verification (Aug 2, comment above): full outcome matrix against a real CLI, real OTLP exporter, real ACP agent, and a real TUI in tmux, with a before/after baseline on main. That is maintainer evidence on head 83e685da4 — clearly attributed, not this bot's own run.
  2. The delta between that head and this one (afd349ca) is six commits of review-feedback fixes and test hardening. A sandboxed @qwen-code /verify run dispatched by this triage trigger is in flight on the current head (watch it live); its report will be posted to this thread when it completes. The code under verification comes from a fork, so the report should be read with the same skepticism as the fork's own CI logs.

No user-visible TUI or stream-JSON surface changes (by design — verified as protocol-clean in the maintainer's run), so there is no tmux before/after to drive even on a local run.

中文说明

代码审查(基于 afd349ca

这是第 11 轮 autofix、多次维护者评审之后的重跑,审查重点是当前 head 是否真正落地了它所声称的内容。我通读了全部生产代码 diff,未发现阻塞性问题。

@wenshao 8 月 2 日评审中的两个必改项在此 head 上可验证地已修复:

  • 取消消息不一致(问题 pre-release: fix ci #1——coreToolScheduler.ts catch 块中两处取消消息现在都以 executionThrew 标志为条件:abort 下执行抛错时报告 'User cancelled tool execution.'(与 ACP 一致),只有真正执行完成后的路径才保留 "The tool had already completed; its output was discarded."。diff 中已核实,包括失败 hook 之后的二次检查。
  • 模块循环(问题 Where is the config saved? #2——StructuredToolError 移入 tool-error.tspriorReadEnforcement.ts 为兼容保留 re-export;mcp-tool.ts 直接从 tool-error.js 导入。依赖方向现在是单向的。已核实。

三个被拒绝的条目(#3 guarded-await 辅助函数、#4 会话销毁时的 span 收尾、#5 warn 级 sink 日志)在第 11 轮有带理由的书面说明,设计文档本身也将其列为后续项。这些属于维护者裁量项,不是门槛项。我独立核实了其中一个事实性声明——#6 generate-notices.js 的格式化噪音成立:main 上两行约 88 字符的 collectDependencies(...) 超过 80 字符宽度,任何触及该文件的提交都会被项目的 Prettier 工具链重新格式化。纯格式化噪音,仅此而已。

交叉核对: 自动化 /review(第 2 轮,v0.21.3)在这个 head 上报告 零发现。我自己对 coreToolScheduler.tsSession.tsmcp-tool.ts 和遥测模块的审查结论一致——有两处值得点名表扬:withPostToolBatchStop 只在被替换的调用本身带有 executionStatus 时才把它带入替换响应(批次停止不会凭空捏造工具从未产生的结果);MCP 的 createParentAbortRace 修复是超越遥测的真实 bug 修复(SDK 的 abort 拒绝与真实超时共用 -32001 码,先在转发前冻结父结果,EXECUTION_TIMEOUT 分类才可信)。

序列图展示了整个契约所依赖的 settle/freeze/cancel 流程(英文见上):调度器在 execute() 边界打开 execution span 并标记 executing;执行 settle(成功、工具错误、抛异常)时立即冻结执行结果;随后运行 PostToolUse/PostToolUseFailure hook;若父 abort 在 settle 之后到达,取消获胜——终态为 cancelled,但冻结的执行结果保留;最后每个实际尝试恰好产出一条归一化事件和一个 execution span。

变更文件概览表见上(21 组覆盖全部 36 个文件)。

测试证据——PR 自己的 CI(本次运行未执行任何 PR 代码)

按 triage 规则,这是无人值守运行:未构建、未测试、未执行任何 PR 脚本。证据为通过 API 获取的被审 commit 的 CI 结果,加上下文点名的实时行为信号。

afd349ca 上所有 check 均已完成——无 pending、无失败(表格见上)。Integration Tests (CLI, No Sandbox) 的跳过是唯一需要人工留意的 check——自动化 /review 也指出了同一点:该套件未在 CI 运行,其断言仅由单测覆盖。其余所有门禁均为绿色。

实时行为信号(按 2b-bis 规则点名): 本 PR 的核心主张是行为性的——取消竞态下状态契约依然成立——仅靠绿色套件无法锁定。此处点名两个更强的信号,而不是掩盖缺口:

  1. @wenshao真实环境验证(8 月 2 日,见上方评论):真实 CLI、真实 OTLP 导出器、真实 ACP agent、tmux 真实 TUI 上的完整结果矩阵,并与 main 做了前后对照。那是维护者在 head 83e685da4 上的证据——明确标注来源,不是本机器人的运行结果。
  2. 该 head 与当前 head(afd349ca)之间的差异是六个 commit 的评审反馈修复与测试加固。由本次 triage 触发的沙箱 @qwen-code /verify 运行正在当前 head 上进行(链接见上),完成后报告会发布到本线程。被验证代码来自 fork,阅读报告时应与对待 fork 自身 CI 日志保持同样的审慎。

本 PR 无用户可见的 TUI 或 stream-JSON 面变更(设计如此——维护者验证已确认协议无泄漏),因此即使在本地运行中也无需 tmux 前后对照。

Qwen Code · qwen3.8-max-preview

Reviewed at afd349ca705415bf40c213787848b00ecf7ae0c4 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean re-review on afd349ca, but Stage 0 escalates ~3,000 core production lines for maintainer sign-off, so the bot defers instead of approving no matter how clean the pass looks.

Stepping back over the whole arc of this PR:

  • My independent baseline for "track tool execution outcomes" was exactly the shape that landed: an execution axis frozen at the invocation.execute() boundary, normalized once before fan-out, a dedicated low-cardinality counter, and execution spans that exist only for attempted executions. Where the PR goes beyond that baseline — the cancellation arbitration, the PostToolBatch span deferral, the MCP abort-race fix — is precisely where the contract could silently lie, so the extra weight is earned, not speculative.
  • The problem exists and was verified, not assumed: feat(telemetry): Distinguish tool execution outcomes from terminal call status #8179 states it, and the maintainer's before/after on main shows events with no call id, no execution axis, and anonymous execution spans.
  • The hard parts were reviewed, not just written. wenshao's six-issue review landed two fixes here and got reasoned declines on three scope items that the design doc already names as follow-ups. The automated /review on this head found nothing. CI is fully green.
  • The residual honesty: this PR changes real semantics for embedders (schedule() no longer rejects per-call, CancelledToolCall.tool/.invocation are optional, post-processing cancellation replaces a completed tool's model-visible output). All are documented in the PR body and design doc, and all were consciously exercised in the maintainer's real-environment run — but they are the reason this merge wants a human hand on it, not a bot's.

⏸️ Deferring to @wenshao. The gate's remaining question is pure policy, not doubt: this is a fork PR touching core infrastructure at ~3,011 production lines, which the two-tier rule escalates for maintainer awareness — and awareness here has been thorough (12 review rounds plus a full real-environment verification on Aug 2). If the in-flight sandboxed verify report lands clean on this head and you are satisfied the embedder-facing semantic changes above are consciously accepted, this is ready for a human approval. One housekeeping note: the standing CHANGES_REQUESTED on this PR is my own stale /review from Jul 31 on 842dce56 (six un-updated test expectations — fixed many commits ago); it can be dismissed when you approve.

中文说明

置信度:3/5 —— 在 afd349ca 上的复审是干净的,但 Stage 0 将约 3,000 行核心生产代码升级为维护者签核项,因此无论这轮审查看起来多干净,机器人都选择 defer 而非批准。

退后一步看这个 PR 的完整轨迹:

  • 我的独立基线("跟踪工具执行结果"该怎么做)与最终落地的形态一致:在 invocation.execute() 边界冻结的执行轴、扇出前归一化一次、专用低基数计数器、仅在实际尝试执行时存在的 execution span。PR 超出基线的部分——取消仲裁、PostToolBatch span 延迟收尾、MCP abort 竞态修复——恰恰是契约可能悄悄说谎的地方,因此这些额外分量是必要的,不是过度设计。
  • 问题真实存在,是被验证出来的而非假设:feat(telemetry): Distinguish tool execution outcomes from terminal call status #8179 有明确陈述,维护者在 main 上的前后对照显示事件没有 call id、没有执行轴、execution span 是匿名的。
  • 难的部分是被评审过的,而不只是被写出来的。 wenshao 的六项评审在此 head 上落地了两项修复,三项范围条目得到了带理由的拒绝(设计文档本身已将其列为后续项)。自动化 /review 在这个 head 上零发现。CI 全绿。
  • 残留的诚实: 本 PR 对嵌入方改变了真实语义(schedule() 不再因单调用失败而 reject、CancelledToolCall.tool/.invocation 变为可选、后处理取消会替换已完成工具的模型可见输出)。这些都记录在 PR 正文和设计文档中,也都在维护者的真实环境运行中被有意验证过——但正因如此,这次合并需要人类之手,而不是机器人的批准。

⏸️ 移交 @wenshao 门槛剩下的问题是纯策略性的,而非疑虑:这是一个触及核心基础设施约 3,011 行生产代码的 fork PR,两级规则将其升级为维护者关注——而此处的关注已经非常充分(12 轮评审加 8 月 2 日的完整真实环境验证)。如果进行中的沙箱验证报告在当前 head 上干净落地,且你确认上述嵌入方语义变更是被有意接受的,这个 PR 就可以由人类批准了。一个内务提示:本 PR 上挂着的 CHANGES_REQUESTED 是我 7 月 31 日在 842dce56 上的过期 /review(六个未更新的测试断言——许多 commit 前已修复),批准时可以顺手 dismiss。

Qwen Code · qwen3.8-max-preview

Reviewed at afd349ca705415bf40c213787848b00ecf7ae0c4 · re-run with @qwen-code /triage

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Review — feat(telemetry): Track tool execution outcomes (78b294d)

Reviewed the full diff against origin/main in a clean worktree, ran the affected suites locally, and read the CI log for run 30643289881.

Verdict: CHANGES REQUESTED — one blocker (red CI caused by this diff), plus scope and a few correctness/consistency issues.


Overview

Adds a second, independent outcome axis to tool calls: ToolExecutionStatus = 'not_started' | 'success' | 'error' | 'cancelled', frozen when invocation.execute() settles and preserved through hooks, recording, image bridging, and batch post-processing. Core (CoreToolScheduler) and ACP (Session.runTool) implement the same contract; telemetry normalizes once before fanning out to UI telemetry, chat recording, QwenLogger, OTLP logs, metrics, and a new qwen-code.tool.execution.count counter. qwen-code.tool.call.count keeps its status dimension from #8176.

The core idea is right, and freezing at settle rather than re-deriving downstream is the only way this axis stays meaningful.


🔴 Blocker

B1 — CI is red, and the failure is caused by this diff.
Test (ubuntu-latest, Node 22.x) fails with 6 failures in packages/core/src/core/nonInteractiveToolExecutor.test.ts, a file the PR does not touch. executeToolCall() routes through CoreToolScheduler, so every response it returns now carries executionStatus, and the test asserts the response shape with toStrictEqual.

Reproduced locally on the PR head (Tests 6 failed | 3 passed). Confirmed the fix is mechanical — adding the field to the 6 expectations turns the file green (Tests 9 passed):

test expected executionStatus
should execute a tool successfully (call1) 'success'
should return an error if tool is not found (call2) 'not_started'
should return an error if tool validation fails (call3) 'not_started'
should return an error if tool execution fails (call4) 'error'
should return an unhandled exception error if execution throws (call5) 'error'
should correctly format llmContent with inlineData (call6) 'success'

Those values are all correct per the design doc, so this is stale expectations only — no deeper defect. Worth noting that the PR body's verification claim ("821 focused Core tests passing") is what let this through: the focused-test approach skipped the one non-focused file the change touches. Please run the full suite before the next push.


🟠 Important

I1 — Scope: this is three or four PRs in one trench coat.
Beyond the execution-status axis, the diff also lands:

  • a substantial rewrite of MCP cancellation/timeout semantics in packages/core/src/tools/mcp-tool.tscreateParentAbortRace, a parentAbortController replacing the raw parent signal inside AbortSignal.any([...]), isMcpRequestTimeout mapping -32001 to StructuredToolError(EXECUTION_TIMEOUT), and an early throw in handleReconnectOnError when the signal is aborted;
  • ~40 new cancelPreExecutionIfAborted / cancelBeforeExecutionIfAborted checkpoints across coreToolScheduler.ts and Session.ts that reclassify many pre-execution terminals from error to cancelled;
  • SubAgentTracker.ts changes with no telemetry content at all — .catch() on the emitters plus !abortSignal.aborted gating on onPermissionCancel?.();
  • executeSingleToolCall no longer re-throwing — the throw error; that used to follow the setStatusInternal at coreToolScheduler.ts:4183-4195 is gone. Real behavior change: a prelude throw in one tool no longer aborts the remaining batches in attemptExecutionOfScheduledCalls, and schedule() no longer rejects for it. This looks like a genuine improvement, but it is invisible in the PR description;
  • drainRequestQueueIfIdle() added to _schedule's finally — this fixes a real stall (a _schedule that produces zero tool calls never reaches checkAndNotifyCompletion, so the queue was previously never drained), also undocumented;
  • blanket try/catch hardening in loggers.ts, session-tracing.ts, and notifyToolCallsUpdate.

Each of these is individually defensible. Together they make a 5,695/1,067-line diff whose blast radius spans Core + CLI + ACP + MCP cancellation, and which cannot be reverted piecemeal if a regression surfaces. At minimum I'd split out the MCP cancellation rewrite and the SubAgentTracker changes.

I2 — deriveSpanStatus cancellation short-circuit is global, not tool-scoped.
packages/core/src/telemetry/log-to-span-processor.ts:461:

if (attrs['status'] === 'cancelled') {
  return { code: SpanStatusCode.UNSET };
}

This runs before the error / error.message checks and applies to every log record converted to a span, not just tool_call. Several other events carry a status that can be 'cancelled' (telemetry/types.ts:975, :1004, :1456). Any of those emitted with an error attribute now silently lose their ERROR status. If the intent is tool-scoped — and the new test name (keeps cancelled tool calls UNSET) says it is — gate on attrs['event.name'] === EVENT_TOOL_CALL.

I3 — the deferred PostToolBatch span and the call whose response gets replaced can be different calls.
coreToolScheduler.ts:2517 picks the span to defer as the last validating call:

const postToolBatchParentCallId = newToolCalls.findLast(
  (toolCall) => toolCall.status === 'validating',
)?.request.callId;

but withPostToolBatchStop replaces completedCalls[length - 1] — the last call in request order, which can be a pre-validation error/cancel that never got a span (spans are only opened in the status === 'validating' loop at :2553). When those two differ, the setToolSpanFailure(stoppedSpan, TOOL_FAILURE_KIND_POST_HOOK_STOPPED, …) at :5744 no-ops, so the batch-stop failure_kind is lost on exactly the call it describes, while a different span is held open for nothing. Keying both off the same call (e.g. defer the last completed call's span) would close the gap.

I4 — cancellation now discards already-produced successful output; the PR body says the opposite.
cancelAfterPostProcessing in core (defined at coreToolScheduler.ts:4807, applied at :4902, :5208, :5369, :5490) and the post-processing re-check in Session.runTool (Session.ts:8702-8713) convert a call whose execute() already returned successfully into terminal cancelled, replacing responseParts with Tool execution was cancelled. when the abort lands during post-processing (PostToolUse hook, large-output persistence, vision bridge). The tool's side effects — file writes, shell mutations — remain; the model just never sees what happened.

That is a defensible reading of Ctrl+C and matches the existing during-execution behavior, but "Evidence (Before & After): N/A — this change does not alter user-visible TUI or JSON output" doesn't cover it. Please call it out under Risk & Scope.


🟡 Suggestions

  • S1packages/cli/src/utils/nonInteractiveHelpers.ts:351 (buildResponse) still constructs a ToolCallResponseInfo without executionStatus, so subagent-derived responses normalize to unknown. That contradicts the design doc's "Built-in Core and ACP producers always set the field". Either populate it or add subagent projections to the doc's explicit exclusions (they're in the PR's out-of-scope list but not the doc's).
  • S2 — ACP Session.runTool passes bare string literals ('tool_cancelled', 'tool_timeout', 'tool_error', 'tool_exception') to endToolExecutionSpan({ error }) while core uses the TOOL_SPAN_STATUS_* constants. Export and reuse them so the two producers of the same span can't drift.
  • S3notifyToolCallsUpdate now swallows observer exceptions into debugLogger.warn. That downgrades a whole class of TUI bug from loud to silent; debugLogger.error would at least keep it findable.
  • S4executionStatus now reaches session-recording JSONL (recordToolResultssanitizeToolCallResultForRecording is a pass-through). Additive and harmless, but worth a line in the doc's Compatibility section since replay readers will see a new field.
  • S5 — The doc states "Only success/error and success/cancelled are invalid combinations", but nothing enforces it. A dev-mode assertion (or a normalization step at the telemetry boundary) would keep the contract from rotting as new terminal paths get added.
  • S6 — Branch is 5 commits behind origin/main (merge base 0d3c864). Rebase before merge.

✅ What holds up

  • Test coverage is substantive, not decorative. 780 targeted core tests pass locally on the head commit — coreToolScheduler 346, mcp-tool 75, session-tracing 150, loggers 73, metrics 48, log-to-span-processor 52, turn 36. The new race tests genuinely drive the abort-in-flight paths (resolve-enablement-after-abort, resolve-permission-deny-after-abort) rather than mirroring the implementation.
  • Freezing at settle is the right design. executionStatus is captured immediately before invocation.execute() (pessimistically 'error') and only narrowed on settle, so setup failures stay not_started and a synchronous throw inside execute() is still correctly attributed. The execSpan-inside-the-callback placement makes the span boundary match that contract exactly, and I confirmed there's no await between the abort guard and the transition, so the widened scheduled window can't be re-entered.
  • Metric hygiene. qwen-code.tool.execution.count carries only execution_status + tool_type, and tool.call.count is untouched — feat(core): Normalize tool-call terminal telemetry #8176's schema survives.
  • Telemetry can no longer break a tool call. runToolTelemetrySink around each fan-out sink plus try/catch in startToolSpan / startToolExecutionSpan is the right fix, and normalizeToolCallEvent deleting error/error_type on non-error terminals is strictly cleaner than the previous undefined-valued keys.
  • CancelledToolCall.tool/invocation optionality is handled at the one runtime-sensitive consumer (useReactToolScheduler.mapToDisplay), and TypeScript covers the rest.

@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.

[Critical] CI is red: Test (ubuntu-latest, Node 22.x) fails with 6 test failures in packages/core/src/core/nonInteractiveToolExecutor.test.ts. The PR adds executionStatus to every CoreToolScheduler response, but the 6 toStrictEqual expectations in that file (lines 104, 189, 227, 271, 308, 350) were not updated. Reproduced locally at HEAD 842dce5: Tests 6 failed | 3 passed. The fix is mechanical — add executionStatus: 'success' or 'not_started' (per the design doc's terminal/execution pairs) to each expectation. This was first reported by @wenshao in an earlier review and remains unfixed at the current commit.

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +4686 to 4689
execution_status: 'error',
}),
}),
expect.objectContaining({

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.

[Suggestion] The gamma assertion in this PostToolBatch payload test omits execution_status, leaving the "error-in-result-body" path unverified for the new field. — Concrete cost: gamma’s execute resolves (not rejects) with { error: { message: ‘gamma failed’ } }, which the scheduler normalizes to executionStatus = ‘error’ via the toolResult.error ? ‘error’ : ‘success’ branch (coreToolScheduler.ts ~line 4743). This is a distinct code path from beta’s rejection-based error. If a future refactor incorrectly sets execution_status for the resolved-with-error path, this test would still pass because gamma’s expect.objectContaining never checks the field.

Suggested change
execution_status: 'error',
}),
}),
expect.objectContaining({
tool_response: expect.objectContaining({
error: ‘gamma failed’,
error_type: ToolErrorType.UNKNOWN,
execution_status: ‘error’,
}),
中文说明

[Suggestion] 此 PostToolBatch 载荷测试中的 gamma 断言缺少 execution_status,导致“结果体中包含 error”的路径未被新字段验证。

— qwen3.8-max-preview via Qwen Code /review

resultDisplay: undefined,
error,
errorType,
errorType: errorType ?? ToolErrorType.EXECUTION_DENIED,

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.

[Suggestion] The EXECUTION_DENIED default is applied to loop-detection skips, which are not approval-mode denials. — Concrete cost: recordSkippedToolCall is called without an explicit errorType at three loop-detection call sites (lines 6608, 6819, 7124). Before this diff those sites recorded errorType: undefined (mapped to UNKNOWN by telemetry). After this diff they record EXECUTION_DENIED, inflating the denial metric.

Suggested change
errorType: errorType ?? ToolErrorType.EXECUTION_DENIED,
errorType: errorType ?? ToolErrorType.UNKNOWN,
中文说明

[Suggestion] EXECUTION_DENIED 默认值被应用于循环检测跳过,而这些并非审批模式拒绝。任何按 error_type === ‘execution_denied’ 过滤的遥测面板现在也会将循环检测跳过计入,导致拒绝指标失真。

— qwen3.8-max-preview via Qwen Code /review

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Jul 31, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 12 finishedview run. See this round's report below.

中文说明

AutoFix 第 12 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action needed

All actionable feedback sections for this round are empty:

  • Reviews: none newer than the last evaluation
  • Inline comments: none
  • Issue-level comments: none
  • Failed checks: none
  • Still-red checks: none

The only feedback item newer than the last evaluation is a non-Critical PR comment by @qwen-code-ci-bot, which the workflow placed in the Deferred non-Critical feedback section because Critical-only mode is active (after 10 change-producing rounds). That section is an audit record, not work for this round: per the Critical-only rules, no code is changed, no thread is resolved, and no reply is written for deferred items. It remains open for human follow-up.

No code changes were made and no commit was created. The PR head stays at its current commit.

中文说明

Autofix 评审轮次:无需处理

本轮所有可执行的反馈区域均为空:

  • Reviews(评审): 上次评估之后没有新的评审
  • Inline comments(行内评论):
  • Issue-level comments(议题级评论):
  • Failed checks(失败的检查):
  • Still-red checks(持续失败的检查):

上次评估之后唯一的反馈条目是 @qwen-code-ci-bot 发布的一条非 Critical PR 评论,由于当前处于 Critical-only 模式(已完成 10 个产生改动的轮次),工作流将其放入了 Deferred non-Critical feedback(延后的非 Critical 反馈) 区域。该区域是审计记录,不属于本轮的工作内容:按照 Critical-only 规则,本轮不会为被延后的条目修改代码、解决讨论串或撰写回复,该条目保持开放,留待人工跟进。

本轮未做任何代码改动,也未创建任何提交。PR 分支停留在当前提交上。

Deferred non-Critical feedback

Critical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@doudouOUC doudouOUC self-assigned this Aug 3, 2026
@wenshao

wenshao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Review: feat(telemetry): Track tool execution outcomes (#8180)

Reviewed at afd349c (+6444 / −1166, 36 files).

What this PR does

Adds a second, independent outcome axis to tool calls: ToolExecutionStatus = 'not_started' | 'success' | 'error' | 'cancelled', recording whether invocation.execute() was actually entered and how it settled, alongside the existing terminal status. The value is frozen when execution settles and carried through hooks, persistence, recording, image bridging and batch post-processing. CoreToolScheduler and ACP Session.runTool both populate it; telemetry normalizes once before fan-out, adds call_id + execution_status to tool_call, adds a low-cardinality qwen-code.tool.execution.count counter, and emits qwen-code.tool.execution child spans only for attempted executions.

What's good

  • The design doc (docs/design/2026-07-31-tool-execution-status.md) states the contract, the valid/invalid (terminal, execution) pairs, and — refreshingly — its own Known Maintenance Hazards section. That honesty is worth more than the code.
  • Normalizing once in normalizeToolCallEvent before all five sinks removes real drift; delete-ing (rather than undefined-ing) error fields on non-error events is the right call for OTLP consumers.
  • The new counter deliberately omits function_name and documents the drill-down path. Correct instinct for metric cardinality.
  • runToolTelemetrySink isolating each sink so one exporter failure can't drop the others, and the try/catch hardening in startToolSpan / startToolExecutionSpan, are good defensive changes.
  • Test coverage is genuinely thorough — ~75 new tests, most of them race-focused (preserves cancellation when X resolves after abort appears in ~10 variants). The gating Ubuntu leg is green.
  • setStatusInternal already no-ops on terminal states, so the removal of the re-throw in executeSingleToolCall can't produce a duplicate terminal record. Verified.

Main concerns

1. Scope — this is not a telemetry PR

The commit title says feat(telemetry), but the diff also contains:

  • packages/core/src/tools/mcp-tool.ts — a new parent-abort race (createParentAbortRace), rewiring combinedSignal off the parent signal, and a new -32001 → EXECUTION_TIMEOUT classification. This changes MCP cancellation and timeout semantics.
  • coreToolScheduler.tsexecuteSingleToolCall no longer re-throws; a new drainRequestQueueIfIdle() in _schedule's finally; notifyToolCallsUpdate() now swallows observer exceptions; setArgsInternal returns a boolean that now gates four call sites.
  • CancelledToolCall.tool / .invocation become optional — a public type weakening for embedders.
  • ACP: model-visible tool output is now replaced on post-execution cancellation.
  • packages/vscode-ide-companion/scripts/generate-notices.js — a pure Prettier reformat, entirely unrelated.

Each is individually defensible, and the description does disclose them. But bundled together they make this hard to review, hard to bisect, and effectively un-revertable: if the execution-axis rollout needs to be pulled after deploy, the MCP cancellation fix goes with it. At minimum, please split out the MCP cancellation/timeout change and the generate-notices.js reformat.

2. Post-processing cancellation drops persistedOutputFiles (data loss, not just output loss)

coreToolScheduler.tscancelAfterPostProcessing() replaces the built successResponse with createCancelledResponse(...), which carries only artifacts:

const successResponse: CoreToolCallResponseInfo = {
  ...,
  ...(persistedOutputFiles !== undefined ? { persistedOutputFiles } : {}),
  ...
};
...
if (cancelAfterPostProcessing(artifacts)) {
  return;                       // ← persistedOutputFiles, visionBridgeNotice, modelOverride all dropped
}
this.setStatusInternal(callId, 'success', successResponse);

The description acknowledges "a successful tool output can be hidden from the model". The unacknowledged part is that when the tool spilled a large result to disk, the file references are dropped too — the files stay on disk with nothing pointing at them, so neither cleanup nor later retrieval can find them. The same applies to the ACP path where responseParts is overwritten.

Suggest carrying persistedOutputFiles (and visionBridgeNotice) into createCancelledResponse so cancellation loses the model-visible content without orphaning on-disk state.

3. 'The tool had already completed' is asserted on a path where it may be false

The exception path correctly distinguishes:

let cancelMessage = executionThrew
  ? 'User cancelled tool execution.'
  : 'The tool had already completed; its output was discarded.';

…but the result-returned path asserts completion unconditionally:

if (aborted) {
  let cancelMessage = 'The tool had already completed; its output was discarded.';

A tool that observes the abort signal and returns a partial result — the common case for an interrupted shell command — reaches this branch. Telling the model it "had already completed" is factually wrong there and could change how the model reasons about side effects. A neutral phrasing (e.g. 'Tool output was discarded due to cancellation.') covers both cases without asserting something unverifiable.

4. MCP -32001 can mislabel a user cancel as a timeout

isMcpRequestTimeout() classifies any error with code === -32001 as EXECUTION_TIMEOUT. The abort path deliberately forwards the abort into the SDK, whose rejection uses the same code. Correctness rests entirely on the microtask-ordering argument in createParentAbortRace:

// Ordering-safe: resolve() queues its Promise.race reaction as a
// microtask before forwardAbort triggers the SDK rejection...
resolve({ [PARENT_ABORT_OUTCOME]: true, reason });
forwardAbort?.(reason);

The reasoning holds today, but it's load-bearing and invisible — a future SDK change that rejects on a different tick, or any added await before the race, silently reclassifies user cancels as tool timeouts and inflates the timeout SLI. idleTimeoutWon already has a signal.aborted guard; the -32001 branch doesn't. A one-line !signal.aborted && in front of isMcpRequestTimeout(error) in both catch blocks makes this robust regardless of ordering.

5. Invalid params recorded as tool_exception

New in setArgsInternal:

setToolSpanFailure(toolSpan, TOOL_FAILURE_KIND_TOOL_EXCEPTION, invocationError.message);

…while the response carries ToolErrorType.INVALID_TOOL_PARAMS. This fires when a PreToolUse hook's updatedInput or an editor-modified param fails validation — neither is the tool implementation throwing. Dashboards cutting by tool.failure_kind will attribute these to tool health. The PR's own comment on the failure-kind vocabulary ("so dashboards can attribute denies to their real cause") argues for a distinct kind here.


Smaller points

  • withPostToolBatchStop — the executionStatus dance is unreachable and hard to read. It passes executionStatus ?? 'not_started' into createErrorResponse, immediately destructures the result away, then conditionally re-adds. Core always populates executionStatus, so the undefined branch is dead for core-produced calls. { ...baseResponse, ...(executionStatus !== undefined && { executionStatus }) } says the same thing in one expression.

  • Two acknowledged telemetry gaps are only visible at debug level. The deferred PostToolBatch parent span leaks until process exit when a batch never completes for a non-abort reason, and post_hook_stopped silently has no span to attach to on mixed batches (last-completed ≠ last-validating). Both are honestly commented — but a debugLogger.debug means production will never know it's degrading. Consider warn (one-shot, like postToolBatchConfigWarned) for the dropped-span case, and force-finalizing on session dispose for the leak.

  • The pre-execution abort invariant is 47 hand-placed guards. 22 cancelPreExecutionIfAborted(...) in coreToolScheduler.ts, 25 cancelBeforeExecutionIfAborted(...) in Session.ts, each in ACP with its own uniquely-named local (enablementCancellation, planPolicyCancellation, hookPlanApprovalCancellation, …). The design doc names this as the top maintenance hazard, and it's right: one new await without a following guard silently reintroduces the stale-execution bug. Since this PR is already touching every one of those sites, it's the cheapest moment this will ever be to wrap them in a guarded helper that throws a cancellation sentinel caught once at the top. Worth doing before more awaits accumulate.

  • Silent telemetry sinks. runToolTelemetrySink logging at debug means uiTelemetryService.addEvent failing would make the TUI session stats quietly stop updating with no signal at default log level. A one-shot warn per sink would be enough.

  • normalizeToolCallEvent now writes the trimmed error_type (event.error_type?.trim() || UNKNOWN), where the old code preserved the original string. There's a test asserting function_name survives byte-for-byte; the equivalent isn't asserted for error_type. Harmless in practice, just noting the silent change.

  • unknown bucket. Every non-Core/non-ACP producer (subagent projections, replay, third parties) lands in execution_status=unknown. Documented and correctly excluded from the SLI — but the counter has little value until that share is small. Worth confirming dashboards land before/with deploy, as the doc recommends.

Verified, no action needed

  • No unguarded consumers of the now-optional CancelledToolCall.tool / .invocation remain (mapToDisplay guards both; toPostToolBatchToolCall doesn't touch them; ToolCallEvent narrows via instanceof).
  • drainRequestQueueIfIdle() in _schedule's finally can't drain into a half-finished batch: checkAndNotifyCompletion() runs synchronously to isFinalizingToolCalls = true before the first await, and isRunning() covers it.
  • The pre-validation try/catch around each request always continues after a push, so no double-push into newToolCalls.

Verdict: the execution-axis design is sound, the contract is well documented, and the race coverage is unusually good for a change this size. Blocking items for me are (2) persistedOutputFiles loss and (4) the MCP -32001 guard — both small fixes. (1) is a process ask: please peel off the MCP change and the generate-notices.js reformat so the telemetry rollout can be reverted independently.

…ants

afd349c gated the cancel message on executionThrew but left the two
wordings as bare literals at four sites and added no test. That is the
exact shape the bug had: it was introduced by editing one literal and
missing the others.

Extract TOOL_CANCELLED_{BEFORE,AFTER}_COMPLETION_MESSAGE so the four
sites cannot drift, and add regression tests for both paths — a tool
interrupted mid-flight (execute() rejected under abort) must report
"User cancelled tool execution.", while a cancel after execute()
returned must report that the output was discarded. The mid-flight test
fails against the pre-afd349ca behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wenshao

wenshao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real stack, base vs. head

I built this PR locally and exercised it end to end against the real CLI binary, not unit mocks: dist/cli.js from a full npm ci && npm run build && npm run bundle, driven by a mock OpenAI-compatible SSE provider that emits scripted tool_calls, with OTel telemetry redirected through --telemetry-outfile and parsed back out. Every number below comes from a telemetry file produced by an actual process, and every row is run twice — once on the PR head, once on the merge-base — so the "before" column is a real build, not a description.

  • head afd349ca7 · base (merge-base) 41f0e3ca5 · Node v24.18.1 · npm 11.16.0 · macOS 26.6

1. Terminal status vs. execution status — the core claim

status matrix

The middle column is the point. On base, rows 02–06 are all status: error and nothing in the event distinguishes them. On head, 02 and 07 (the tool actually ran and failed) stay error/error, while validation rejection, an unregistered tool, headless permission denial, and a blocking PreToolUse hook all become error/not_started. The qwen-code.tool.execution child span exists exactly for the attempted executions and for none of the pre-execution failures — no extra span was emitted anywhere I could find one.

Side checks on the same runs:

  • 05-permission-denied — the denied write_file left no file on disk, so not_started is describing reality, not just a label.
  • 07/08 — MCP tools were served by a real stdio MCP server; mcp_boom returning isError lands as error/error with error_type: mcp_tool_error and tool_type: mcp.
  • All five failure scenarios still complete the turn normally (the mock provider receives the second request, exit code 0) — consistent with the PR note that per-tool failures now flow through completion callbacks rather than rejecting schedule().

2. ACP path

Session.ts has its own runTool(), so I drove qwen --acp with a minimal JSON-RPC client over stdio (initializeauthenticatesession/newsession/set_modesession/prompt), handling session/request_permission and fs/* callbacks.

acp matrix

The last row is the one worth calling out to reviewers: this is a behaviour fix, not just a new field. On base, a user cancelling the ACP permission prompt was recorded as status: error, error.type: unknown, with error.message: "Tool \"write_file\" was canceled by the user." — a user decision counted against the tool failure rate. On head it is status: cancelled / execution_status: not_started, with no call-level error fields at all. acp-03 also sharpens from unknown to invalid_tool_params.

3. Cancellation, from a real TUI

Not simulated: real tmux session, real Ink TUI, a run_shell_command executing a 25s command, cancelled with ESC while it was running.

tui executing
tui cancelled

cancel telemetry

cancelled/cancelled, zero error fields on the event, and both the parent qwen-code.tool span and the qwen-code.tool.execution child span end with status code 0 (UNSET). The execution counter records execution_status=cancelled, which is exactly what you want excluded from a failure-rate denominator.

4. QwenLogger payload

QwenLogger only ships on a >60s flush interval, so I ran a long TUI session and intercepted the actual https.request upload in-process — this is the serialized body, not a reconstruction.

qwenlogger payload

call_id and execution_status are present; mcp_server_name and function_args are absent. Matches the PR description.

5. Compatibility

Surface Result
stream-json output Base and head produce 161 identical key paths, 0 differences. No executionStatus / execution_status anywhere in the stream.
qwen-code.tool.call.count dimensions Unchanged: function_name, status, success, decision, tool_type on both builds — the #8176 schema is preserved.
tool_call log attributes Head adds exactly two keys (call_id, execution_status), 13 → 15. Nothing removed or renamed.
Session JSONL recording Gains optional executionStatus on toolCallResult, as documented in the migration notes.
ACP session/update notifications No leakage of the new internal fields.

6. Focused test suites (local, PR head)

  • packages/corecoreToolScheduler, loggers, metrics, session-tracing, log-to-span-processor, mcp-tool, qwen-logger: 796 passed
  • packages/cliSession, SubAgentTracker, useToolScheduler: 563 passed

7. What I could not verify, and one observation

  • log-to-span-processor.ts cancelled→UNSET is unit-test-only in practice. I stood up an OTLP/HTTP receiver with a traces-only endpoint (the configuration that activates LogToSpanProcessor), ran both interactive and headless sessions, and got zero log.bridge-tagged derived spans — on head and on base (9 spans each, none bridged). So this is pre-existing behaviour, not a regression from this PR, but it does mean the new EVENT_TOOL_CALL && status === 'cancelled' → UNSET branch was not exercised on a live pipeline here. Worth a separate look at why the bridge is silent under a traces-only endpoint.
  • Not covered: OTLP gRPC transport, Windows/Linux, a real model-backed session, and the mixed PostToolBatch replacement-span attribution the PR already lists as out of scope.

Verdict

Every user-visible claim in the description that I could reach from outside the process reproduced, on both the Core scheduler and the ACP paths, with a real before/after. The execution axis is correctly not_started for all four pre-execution rejection paths I could construct, correctly error only when execute() actually ran, and correctly cancelled with no error contamination. No protocol or metric-schema regression detected. LGTM from a verification standpoint.

中文版本

Maintainer 验证 —— 真实栈,base 对比 head

我在本地构建了这个 PR,并针对真实 CLI 产物(而不是单测 mock)做了端到端验证:完整跑 npm ci && npm run build && npm run bundle 得到 dist/cli.js,用一个 mock 的 OpenAI 兼容 SSE 服务按脚本下发 tool_calls,通过 --telemetry-outfile 把 OTel 遥测落盘后再解析。下面每一个数字都来自真实进程产生的遥测文件;每一行都跑两遍——PR head 一遍,merge-base 一遍——所以"变更前"那一列是真实构建的结果,不是文字描述。

  • head afd349ca7 · base(merge-base)41f0e3ca5 · Node v24.18.1 · npm 11.16.0 · macOS 26.6

1. 终态与执行状态的分离 —— 核心诉求

见上文第一张图。重点在中间那一列:在 base 上,02–06 行全部status: error,事件里没有任何字段能区分它们。在 head 上,0207(工具确实执行了并失败)保持 error/error,而校验拒绝、未注册工具、headless 权限拒绝、阻断型 PreToolUse hook 全部变成 error/not_startedqwen-code.tool.execution 子 Span 恰好只在真正尝试执行时存在,所有执行前失败都没有产生该 Span——我没有找到任何多余的 Span。

同一批运行里的旁证:

  • 05-permission-denied —— 被拒绝的 write_file 磁盘上没有留下文件,说明 not_started 描述的是真实情况,不只是一个标签。
  • 07/08 —— MCP 工具由真实 stdio MCP server 提供;mcp_boom 返回 isError 落到 error/errorerror_type: mcp_tool_errortool_type: mcp
  • 五个失败场景都正常走完了该轮(mock provider 收到了第二次请求,退出码 0),与 PR 中"单工具失败改为经 completion callback 完成、不再 reject schedule()"的说明一致。

2. ACP 路径

Session.ts 有自己独立的 runTool(),所以我用一个最小 JSON-RPC 客户端通过 stdio 驱动 qwen --acpinitializeauthenticatesession/newsession/set_modesession/prompt),并处理 session/request_permissionfs/* 回调。

第二张图的最后一行值得 reviewer 特别注意:这是一个行为修复,不只是新增字段。 在 base 上,用户在 ACP 权限弹窗中取消,会被记录成 status: errorerror.type: unknownerror.message: "Tool \"write_file\" was canceled by the user."——用户的主动决定被算进了工具失败率。在 head 上它是 status: cancelled / execution_status: not_started,且完全没有调用级错误字段。acp-03 的分类也从 unknown 收敛为 invalid_tool_params

3. 真实 TUI 下的取消

不是模拟:真实 tmux 会话、真实 Ink TUI,run_shell_command 正在执行一个 25 秒的命令时用 ESC 取消。

结果是 cancelled/cancelled,事件上错误字段,父 qwen-code.tool Span 与 qwen-code.tool.execution 子 Span 都以状态码 0(UNSET)结束。执行计数器记录 execution_status=cancelled,正是失败率分母应该排除的那一类。

4. QwenLogger 载荷

QwenLogger 只在超过 60 秒的 flush 间隔后才上报,所以我跑了一个较长的 TUI 会话,并在进程内拦截了真实的 https.request 上报——第四张图是实际序列化出来的 body,不是重构出来的。

call_idexecution_status 存在;mcp_server_namefunction_args 不存在。与 PR 描述一致。

5. 兼容性

结果
stream-json 输出 base 与 head 产生完全相同的 161 条 key path,0 处差异。流中不含任何 executionStatus / execution_status
qwen-code.tool.call.count 维度 未变:两个构建都是 function_name, status, success, decision, tool_type#8176 的 schema 得到保留。
tool_call 日志属性 head 恰好新增两个 key(call_idexecution_status),13 → 15。没有删除或重命名。
会话 JSONL recording toolCallResult 上新增可选 executionStatus,与迁移说明一致。
ACP session/update 通知 没有泄露新增内部字段。

6. 定向测试(本地,PR head)

  • packages/core —— coreToolSchedulerloggersmetricssession-tracinglog-to-span-processormcp-toolqwen-logger796 通过
  • packages/cli —— SessionSubAgentTrackeruseToolScheduler563 通过

7. 未能验证的部分,以及一个观察

  • log-to-span-processor.ts 的 cancelled→UNSET 实际上只有单测覆盖。 我搭了一个 OTLP/HTTP 接收端并只配置 traces endpoint(这正是激活 LogToSpanProcessor 的配置),交互式和 headless 都跑了,结果一条 log.bridge 派生 Span 都没有——head 和 base 都一样(各 9 条 Span,无一来自桥接)。所以这是既有行为,不是本 PR 引入的回归,但也意味着新增的 EVENT_TOOL_CALL && status === 'cancelled' → UNSET 分支在真实链路上没有被跑到。建议单独排查为什么 traces-only endpoint 下桥接是静默的。
  • 未覆盖:OTLP gRPC 传输、Windows/Linux、真实模型驱动的会话,以及 PR 已声明范围外的混合 PostToolBatch replacement span 归属。

结论

描述中所有我能从进程外部观测到的用户可见声明都复现了,Core 调度器和 ACP 两条路径都验证过,并且有真实的变更前后对比。执行轴在我能构造的四种执行前拒绝路径上都正确为 not_started,只有 execute() 真正运行时才为 error,取消时为 cancelled 且不被错误信息污染。未发现协议或指标 schema 回归。从验证角度 LGTM。

wenshao
wenshao previously approved these changes Aug 3, 2026
@wenshao

wenshao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

Classifying every `-32001` as EXECUTION_TIMEOUT skips
handleReconnectOnError, which previously recovered one real case: the
transport dies mid-request, the SDK request times out because no
response will ever arrive, and the server is already recorded
DISCONNECTED. That reconnected and retried; now it hard-fails and the
user has to retry by hand.

Divert back to the reconnect path only on positive evidence the
transport is dead. Note that getMCPServerStatus() reports DISCONNECTED
for servers it has never seen, so the guard checks for a *recorded*
DISCONNECTED — the naive comparison misroutes every timeout from a
server whose status was never registered, which broke four existing
timeout tests when tried.

A timeout on a healthy server is still EXECUTION_TIMEOUT: retrying it
after a reconnect would just double the wait. The client-side idle
timeout keeps classifying unconditionally; it is our own timer, not a
transport signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 223 passed · 0 failed · 223 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:223 通过 · 0 失败 · 223 总计

Verification report

PR #8180 Deep Verification — feat(telemetry): Track tool execution outcomes (follow-up round 2)

Verdict: merge-ready — 223/223 scripted harness assertions passed (A/B lifecycle matrix 105 + MCP delta probe 53 + wire-oracle telemetry 65), 0 failed; targeted gates 1712 passed | 1 skipped | 0 failed across 14 test files (witness 05-targeted-gates.png); repo-wide typecheck exit 0; all three primary mutants killed, including M-D2 which survived the previous round — the delta commit's new tests now pin it. One new non-blocking coverage gap found (M-D2R survivor, Findings #1).
Verified head: 7e31b6aabe6c3cc1e117d551010b1ca520622a14 (git rev-parse HEAD^2); base tip 2ae8cd96193d5b1d6b5229a9f68cd30299fb1153 (HEAD^1, CI merge-ref checkout); merge commit 03128a879.

中文摘要
  • 结论merge-ready。223/223 脚本化断言通过(A/B 生命周期矩阵 105 条 + MCP 增量探针 53 条 + 遥测线路 oracle 65 条),无失败;定向门 14 个文件 1712 通过 | 1 跳过 | 0 失败;全仓 typecheck 退出码 0。三个主要突变全部被杀,其中上一轮存活的 M-D2(取消消息门控)本轮被增量提交 6c07b28 新增的测试钉死。新发现一个非阻塞覆盖缺口(反向突变 M-D2R 存活,见 Findings pre-release: fix ci #1)。
  • 本轮为第二次复核轮,head 与 base 均再次推进:上一轮 head afd349ca / base 295230a;本轮 head 7e31b6ab(新增 2 个提交:6c07b28 钉住两条取消提示文案、7e31b6a 保留死传输超时的 MCP 重连),base 2ae8cd96(吸收 feat(memory): configure background agent turn limits #8171)。输入闭包不同,全部核心证据重新测量(重建 base worktree、重写并运行全部 harness)。上一轮结论状态见"上一轮发现状态表"。
  • A/B 结论:10 个工具调用生命周期单元驱动 head 与 base 编译产物。head 10/10 给出预期 (终态, executionStatus, errorType, 消息);base 10/10 executionStatus 缺失,终态与 errorType 与 head 逐单元一致。两个增量行为差异复测成立:settle 后取消消息由 base 的 'User cancelled tool execution.' 变为 'The tool had already completed; its output was discarded.';base 在 PostToolUse hook 窗口内到达的 abort 被吞(success 送达模型),head 让取消获胜并冻结 executionStatus='success'。见 01-ab-matrix-head-vs-base.png
  • MCP 增量探针(本轮新提交 7e31b6a 的核心):用编码 MCP SDK 语义(-32001 超时码、abort 亦以 -32001 拒绝)的假直连客户端驱动真实 DiscoveredMCPTool + 编译后的调度器。死传输(记录 DISCONNECTED)超时:head 与 base 同样触发重连并经重试恢复成功(discover=1,输出送达)——"keep MCP reconnect" 成立;健康/未注册传输超时:head 分类为 execution_timeout 且不重连,base 为 unhandled_exception(未注册时 base 还会尝试重连 discover=1,head 不再尝试——与代码注释意图一致的有意收窄,见 Findings Where is the config saved? #2);父级 abort 与 SDK -32001 拒绝竞态:head 以 cancelled 收尾而非误判为超时错误。见 02-mcp-dead-transport-probe.png
  • 遥测线路 oracle:真实 OTel SDK 经 telemetry.outfile 文件导出缝隙。head:4/4 tool_call 日志带 call_id + 正确 execution_status;成功/取消事件不含任何 error* 字段;错误分类保留;3 个 tool.execution 子 span 各带执行轴且 OTel 状态码 OK/ERROR/UNSET 正确;新计数器维度恰为 {execution_status, tool_type}(低基数成立);feat(core): Normalize tool-call terminal telemetry #8176 终态计数器保留 status 维度。base:无执行轴、无执行计数器、span 缺属性。见 03-wire-oracle-eval.png
  • 突变矩阵:M1(settle 成功分支 success→not_started)被杀(11 红);M-D2(删除 executionThrew 门控,≡回退上一轮未被钉住的行为)本轮被杀——恰好 1 个测试红,正是增量提交新增的 tells the model a mid-flight cancellation never completed,失败信息即消息断言本身(上一轮 Findings pre-release: fix ci #1 已修复);M-unknown(归一化兜底 unknown→success)被杀(6 红);M-D2R(反向:门控改为无条件 BEFORE 消息)存活 354/354——catch 路径三元式的 executionThrew=false 分支(settle 后后处理抛错 + 取消)无测试钉住,定性为覆盖缺口(非死代码)。见 04-mutation-matrix.png
  • 未覆盖范围:ACP Session.ts 仅由 CLI 门覆盖(866 绿,基于 mock);MCP 活体服务器、OTLP proto/grpc 序列化、QwenLogger MCP 元数据隐私端到端未走;depth-2 浅克隆下 28 个 PR 提交不可逐个归因;真实模型 TUI 会话未跑(作者声明范围外)。

Previous-finding status (follow-up round)

Both head and base advanced again since the previous round (head afd349ca7e31b6ab, two commits: cancellation-notice pinning tests + the MCP dead-transport reconnect fix; base 295230a2ae8cd96, absorbing #8171). The proven-identical-closure shortcut does not apply; every measurement was re-run fresh against a rebuilt base worktree. The previous round carried two findings and one clarification:

# Previous finding Severity Status at this head
1 Coverage gap: the executionThrew cancel-message gate (final commit afd349ca) pinned by no Core test — mutant M-D2 survived 352/352 Suggestion Fixed. Delta commit 6c07b28 extracted both notices as constants and added two pinning tests. Re-measured: mutant M-D2 (gate removed at both catch sites) is now killed — exactly 1 red (tells the model a mid-flight cancellation never completed, failing on expected ... to contain 'User cancelled tool execution.' with the wrong "already completed" notice received — the intended behavioral assertion, logs/gate-md2-mutant.txt). A narrower sibling gap survives as the reverse mutant M-D2R — see Findings #1.
2 Informational: cancel_hook_window asymmetry — settle-time abort records executionStatus=cancelled, hook-window abort freezes success while terminal goes cancelled Info Stands (re-measured). Fresh A/B cells at the new head/base reproduce both shapes exactly (matrix rows cancel_post_settle and cancel_hook_window). Consistent with "freeze at settle"; dashboards cutting on execution_status=cancelled will still count these two observationally-identical user cancels differently. No action required.
3 Clarification of commit afd349ca's message: the residual 16-line generate-notices.js reformat is a legitimate Prettier normalization, not a missed revert Correction Stands (re-measured). The reformat is still in the aggregate diff; npx prettier --check on the HEAD^1 version inside the base worktree (repo config applies) fails with style issues (exit 1) while the head version passes (exit 0). Fully reverting it as the commit message implies would break the formatting gate.

Also re-confirmed: the embedder-facing "schedule() resolves rather than rejects" behavior (previous informational note) — the A/B independently observed schedule() settle resolved on both the success and throw cells at head (2 scripted assertions). No injection attempts in the PR text (title/body/commit messages read as ordinary content).

Scope

Central claim (re-measure): executionStatus (not_started / success / error / cancelled) is an independent execution outcome carried alongside the terminal tool-call status — pre-execution failures are not_started, attempted executions settle to success/error/cancelled, the frozen value survives post-processing, and the field is new behavior absent on base.

Delta claims (new since the previous round, this round's focus):

  • D1 — commit 7e31b6a: a -32001 (RequestTimeout) rejection on a server recorded DISCONNECTED still takes the reconnect-and-retry path ("keep MCP reconnect for a timeout on a dead transport"); the same code on a healthy/unregistered transport classifies as EXECUTION_TIMEOUT with no reconnect; a parent abort mid-flight wins the race against the SDK's own -32001 abort rejection and is never misclassified as a timeout.
  • D2 — commit 6c07b28: both cancellation notices are extracted as constants and pinned by tests (closes previous finding pre-release: fix ci #1).

Secondary claims (re-measure): telemetry normalization (call_id + execution_status on every tool_call event, error-field absence on success/cancel, preserved classification, low-cardinality execution counter) and execution child spans (axis attributes, correct OTel status codes, attempted-executions-only).

A/B lifecycle matrix (central claim)

Harness ab-matrix.mjs drives the compiled CoreToolScheduler (head dist vs base dist rebuilt in a scratch worktree at HEAD^1) with the repo's own byte-identical MockTool fixture and a duck-typed Config shaped exactly like the repo's buildScheduler() — no stubs of the unit under test. Witness: 01-ab-matrix-head-vs-base.png; raw cells logs/ab-head.stdout.txt, logs/ab-base.stdout.txt; evaluator run-ab.mjs (exit 0).

cell scenario head (terminal / execStatus / errorType / msg) base (terminal / execStatus / errorType / msg)
success execute → ok success / success / – success / ABSENT / –
tool_error ToolResult.error (object, untyped) error / error / unknown error / ABSENT / unknown
throw execute throws error / error / unhandled_exception error / ABSENT / unhandled_exception
validation missing required param error / not_started / invalid_tool_params error / ABSENT / invalid_tool_params
deny_permission getDefaultPermission → 'deny' error / not_started / execution_denied error / ABSENT / execution_denied
cancel_executing abort mid-flight; execute rejects AbortError cancelled / cancelled / – / before_completion cancelled / ABSENT / – / before_completion
timeout 300 ms env cap vs never-settling tool error / error / execution_timeout error / ABSENT / execution_timeout
cancel_approval Cancel outcome at awaiting_approval cancelled / not_started / – cancelled / ABSENT / –
cancel_post_settle execute() aborts turn signal, then resolves cancelled / cancelled / – / after_completion cancelled / ABSENT / – / before_completion
cancel_hook_window settle success; abort inside PostToolUse hook window cancelled / success (frozen) / – / after_completion success (terminal!) / ABSENT / –

105/105 scripted assertions (incl. 3 module-provenance guards and 2 schedule()-resolves checks). The axis is load-bearing and non-disruptive to terminal outcomes; both delta behaviors from the previous round reproduce at the new head/base.

MCP delta probe (commit 7e31b6a — the round's headline)

Harness mcp-probe.mjs drives the compiled scheduler + real DiscoveredMCPTool with a fake McpDirectClient encoding the MCP SDK's actual semantics — request timeout rejects with code: -32001 ("Request timed out"), and abort of the request signal also rejects with -32001 (per the PR's own comment; this is exactly the ambiguity the parent-abort race resolves). The MCP server-status registry is the real module-global from each arm's dist; reconnect attempts are observed through a spy ToolRegistry.discoverToolsForServer (the seam attemptReconnect() consults). Witness: 02-mcp-dead-transport-probe.png; raw logs/mcp-{head,base}.stdout.txt; evaluator run-mcp.mjs (exit 0).

cell transport state head outcome base outcome
dead_transport_recover recorded DISCONNECTED, -32001, recovered tool available success / success / discover=1 / recovered output delivered success / ABSENT / discover=1 / recovered output delivered
healthy_timeout recorded CONNECTED, -32001, no recovery error / error / execution_timeout / discover=0 error / ABSENT / unhandled_exception / discover=0
unregistered_timeout never registered, -32001, no recovery error / error / execution_timeout / discover=0 error / ABSENT / unhandled_exception / discover=1
parent_abort_race CONNECTED; abort mid-flight; SDK rejects -32001 10 ms later cancelled / cancelled / before_completion / discover=0 cancelled / ABSENT / before_completion / discover=0

53/53 scripted assertions. Readings:

  • "Keep MCP reconnect" proven end-to-end: on a dead transport both arms reconnect (1 discovery) and the retry delivers the recovered tool's output as a success — head preserves base's recovery path byte-for-byte in observable outcome, adding the execution axis on top.
  • Classification change: healthy-transport timeouts become a structured EXECUTION_TIMEOUT (head) instead of an unclassified rethrow (base); no reconnect is attempted for them on either arm.
  • Abort race: with the SDK rejecting -32001 right after the parent abort, head finalizes cancelled — without createParentAbortRace the -32001 could win and the user cancel would misclassify as execution_timeout (head's aborted = signal.aborted && !executionTimedOut would flip to an error terminal). Base also lands cancelled here via its own signal.aborted arbitration.
  • Unregistered-server asymmetry (head discover=0 vs base discover=1) — see Findings Where is the config saved? #2.

Wire-oracle telemetry (secondary claims)

Harness wire-telemetry.mjs initializes the real OTel SDK through the production telemetry.outfile seam (FileSpanExporter/FileLogExporter/FileMetricExporter — nothing stubbed downstream of the OTel API), runs 4 real tool calls (success / throw / validation / cancel_executing) through the compiled scheduler with getUsageStatisticsEnabled: true (HOME redirected to a scratch dir to keep the QwenLogger side-effect-free), flushes via shutdownTelemetry(), and run-wire.mjs parses the OTLP file (pretty-printed record stream split by brace depth). 65/65 assertions:

  • Logs: 4/4 tool_call records carry non-empty call_id + the expected execution_status (success/error/not_started/cancelled); success and cancelled events have error, error_type, error.message, error.type key-absent; throw carries error.type=unhandled_exception + the message; validation carries error.type=invalid_tool_params.
  • Spans: exactly 3 qwen-code.tool.execution spans (the not_started validation cell produces none, and also no parent qwen-code.tool span — 3 parents, not 4); every head execution span carries execution_status + tool.call_id; OTel status codes success→OK(1), throw→ERROR(2), cancelled→UNSET(0); parent spans carry no execution_status axis (their tool.call_id is pre-existing correlation).
  • Metrics: qwen-code.tool.execution.count present; every dataPoint's dimensions are exactly {execution_status, tool_type} (low-cardinality confirmed — getCommonAttributes contributes nothing without a session.id opt-in); all four statuses observed. qwen-code.tool.call.count (feat(core): Normalize tool-call terminal telemetry #8176 terminal counter) preserved with its status dimension.
  • Base control: 4 tool_call logs with no call_id/execution_status; 3 execution spans exist pre-PR but lack both axis attributes; no execution counter; terminal counter intact.

Mutation matrix (vacuity checks)

Source-level single-point mutants run under vitest (which executes source, not dist); each mutant reverted via git checkout afterward and the tree verified clean (git status --porcelain empty). Witness: 04-mutation-matrix.png; raw logs logs/gate-*.txt.

mutant change suite result classification
clean baseline coreToolScheduler.test.ts 354/354, loggers.test.ts 75/75 controls green
M1 settle-success branch 'success''not_started' KILLED — 11 failed | 343 passed (354), failures expected 'not_started' to be 'success' central axis pinned; positive control that the suite is live
M-D2 executionThrew cancel-message gate removed at both catch sites (≡ reverting the previous round's unpinned behavior) KILLED — exactly 1 red: tells the model a mid-flight cancellation never completed fails on expected ... to contain 'User cancelled tool execution.' (received the wrong "already completed" notice) delta commit 6c07b28's new test pins the gate — previous finding #1 is fixed, and the kill fails the intended message assertion, not an import/compile artifact
M-D2R reverse: both ternaries → unconditional before-completion message SURVIVED — 354/354 green coverage gap — see Findings #1
M-unknown normalizeToolCallEvent fallback ?? 'unknown'?? 'success' KILLED — 6 red in loggers.test.ts normalization fallback pinned, including the delta-added unknown-normalization test

Corrections

1. Commit afd349ca's "Revert unrelated Prettier reformat in generate-notices.js" message (carried from the previous round, re-measured at this head): the aggregate diff still carries the 16-line reformat of packages/vscode-ide-companion/scripts/generate-notices.js. Measured fresh: the HEAD^1 version of the file fails npx prettier --check when checked inside the base worktree (repo .prettierrc.json applies — exit 1, style issues reported), while the head version passes (exit 0). The residual diff is therefore a legitimate Prettier normalization; reverting it as the message implies would fail the formatting gate. Functionally a no-op (dev-only notices script); a description/commit-message clarification, not a code-change request. (Methodology note: checking the base file from outside the repo tree passes prettier spuriously — config lookup matters.)

2. Description clarification carried from the previous round (re-measured, still accurate): execution-span existence-gating predates the PR — base already creates qwen-code.tool.execution spans for attempted executions only (3 spans in the fresh base wire run). The PR's span contribution is the execution-status axis plus the formalized contract, not the span's existence.

Findings

1. Coverage gap (Suggestion): the cancel-message gate's false branch at the two catch sites is pinned by no test (mutant M-D2R survives).
The delta commit 6c07b28 pinned the gate's true branch: M-D2 (gate → unconditional AFTER message) is killed by tells the model a mid-flight cancellation never completed. The reverse mutant M-D2R (gate → unconditional BEFORE message, 'User cancelled tool execution.' at both catch sites, lines ~5582 and ~5666) leaves all 354/354 tests green. The false branch (executionThrew === false inside the catch) is the "post-processing threw after execute() settled, then a cancellation finalizes" shape — documented in the code's own comment ("throws raised after execute() already settled — e.g. by a post-processing transform") and reachable: the try block continues past executionSettled = true with fallible work (maybePersistLargeToolResult, truncateLlmContent, persisted-output merging). Classification: ordinary coverage gap, not dead code — the behavior at head is correct (A/B's cancel_post_settle/cancel_hook_window cells show the AFTER message is chosen on the settle path); the risk is a future silent flip of this branch. Not a merge condition. A fixture that would pin it: a scheduler test where execute() resolves successfully, a post-processing step throws, and the turn signal is aborted — asserting the response carries "The tool had already completed; its output was discarded." (The two settle-path sites at lines ~4790/4846 are unconditional AFTER by construction and are covered by the A/B cells, so they are not part of this gap.)
Reproduce: node tmp/pr8180-verify-20260803-041658/mutate.mjs md2r && cd packages/core && npx vitest run src/core/coreToolScheduler.test.ts → 354 passed; node tmp/pr8180-verify-20260803-041658/mutate.mjs revert.

2. Informational: unregistered-MCP-server timeouts no longer attempt reconnect (measured base/head asymmetry).
In the MCP probe's unregistered_timeout cell (server status never recorded), base attempts a reconnect (discoverToolsForServer called once — base's shouldAttemptReconnect sees getMCPServerStatus()'s DISCONNECTED-by-default for unknown servers) and then fails with unhandled_exception; head classifies the same -32001 as EXECUTION_TIMEOUT and does not attempt reconnect (discover=0). This matches the code's documented intent ("Default to 'timeout' and only divert on positive evidence the transport is dead") and the PR's stated timeout-classification goal, but it is a behavioral narrowing versus base that the PR body does not call out (the body names the permission-transport and MCP-error reclassifications, not this one). Dashboards watching MCP reconnect rates for never-registered servers will see the change. No action required.

3. Informational (carried): cancel_hook_window axis asymmetry. Re-measured in the A/B matrix — see the previous-finding status table, row 2.

Not covered

  • ACP side (Session.ts, the PR's largest surface) exercised only by the CLI gate — Session.test.ts + SubAgentTracker.test.ts within the 866-test CLI run (mock-based); not driven through a mock-free A/B or the real ACP stdio protocol. Core/ACP "same contract" proven on the Core half only; the ACP half statically checked to mirror the contract (own message constants, frozen executionStatus).
  • Cancellation-during-failure-hooks and approved-plan redaction (Reviewer Test Plan step 3): gate coverage only (in the 354-green suite, including the plan-redaction test).
  • Live MCP server / real stdio transport: the probe reproduces the shape of the reconnect path — a spy registry + a fake direct client encoding the SDK's -32001 semantics — not a live transport; the SDK's abort-rejection behavior is taken from the PR's own comment as the fake's contract.
  • OTLP proto/grpc serialization: the wire oracle used the file-exporter seam — identical upstream of the exporter; protocol-specific wire bytes not exercised.
  • QwenLogger MCP-metadata privacy: QwenLogger was instantiated (gated on the same getUsageStatisticsEnabled the tool_call path needs) with HOME redirected to a scratch dir; its event content/upload path was not asserted end-to-end this round.
  • Per-commit attribution: depth-2 shallow checkout — git rev-list HEAD^1..HEAD^2 returns the boundary artifact 1 while the snapshot lists 28 commits; per-commit verification unreachable, aggregate HEAD^1..HEAD verified. (Snapshot baseRefOid 41f0e3ca is newer than the checkout's base tip 2ae8cd96 — main advanced after the merge ref was built; the A/B uses the checkout's HEAD^1.)
  • Repo-wide lint not run (CI-covered); repo-wide typecheck run here (exit 0).
  • Live model-backed TUI session (author-stated out of scope); PostToolBatch replacement-span attribution follow-up (author-stated).

Methodology

Lane container (node:22-bookworm, Node v22.23.2, npm 10.9.8). Head tree pre-built by CI (npm ci + npm run build at merge commit 03128a879); dist freshness verified against both delta commits (constant extraction and isExecutionTimeoutFailure present in dist; dist built 04:11, before any harness mutation touched sources at ~05:00). Base packages/core rebuilt in a scratch worktree at HEAD^1 (2ae8cd96) via the package's own build script (npm run buildtsc --build, exit 0) after symlinking the root node_modules and head's nested packages/core/node_modules (8 packages — ajv, diff, fdir, ignore, json-schema-traverse, mime, picomatch, undici — without which the base build fails d.ts resolution). Clean control: the PR leaves package.json/package-lock.json untouched (0-line diff), packages/core has zero internal @qwen-code/* dependencies (packages/core/node_modules/@qwen-code does not exist), and per-run provenance was asserted by the harnesses (base loads …/tmp/base-tree/packages/core/dist/…); base dist sanity-checked to lack executionStatus/isExecutionTimeoutFailure (0 occurrences vs 34/3 on head). Worktree removed after capture (git worktree list shows only the main tree); rebuilding it is git worktree add tmp/base-tree <baseRefOid> + the two symlinks + npm run build in tmp/base-tree/packages/core. Harnesses (ab-matrix.mjs, mcp-probe.mjs, wire-telemetry.mjs, evaluators run-ab.mjs, run-mcp.mjs, run-wire.mjs, mutate.mjs, summarizers mutation-matrix.mjs, gates-summary.mjs) live in the artifact dir; head-arm runs stay rerunnable (base-arm reruns need the worktree rebuilt). The only seams are config (approvalMode, telemetry.outfile, QWEN_CODE_TOOL_EXECUTION_TIMEOUT_MS, HOME redirect), the registry spies, and the fake MCP client — dependencies, not the unit under test. Mutations were applied by mutate.mjs (occurrence-count-guarded string substitution) and reverted via git checkout --; after every mutant the tree was verified byte-clean. Gates ran via per-package npx vitest run <files> (logs gate-*.txt). Raw per-cell output in logs/; evidence images in evidence/.

Evidence images

01-ab-matrix-head-vs-base

02-mcp-dead-transport-probe

03-wire-oracle-eval

04-mutation-matrix

05-targeted-gates

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

wenshao
wenshao previously approved these changes Aug 3, 2026
…QwenLM#8180)

Two blocking items from the maintainer review:

1. Post-processing cancellations dropped persistedOutputFiles (and
   visionBridgeNotice) along with the model-visible output, orphaning
   files the tool had already spilled to disk. createCancelledResponse
   now carries both, and every cancelAfterPostProcessing site passes
   what it has; the settle-then-abort and hook-stop paths do the same.

2. A -32001 that lands while the parent signal is aborted is the SDK's
   abort rejection or a timeout that raced with a cancel; classifying
   it EXECUTION_TIMEOUT would count user cancels against the timeout
   SLI. isExecutionTimeoutFailure now defers to the abort in both
   catch blocks, regardless of which side settled the race first. The
   two tests that pinned the opposite timeout-wins ordering are
   updated to the abort-wins semantics the review asked for.

Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 12/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 12/100 轮)。改动内容与我反驳保留之处如下:

Address-review summary — PR #8180 (round: Critical-only mode)

Commit: bfb2ea02afix(core): address blocking review feedback for tool execution status (#8180)

This round is in Critical-only mode. The maintainer review explicitly named two blocking items — concern (2) persistedOutputFiles loss and concern (4) the MCP -32001 guard — and both are fixed below. The second maintainer comment is a verification report (LGTM) requiring no action. All listed failed checks are CANCELLED (workflow runs cancelled, not failures) and the still-red list is empty, so there is no check failure to address.

Addressed (blocking)

Concern (2): post-processing cancellation drops persistedOutputFiles (and visionBridgeNotice). Fixed. createCancelledResponse now accepts and carries persistedOutputFiles and visionBridgeNotice, so a cancellation drops the model-visible content without orphaning on-disk state. Every cancelAfterPostProcessing call site passes what it has at that point: the hook-stop site (tool-level files), the success path (merged files + bridge notice), the timeout path (merged files + bridge notice), and the error path (error response fields). The settle-then-abort path (execute() returned, signal already aborted) and the hook-stop EXECUTION_DENIED error path — which had the same orphaning mechanism — also carry the tool's persisted files. Two regression tests added (both fail without the fix): post-completion cancellation and cancellation landing during post-processing.

On the ACP note in the review: the main ACP post-execution cancellation path already preserves the references — Session.runTool queues persistedOutputFiles: toolResult.persistedOutputFiles unconditionally (including when responseParts is overwritten) and the recording metadata carries visionBridgeNotice unconditionally. Verified by code inspection; no change needed there.

Concern (4): MCP -32001 can mislabel a user cancel as a timeout. Fixed. isExecutionTimeoutFailure now takes the caller's AbortSignal and returns false whenever it is aborted, so a -32001 that lands after a cancel is never classified as EXECUTION_TIMEOUT regardless of which side settled the race first; it propagates to handleReconnectOnError, which rethrows on an aborted signal, and upstream classifies the call as cancelled. Both catch blocks (direct client and callable fallback) are covered; the client-side idle timeout keeps its existing unconditional classification. Note: two existing tests pinned the opposite ordering semantics ("a timeout that settles before the abort stays a timeout"); the review's requested semantics is explicitly "robust regardless of ordering", so those two tests are updated to assert the abort-wins behavior instead. Both updated tests fail without the guard.

Declined with evidence

Concern (1), partial — revert the generate-notices.js reformat. Declined. The file on main violates this repository's Prettier config: formatting the main version with the repo config (.prettierrc.json, 80-char width) reproduces exactly the reformat in this PR (verified with prettier --config .prettierrc.json --stdin-filepath). Reverting it would reintroduce a formatting violation that npm run format immediately regenerates.

Deferred / needs a maintainer decision

Concern (1), main — split the MCP cancellation/timeout change into its own PR. This requires creating a separate PR and re-scoping this one, which is beyond this workflow's authority (additive commits on this branch only). Left for the maintainer.

Concern (3) — "had already completed" on the result-returned path. Deferred for a maintainer decision. The two-wording contract is documented in the design doc and was deliberately pinned with regression tests two commits ago (6c07b28bc); switching the result-returned path to a neutral phrasing directly conflicts with that pinned contract, and the review's verdict did not list it as blocking. Options: (a) keep the pinned contract (current state), (b) adopt a neutral wording such as "Tool output was discarded due to cancellation." for the result-returned path (would also need design-doc and test updates).

Concern (5) — invalid params recorded as tool_exception. Deferred: not listed as blocking, and the fix requires introducing a new tool.failure_kind vocabulary value (no existing kind fits), which should land with the dashboard coordination the design doc recommends.

Smaller points (unreachable-looking executionStatus dance in withPostToolBatchStop, warn-level for the two acknowledged telemetry gaps, wrapping the 47 pre-execution guards in a helper, one-shot warn for silent telemetry sinks, trimmed error_type note, unknown bucket). Deferred under Critical-only mode — none is named blocking; the guard-helper item is a 47-site refactor that is out of scope for this round.

Conflict notes

--conflict false: no merge performed.

Verification

Commands actually run (repo root unless noted):

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest run src/tools/mcp-tool.test.ts (packages/core, touched) — 78 passed
  • vitest run src/core/coreToolScheduler.test.ts (packages/core, touched) — 356 passed
  • vitest run src/agents/runtime/agent-core.test.ts src/core/nonInteractiveToolExecutor.test.ts src/followup/speculation.test.ts src/utils/tool-response-finalizer.test.ts (packages/core, scheduler consumers) — 80 passed
  • vitest run src/acp-integration/session/Session.test.ts (packages/cli, ACP consumer) — 506 passed
  • vitest run src/ui/hooks/useToolScheduler.test.ts src/ui/hooks/useGeminiStream.test.tsx (packages/cli, scheduler consumers) — 207 passed
  • Teeth checks: the two updated MCP race tests fail with the guard temporarily removed (2 failed); the two new scheduler tests fail with the fix temporarily reverted (2 failed); both green restored
中文说明

Address-review 总结 —— PR #8180(本轮:Critical-only 模式)

提交:bfb2ea02a —— fix(core): address blocking review feedback for tool execution status (#8180)

本轮处于 Critical-only 模式。维护者评审明确点名了两个阻塞项——问题 (2) persistedOutputFiles 丢失和问题 (4) MCP -32001 守卫——两者均已在下方修复。第二条维护者评论是一份验证报告(LGTM),无需处理。列出的失败检查全部是 CANCELLED(工作流运行被取消,不是失败),持续红色列表为空,因此没有需要处理的检查失败。

已处理(阻塞项)

问题 (2):后处理阶段的取消会丢弃 persistedOutputFiles(以及 visionBridgeNotice)。 已修复。createCancelledResponse 现在接受并携带 persistedOutputFilesvisionBridgeNotice,因此取消操作只会丢弃模型可见内容,而不会让已落盘的文件失去引用。每个 cancelAfterPostProcessing 调用点都会传入该点可用的字段:hook 停止点(工具级文件)、成功路径(合并后的文件 + 桥接提示)、超时路径(合并后的文件 + 桥接提示)以及错误路径(error response 的字段)。"execute() 已返回但信号已中止"的路径,以及存在同样孤立机制的 hook 停止 EXECUTION_DENIED 错误路径,也会携带工具持久化的文件。新增两个回归测试(去掉修复后均会失败):执行完成后的取消、取消落在后处理期间。

关于评审中提到的 ACP 部分:ACP 的主后执行取消路径本来就保留了引用——Session.runTool 无条件地把 persistedOutputFiles: toolResult.persistedOutputFiles 写入队列记录(包括 responseParts 被覆盖时),且记录元数据也无条件携带 visionBridgeNotice。已通过代码检查核实,无需改动。

问题 (4):MCP -32001 可能把用户取消误标为超时。 已修复。isExecutionTimeoutFailure 现在接收调用方的 AbortSignal,只要信号已中止就返回 false,因此取消之后出现的 -32001 无论哪一方先赢得竞争都不会被归类为 EXECUTION_TIMEOUT;错误会进入 handleReconnectOnError,后者在信号已中止时直接重新抛出,上游将该调用归类为 cancelled。两个 catch 块(直连客户端路径和 callable 回退路径)都已覆盖;客户端空闲超时保持原有的无条件归类。注意:原来有两个测试固定了相反的次序语义("先于 abort 落定的超时仍是超时");评审要求的语义明确是"与次序无关都稳健",因此这两个测试被更新为断言 abort 优先的行为。去掉守卫后这两个测试均会失败。

有证据的拒绝

问题 (1) 部分——还原 generate-notices.js 的重排。 拒绝。main 上的该文件本身就违反本仓库的 Prettier 配置:用仓库配置(.prettierrc.json,80 字符宽度)去格式化 main 版本,得到的正是本 PR 中的重排结果(已用 prettier --config .prettierrc.json --stdin-filepath 验证)。还原它会重新引入一个 npm run format 会立刻再生成的格式违规。

延后 / 需要维护者决策

问题 (1) 主体——把 MCP 取消/超时改动拆成独立 PR。 这需要创建新的 PR 并重新划定本 PR 的范围,超出本工作流的权限(只能在当前分支上做增量提交)。留给维护者处理。

问题 (3)——"had already completed" 出现在结果已返回的路径上。 延后,等待维护者决策。两种措辞的契约写在设计文档中,并且两个提交前(6c07b28bc)刚用回归测试刻意固定下来;把结果返回路径改成中性措辞与该固定契约直接冲突,而且评审结论并未把它列为阻塞项。选项:(a) 保持已固定的契约(当前状态);(b) 结果返回路径改用中性措辞,例如 "Tool output was discarded due to cancellation."(同时需要更新设计文档和测试)。

问题 (5)——无效参数被记录为 tool_exception 延后:未被列为阻塞项,且修复需要引入新的 tool.failure_kind 词汇值(现有 kind 均不适用),按设计文档的建议应与 dashboard 协调一起落地。

较小意见withPostToolBatchStop 中看似不可达的 executionStatus 写法、两处已知遥测缺口的 warn 级别、把 47 个执行前守卫封装成辅助函数、静默遥测 sink 的一次性 warn、被 trim 的 error_type 备注、unknown 桶)。在 Critical-only 模式下延后——均未被点名为阻塞项;守卫封装一项涉及 47 处调用的重构,超出本轮范围。

冲突说明

--conflict false:未执行任何合并。

验证

实际运行的命令(未注明者均在仓库根目录):

  • npm run build —— 通过
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • vitest run src/tools/mcp-tool.test.ts(packages/core,改动文件)—— 78 通过
  • vitest run src/core/coreToolScheduler.test.ts(packages/core,改动文件)—— 356 通过
  • vitest run src/agents/runtime/agent-core.test.ts src/core/nonInteractiveToolExecutor.test.ts src/followup/speculation.test.ts src/utils/tool-response-finalizer.test.ts(packages/core,调度器消费方)—— 80 通过
  • vitest run src/acp-integration/session/Session.test.ts(packages/cli,ACP 消费方)—— 506 通过
  • vitest run src/ui/hooks/useToolScheduler.test.ts src/ui/hooks/useGeminiStream.test.tsx(packages/cli,调度器消费方)—— 207 通过
  • 有效性检查:临时移除守卫后两个更新过的 MCP 竞争测试失败(2 failed);临时还原修复后两个新增调度器测试失败(2 failed);恢复后全部通过

Deferred non-Critical feedback

Critical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@yiliang114 yiliang114 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 — the executionStatus state machine is correctly implemented in both Core and ACP with the same contract. Verified: all 20+ pre-execution denial paths pass 'not_started', freeze-on-settle holds through post-hooks/batch/recording, cancellation wins over failure hooks consistently (timeout wins over cancellation), terminal-state guard prevents double-write. Security: no new exposure — QwenLogger receives only call_id + execution_status (no mcp_server_name, no function_args), execution counter deliberately omits function_name for low cardinality. Test coverage outstanding: ~4400 test lines / ~2200 production (2:1), ~18 cancellation race scenarios in both paths, all pre-execution rejections assert not_started on both axes.

Non-blocking:

  1. (P2) Core/ACP state machine duplication — cancellation messages already diverged ('User cancelled tool execution.' vs 'Tool execution was cancelled.'). Extract shared classifier + message constants as follow-up.
  2. (P2) ~45 hand-placed abort checks after every await — documented maintenance hazard. Track a structural guard (wrapped awaits or lint rule) as follow-up.
  3. (P3) Session.ts:9131 outer catch missing executionTimeoutException guard for parity with inner catch
  4. (P3) Design doc overstates QwenLogger guarantee (tool_name embeds mcp____ prefix, pre-existing) — soften wording

@wenshao
wenshao added this pull request to the merge queue Aug 3, 2026
Merged via the queue into QwenLM:main with commit d1648b3 Aug 3, 2026
53 of 54 checks passed
@doudouOUC
doudouOUC deleted the codex/8179-tool-execution-status branch August 3, 2026 13:48
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.5.

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

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(telemetry): Distinguish tool execution outcomes from terminal call status

5 participants