Skip to content

fix(core): disambiguate send_message destinations - #10083

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
Sumire-no-kai:codex/issue-10073
Aug 29, 2026
Merged

fix(core): disambiguate send_message destinations#10083
wenshao merged 5 commits into
QwenLM:mainfrom
Sumire-no-kai:codex/issue-10073

Conversation

@Sumire-no-kai

Copy link
Copy Markdown
Contributor

What this PR does

This PR makes send_message destination selection unambiguous. Calls that supply both a teammate destination and a background-task destination now fail validation before either message path is invoked. When a background-task lookup misses but the supplied identifier matches a teammate in the active team roster, the error also suggests retrying with the teammate destination field.

The change preserves the existing missing-task error type and machine-readable message, and adds focused regression coverage for non-dispatch on ambiguous input, teammate-name normalization, and the unchanged generic error for unrelated task IDs.

Why it's needed

Both destination fields are optional in the tool schema, while execution previously prioritized the background-task field. If both were supplied, the teammate destination was silently ignored; if the task lookup then missed, the caller received only a background-task error even when the intended recipient was a teammate. Runtime validation closes that ambiguity without introducing the provider-incompatible top-level oneOf schema shape discussed in #7984.

Reviewer Test Plan

How to verify

  1. Invoke send_message with both to and task_id targeting valid destinations. Confirm that it returns an invalid-parameters error and that neither the teammate delivery path nor the background-task queue receives the message.
  2. Invoke it with only a nonexistent task_id whose normalized value matches a teammate name, such as QA Reviewer for qa-reviewer. Confirm that the result remains SEND_MESSAGE_NOT_FOUND and suggests to: "qa-reviewer".
  3. Invoke it with an unrelated nonexistent task_id. Confirm that it retains the generic missing-background-task error and does not add a teammate hint.

Evidence (Before & After)

Before: a call carrying both destinations queued the message to the background task and never called teammate delivery. A missing task identifier matching a teammate returned only Error: No background task found with ID "qa-reviewer".

After: the ambiguous call returns Only one of "to" or "task_id" may be provided. before dispatch. The matching missing-task case retains SEND_MESSAGE_NOT_FOUND and adds Did you mean to message teammate "qa-reviewer"? If so, use to: "qa-reviewer" instead of task_id.

Focused verification: 23/23 tests passed. Targeted ESLint and Prettier checks passed. The repository build and typecheck passed. The full preflight completed clean/install/format/lint/build/typecheck successfully, but its all-workspace test phase was not green on this Windows host: unrelated suites reported WSL /bin/bash absence, Windows symlink/path behavior differences, filesystem lock/permission errors, performance/time-limit failures, and one DWS timing/state expectation. No full-suite failure referenced the changed behavior; the focused test file also passed 23/23 inside the core full-suite run.

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ✅ focused tests, lint, formatting, build, and typecheck
🐧 Linux ⚠️ not tested

Environment (optional)

Windows PowerShell, Node.js v24.11.0, npm 11.6.1, isolated Git worktree, no sandbox runtime.

Risk & Scope

  • Main risk or tradeoff: the teammate hint is based on normalized team-roster membership; actual delivery remains subject to the existing teammate lifecycle checks.
  • Not validated / out of scope: changing teammate lifecycle behavior, background-task lifecycle behavior, provider schemas, or unrelated full-suite Windows failures.
  • Breaking changes / migration notes: calls that previously supplied both destination fields and silently selected task_id now receive an explicit validation error. No data migration or schema change is required.

Linked Issues

Fixes #10073

中文说明

本 PR 做了什么

本 PR 消除了 send_message 目的地选择的歧义。同时提供 teammate 目的地和后台任务目的地的调用,现在会在进入任一消息通道前校验失败。当后台任务查找未命中,但提供的标识符与当前团队 roster 中的 teammate 名称匹配时,错误还会提示改用 teammate 目的地字段重试。

本次修改保留了原有的任务未找到错误类型和机器可读错误消息,并增加了聚焦回归覆盖:歧义输入不得投递、teammate 名称规范化匹配,以及无关任务 ID 仍保持通用错误。

为什么需要它

工具 schema 中两个目的地字段均为可选,而原执行逻辑优先处理后台任务字段。如果两者同时提供,teammate 目的地会被静默忽略;随后如果任务查找未命中,即使调用者本意是联系 teammate,也只会收到后台任务错误。运行时校验关闭了这一歧义,同时没有引入 #7984 中提到的、与部分 provider 不兼容的顶层 oneOf schema 结构。

Reviewer 测试计划

如何验证

  1. 使用同时包含 totask_id、且两者都指向有效目的地的参数调用 send_message。确认它返回参数无效错误,并且 teammate 投递路径和后台任务队列均未收到消息。
  2. 只提供一个不存在的 task_id,但其规范化值与 teammate 名称匹配,例如用 QA Reviewer 匹配 qa-reviewer。确认结果仍为 SEND_MESSAGE_NOT_FOUND,并提示使用 to: "qa-reviewer"
  3. 使用一个与 teammate 无关且不存在的 task_id。确认它保留通用的后台任务未找到错误,并且不增加 teammate 提示。

前后证据

修复前:同时携带两个目的地的调用会把消息排入后台任务队列,且永远不会调用 teammate 投递。与 teammate 同名的缺失任务标识符只会返回 Error: No background task found with ID "qa-reviewer".

修复后:歧义调用会在投递前返回 Only one of "to" or "task_id" may be provided.。同名的任务未命中场景仍保留 SEND_MESSAGE_NOT_FOUND,并增加 Did you mean to message teammate "qa-reviewer"? If so, use to: "qa-reviewer" instead of task_id. 提示。

聚焦验证:23/23 项测试通过。目标 ESLint 和 Prettier 检查通过。仓库 build 和 typecheck 通过。完整 preflight 的 clean/install/format/lint/build/typecheck 阶段均成功,但其全工作区测试阶段在这台 Windows 主机上未全绿:无关套件报告了 WSL /bin/bash 缺失、Windows 符号链接/路径行为差异、文件锁/权限错误、性能/超时失败,以及一个 DWS 时序/状态断言失败。所有全量失败均未引用本次修改行为;目标测试文件在 core 全量测试中也为 23/23 通过。

测试平台

操作系统 状态
🍏 macOS ⚠️ 未测试
🪟 Windows ✅ 聚焦测试、lint、格式、build 和 typecheck 已验证
🐧 Linux ⚠️ 未测试

环境(可选)

Windows PowerShell、Node.js v24.11.0、npm 11.6.1、隔离 Git worktree、未使用 sandbox runtime。

风险与范围

  • 主要风险或权衡:teammate 提示基于规范化后的团队 roster 成员匹配;实际投递仍受现有 teammate 生命周期检查约束。
  • 未验证 / 范围外:更改 teammate 生命周期、后台任务生命周期、provider schema,或修复无关的 Windows 全量测试失败。
  • 破坏性变更 / 迁移说明:之前同时提供两个目的地字段并静默选择 task_id 的调用,现在会收到明确的校验错误。不需要数据迁移或 schema 变更。

关联 Issue

Fixes #10073

@Sumire-no-kai

Copy link
Copy Markdown
Contributor Author

E2E / reproduction report

Environment: Windows PowerShell, Node.js v24.11.0, isolated worktree.

Baseline reproduction

  • With a running background task and an active team, a send_message call containing both to and task_id queued the message to the background task and did not call teammate delivery.
  • A missing task_id equal to an active teammate name returned the generic SEND_MESSAGE_NOT_FOUND response without a destination correction.
  • The pre-change focused suite passed 21/21. The two added regression cases then failed before the implementation, confirming that they detected the reported behavior.

Post-fix verification

  • The both-field call now returns INVALID_TOOL_PARAMS before dispatch; the background-task queue remains empty and teammate delivery is not called.
  • A normalized teammate-name miss (QA Reviewerqa-reviewer) retains SEND_MESSAGE_NOT_FOUND and suggests using to: "qa-reviewer".
  • An unrelated missing task ID retains the generic response without a teammate hint.
  • Focused suite: 23/23 passed, including an independent post-fix reproduction run.
  • Targeted ESLint and Prettier checks passed; repository build and typecheck passed.

The all-workspace preflight test phase was not green on this Windows host because unrelated suites hit missing WSL /bin/bash, Windows symlink/path and file-lock behavior, and timing limits. The changed focused suite itself passed 23/23 both independently and during the core full-suite run.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on 3267e34 and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— 3267e34 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR! This re-run reviews the rebased head 3267e34b, which carries the change onto current main — including #10158 (cross-session messaging), which landed minutes before this push.

Template looks good ✓

Problem: observed bug — #10073 reports the exact No background task found with ID "qa-reviewer" error from a real Agent Team session, and the maintainer's round-2 base arm reproduced that very diagnostic against the pre-fix code on current main. This is not theoretical hardening.

Direction: aligned. The issue triage endorsed exactly this shape — runtime rejection of ambiguous to + task_id calls plus a team-aware hint on a task-lookup miss — and explicitly ruled out the top-level schema oneOf that providers reject (#7984). This PR stays on the blessed side of that line.

Size: core paths touched (packages/core/src/tools/**) — 28 production lines (+25/−3) and 79 test lines (+77/−2), no generated/schema files. Well under every threshold.

Approach: the scope is right. Two focused edits implementing exactly the triaged direction — no unrelated changes, no drive-by refactors. It reuses the existing findMemberByName helper and the standard validateToolParamValues hook, and the round-1 defect (the hint never reaching the model) was fixed exactly the way the maintainer suggested, by moving it into error.message.

Risk: no elevated risk signals (none of the changed files match the revert-correlated path list). One thing worth knowing for the review below: this rebase lands on top of #10158, so the Stage 2 comment pays specific attention to that interaction.

Moving on to code review. 🔍

中文说明

感谢贡献!本次 re-run 审查的是 rebase 后的 head 3267e34b——它把改动落到了当前 main 上,包括在本次推送前几分钟刚合入的 #10158(跨会话消息)。

模板完整 ✓

问题:已观测到的 bug——#10073 报告了真实 Agent Team 会话中出现的 No background task found with ID "qa-reviewer" 错误,维护者第 2 轮验证的 base 臂在修复前的当前 main 代码上原样复现了这条诊断。不是理论性加固。

方向:对齐。issue 分诊明确认可了这个形态——运行时拒绝同时携带 totask_id 的调用,并在任务查找未命中时给出团队感知提示——同时明确排除了 provider 不兼容的顶层 schema oneOf#7984)。本 PR 守住了这条线。

规模:触及核心路径(packages/core/src/tools/**)——生产代码 28 行(+25/−3),测试 79 行(+77/−2),无生成/schema 文件,远低于任何阈值。

方案:范围合理。两处聚焦的修改恰好实现分诊认可的方向——无夹带改动、无顺手重构。复用既有 findMemberByName helper 和标准 validateToolParamValues 钩子;第 1 轮发现的缺陷(提示语无法到达模型)已按维护者建议的方式修复——移入 error.message

风险:无升级风险信号(改动文件均未命中与 revert 相关的高风险路径)。一个审查时需要知道的点:本次 rebase 落在 #10158 之上,因此 Stage 2 评论专门审查了二者的交互。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal for this issue, written before reading the diff: reject both-field calls through the standard validateToolParamValues hook, and on a task-lookup miss consult the roster via findMemberByName and put the hint where the scheduler actually forwards it — error.message, since round 1 proved llmContent/returnDisplay are discarded on error results. The PR does exactly this; I found no simpler path it missed.

What I verified on this rebased head:

No blockers. Known non-blocking gaps, all in the same class of confusing diagnostic that #10073 is about, and all observable now that the hint is live: leader is never in members so it gets no hint (round 2 already carried a verified witness patch for this); the hint is one-directional — a live task id passed as to gets no task_id suggestion; and newly observable after #10158, a missing task_id that matches a peer session name gets no session hint either. Round 2 said to file these as a follow-up rather than hold the PR — I agree; the third one can join the same issue.

Testing

This comment carries: the maintainer's two verification rounds cited in this thread (round 1 macOS, round 2 Linux — wire-level A/B evidence on a patch proven identical to this head), a static review of the #10158 interaction, and the CI snapshot below. This is an unattended CI re-run, so no tmux was driven here; nothing below is the author's self-report.

Final CI results for 3267e34 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

CI is running on the rebased head right now — the first execution of this commit. The earlier red on Test (ubuntu-latest) was stale-base noise (check:tui-dep-direction script missing at this PR's old merge-base; round 2 proved merging main resolves it), and the rebase removes the cause. There are no failures to quote at this moment; the platform legs beyond ubuntu haven't started yet.

The central behavioural claim is already substantiated beyond what CI alone could give — round 2 ran this exact patch through a real Agent Team and checked delivery on the wire. What remains unsettled is only the same patch on the #10158 base, which the running suite exercises at the test level. Sandboxed verification would settle it end-to-end: @qwen-code /verify — that the rebased head still rejects ambiguous destinations before dispatch and surfaces the hint on the wire, on the #10158 base. The author lacks write access, so this would be a sponsored run: a maintainer's comment approves the head it was written against, and that run carries a pre-execution risk screen and a full workspace wipe — still, read the resulting report with the same skepticism as the fork's own CI logs. Given the proven line-identity with the round-2 patch, green CI on this head may well be sufficient; the lane is named for completeness.

中文说明

代码审查

我在看 diff 之前的独立提案:用标准 validateToolParamValues 钩子拒绝双字段调用;任务查找未命中时用 findMemberByName 查询名册,并把提示语放在调度器真正转发的位置——error.message(第 1 轮已证明带 error 的结果会丢弃 llmContent/returnDisplay)。PR 的做法与此完全一致,我没有找到被它遗漏的更简路径。

在这个 rebase 后的 head 上核实的内容:

无阻塞项。已知的非阻塞缺口,均属 #10073 所针对的同类误导性诊断,且提示语可达后均可观测:leader 从不在 members 中、不会得到提示(第 2 轮已附验证过的 witness 补丁);提示是单向的——把存活任务 id 当 to 传不会得到改用 task_id 的建议;#10158 之后新增一类——未命中的 task_id 若撞上 peer 会话名也不会有会话提示。第 2 轮建议把这些作为后续 issue 而非卡住本 PR——同意,第三类可以并入同一个 issue。

测试

本评论携带的证据:本线程中维护者的两轮验证(第 1 轮 macOS、第 2 轮 Linux——在与本 head 证明逐行一致的补丁上做的链路级 A/B)、针对 #10158 交互的静态审查,以及下方 CI 快照。这是无人值守的 CI re-run,未在此驱动 tmux;以下不含任何作者自述结果。

CI 正在 rebase 后的 head 上运行——这是该提交的首次执行。此前 Test (ubuntu-latest) 的变红是基线过旧噪音(check:tui-dep-direction 脚本存在于 main 而不在本 PR 旧 merge-base 上;第 2 轮已证明合入 main 即可解决),rebase 已消除该原因。此刻没有可引用的失败;ubuntu 之外的平台测试尚未开始。

核心行为声明已得到超出 CI 本身的证实——第 2 轮用这个完全相同的补丁驱动了真实 Agent Team 并在链路上核对了投递。尚未落定的只剩同一补丁在 #10158 基线上的表现,正在运行的套件会在测试层面覆盖它。沙盒验证可以端到端落定:@qwen-code /verify——验证 rebase 后的 head 在 #10158 基线上仍在分发前拒绝歧义调用、并在链路上呈现提示语。作者没有写权限,因此这将是受赞助运行:维护者的评论批准其写就时的 head,该运行带有执行前风险筛查和完整工作区清理——即便如此,也请以审视 fork 自身 CI 日志的同等怀疑态度阅读其报告。鉴于与第 2 轮补丁的逐行同一性已被证明,本 head 上变绿的 CI 很可能已经足够;列出此通道只为完备。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — this is the exact patch @wenshao verified end-to-end in round 2, rebased onto a moving main without a single changed line; approval is held only for CI to land green on this head.

Stepping back: my last pass deferred at 3/5 for one reason — the behavioural claim had no evidence behind it. That gap has since been closed thoroughly: round 1 found the one real defect (the hint inert because the scheduler forwards only error.message for error results), the author fixed it exactly as suggested, and round 2 re-verified the whole PR on the wire and in the TUI, with A/B non-vacuity and a 5/5 mutation kill. When the author then rebased — racing #10158 onto main by minutes — I checked the force-push line by line: the effective patch is byte-for-byte what round 2 verified, and the #10158 interaction is orthogonal (build-time guard, untouched Route 1, unchanged error forwarding). If I had to maintain this in six months I'd thank the author: it's the boring, localized version of the fix, and it survived two rounds of adversarial checking.

Reservations, none of them blocking: the three hint gaps named in Stage 2 (leader, one-directional, and the new peer-session case) should land in a follow-up issue now that the hint is live. And #10090 remains open as a competing fix for the same issue — whichever lands should close the other; on the evidence in this thread, this one is further along.

Verdict: approve. CI is still running on 3267e34b, so approval is deferred until it lands green on that commit — the finalize step will post it, pinned to the reviewed commit, and withhold it if anything lands red or the head moves.

中文说明

置信度:4/5 —— 这正是 @wenshao 第 2 轮端到端验证过的补丁,rebase 到移动中的 main 上而一行未改;暂缓批准只等该 head 上的 CI 变绿。

退后一步看:上一轮我以 3/5 暂缓,唯一原因是行为声明没有证据支撑。这个缺口此后已被彻底补上:第 1 轮找到了唯一的真实缺陷(提示语失效——调度器对带 error 的结果只转发 error.message),作者按建议原样修复,第 2 轮在链路与 TUI 上重新验证了整个 PR,并做了 A/B 非平凡性检验、5/5 变异体全部被杀。随后作者 rebase——与 #10158 合入 main 仅差几分钟——我逐行核对了 force-push:有效补丁与第 2 轮验证过的版本逐字节一致,且与 #10158 的交互是正交的(build 期守卫、Route 1 未被触碰、错误转发未变)。六个月后接手维护这样的代码只会心存感谢:这是修复的朴素、局部版本,且经受住了两轮对抗性检验。

保留意见,均不阻断:Stage 2 点名的三个提示语缺口(leader、单向、以及新增的 peer 会话场景)应落入一个后续 issue——提示语可达之后它们都是可观测的。另外 #10090 作为同一 issue 的竞争修复仍然 open——无论哪个合入都应关闭另一个;就本线程的证据而言,本 PR 走得更远。

结论:批准。CI 仍在 3267e34b 上运行,因此批准推迟到该提交变绿——finalize 环节会发布钉在受审提交上的批准,若有检查变红或 head 移动则不发。

Qwen Code · qwen3.8-max

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

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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent 5": running send-message.test.ts to confirm the suite is green — the worktree has no node_modules or built dist/ , and npm ci + npm run build exceeded the ….

Test Plan (not a blocker): 23 tests passed — this review observed 21609, 1702, 24740, 1659, 601, 4235, 630 passed.

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):"agent 5"running send-message.test.ts to confirm the suite is green — the worktree has no node_modules or built dist/ , and npm ci + npm run build exceeded the …

Test Plan(非阻断):23 tests passed — this review observed 21609, 1702, 24740, 1659, 601, 4235, 630 passed

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +238 to +241
getTeamManager: () =>
({
getTeamFile: () => ({ members: [{ name: 'qa-reviewer' }] }),
}) as ReturnType<Config['getTeamManager']>,

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] Swapping this fixture from getTeamManager: () => null to a non-null stub removed the only test coverage of the new hint code's no-team branch (teamManager ? findMemberByName(...) : undefined in send-message.ts). After the swap, no test exercises a task_id miss while no team is active — the production-common solo-session path — so a follow-up edit that drops the null guard would ship green.

A mutation probe confirmed the gap: removing the teamManager ? guard leaves all 23 existing tests green, while a null-manager {task_id: 'nope', message} call then crashes into EXECUTION_FAILED instead of returning SEND_MESSAGE_NOT_FOUND:

AssertionError: expected 'execution_failed' to be 'send_message_not_found'

Keep the stub for the hint test, but add one not-found case with a null team manager: build a config with getTeamManager: () => null, call validateBuildAndExecute({ task_id: 'nope', message: 'hello' }, ...), and expect ToolErrorType.SEND_MESSAGE_NOT_FOUND with 'No background task found' and no hint.

中文说明

将此处 fixture 从 getTeamManager: () => null 换成非空 stub,移除了新增提示代码"无团队"分支(send-message.ts 中的 teamManager ? findMemberByName(...) : undefined)的唯一测试覆盖。替换之后,没有任何测试覆盖"无活动团队时 task_id 未命中"这一场景——而这正是生产上常见的单人会话路径——因此后续若有编辑移除空值保护,测试仍会全绿通过。

变异探针确认了该缺口:移除 teamManager ? 保护后,现有 23 个测试仍全部通过,而空团队管理器下的 {task_id: 'nope', message} 调用会崩溃为 EXECUTION_FAILED,而不是返回 SEND_MESSAGE_NOT_FOUND

AssertionError: expected 'execution_failed' to be 'send_message_not_found'

建议保留该 stub 用于提示测试,但补充一个空团队管理器的未命中用例:构造 getTeamManager: () => null 的 config,调用 validateBuildAndExecute({ task_id: 'nope', message: 'hello' }, ...),断言 ToolErrorType.SEND_MESSAGE_NOT_FOUND、包含 'No background task found' 且不含提示文本。

— qwen3.8-max via Qwen Code /review (v0.22.0)

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

Reviewed — no blockers. Suggestions are inline.

Test Plan (not a blocker): 23 tests passed — this review observed 21610, 1702, 24741, 1659, 601, 4235, 630 passed.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/core/src/tools/send-message.ts:341 — [review] to / task_id mutual exclusivity not documented in the schema descriptions
  • packages/core/src/tools/send-message.ts:101 — [review] Teammate hint can fire for terminated teammates, steering into a guaranteed-failing retry
中文说明

已审查——无阻断问题。 建议见行内评论。

Test Plan(非阻断):23 tests passed — this review observed 21610, 1702, 24741, 1659, 601, 4235, 630 passed

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +113 to +115
returnDisplay: teammate
? `Task not found; use "to" for teammate "${teammate.name}".`
: 'Task not found.',

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] This new teammate-hint branch of returnDisplay has no test assertion — the one-line mutation replacing this ternary with 'Task not found.' survives the whole suite. I confirmed this with a mutation probe in a scratch tree: baseline send-message.test.ts is 24/24 green, the mutant is 24/24 green as well, and adding the assertion below against the mutant turns it red (× suggests the teammate destination for a matching task ID → expected 'Task not found.' to contain 'use "to" for teammate "qa-reviewer"'). So if a later change or revert drops the user-facing half of the hint, every test stays green while the TUI silently loses the disambiguation guidance this PR exists to surface — the user sees a bare "Task not found." with no pointer to the to: field. With the assertion added and the PR code restored the suite is 24/24 green again.

The assertion belongs in the suggests the teammate destination for a matching task ID test in send-message.test.ts:

expect(result.returnDisplay).toContain('use "to" for teammate "qa-reviewer"');
中文说明

returnDisplay 中新增的 teammate 提示分支没有任何测试断言——把这个三元表达式直接替换为 'Task not found.' 的单行突变可以在整套测试中存活。我在独立的 scratch 工作树中做了突变探测验证:基线 send-message.test.ts 为 24/24 全绿;应用该突变后同样 24/24 全绿;对突变体加入下面的断言后变红(× suggests the teammate destination for a matching task ID → expected 'Task not found.' to contain 'use "to" for teammate "qa-reviewer"')。因此,如果后续改动(或回退)删掉了提示中面向用户的这一半,所有测试仍会是绿的,而 TUI 会悄悄丢失本 PR 要呈现的歧义消除指引——用户只会看到一个没有任何 to: 字段指引的 "Task not found."。加入下面的断言并恢复 PR 代码后,重新回到 24/24 全绿。

send-message.test.tssuggests the teammate destination for a matching task ID 测试中加入该断言:

expect(result.returnDisplay).toContain('use "to" for teammate "qa-reviewer"');

— qwen3.8-max via Qwen Code /review (v0.22.0)

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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent 5": none — no check was cut short by the tool budget; the only unfinished check (running the suite) was blocked by the environment, as disclosed above..

Test Plan (not a blocker): 23 tests passed — this review observed 21610, 1702, 24740, 1659, 601, 4235, 630 passed.

Convergence: round 3 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 1 (1 new). The rate of new findings is not falling. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):"agent 5"none — no check was cut short by the tool budget; the only unfinished check (running the suite) was blocked by the environment, as disclosed above.

Test Plan(非阻断):23 tests passed — this review observed 21610, 1702, 24740, 1659, 601, 4235, 630 passed

收敛情况:第 3 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。新发现的产出速度没有下降。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +361 to +363
expect(result.returnDisplay).toContain(
'use "to" for teammate "qa-reviewer"',
);

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] This round adds returnDisplay coverage to the positive hint test, but its two negative siblings — returns error for non-existent task and returns error for non-existent task without an active team — still assert only on llmContent/error type, leaving the no-hint returnDisplay ('Task not found.') unpinned. Production builds both surfaces from the same teammate ternary in send-message.ts; if a future refactor unconditionally formats the hint into returnDisplay (the ternary condition dropped, or the hint hoisted into a shared formatter), the UI shows a teammate hint precisely when no teammate matches or no team exists — the misleading display this PR exists to prevent — and the whole suite stays green because only the positive case inspects returnDisplay. I confirmed this with a mutation probe in a scratch tree at this commit: dropping the ternary so returnDisplay always emits the hint leaves all 24 tests green (the mutation survives); adding the assertions suggested below makes exactly the two negative tests flip red; with the mutation reverted and the assertions kept the suite is green again, so they pin real behaviour without false-failing.

Consider adding display assertions to both no-hint tests, e.g.:

expect(result.returnDisplay).toContain('Task not found.');
expect(result.returnDisplay).not.toContain('use "to"');

Fix witness: mutate the not-found branch in send-message.ts to always emit the hint-formatted returnDisplay — with the added assertions the two negative tests must go red (without them that mutation survives today).

中文说明

本轮为正向提示测试新增了 returnDisplay 断言,但它的两个负向兄弟测试(returns error for non-existent taskreturns error for non-existent task without an active team)仍然只断言 llmContent/错误类型,未对无提示时的 returnDisplay'Task not found.')形成约束。生产代码中两个界面来自 send-message.ts 里同一个 teammate 三元表达式;如果未来重构把提示无条件地写入 returnDisplay(去掉三元条件,或把提示移入共享的格式化逻辑),那么在没有匹配 teammate 或没有团队时界面也会显示 teammate 提示——这正是本 PR 要防止的误导性显示——而整个测试套件仍会全绿,因为只有正向用例检查了 returnDisplay。我在本提交的临时工作树中用变异探针确认了这一点:去掉三元表达式使 returnDisplay 始终输出提示时,24 个测试仍全部通过(变异存活);加上下方建议的断言后,恰好那两个负向测试变红;还原变异并保留断言后套件再次全绿,说明断言约束的是真实行为、不会误报。

建议在两个无提示测试中各加一条显示断言,例如:

expect(result.returnDisplay).toContain('Task not found.');
expect(result.returnDisplay).not.toContain('use "to"');

修复见证:将 send-message.ts 的 not-found 分支变异为始终输出带提示的 returnDisplay——加上断言后那两个负向测试必须变红(目前不加断言时该变异可以存活)。

— qwen3.8-max via Qwen Code /review (v0.22.2)

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

No issues found. LGTM! ✅

Test Plan (not a blocker): 23 tests passed — this review observed 21610, 1702, 24736, 1659, 601, 4235, 630 passed.

中文说明

未发现问题。LGTM!✅

Test Plan(非阻断):23 tests passed — this review observed 21610, 1702, 24736, 1659, 601, 4235, 630 passed

— qwen3.8-max via Qwen Code /review (v0.22.2)

@Sumire-no-kai
Sumire-no-kai deleted the codex/issue-10073 branch August 27, 2026 14:08
@Sumire-no-kai
Sumire-no-kai restored the codex/issue-10073 branch August 27, 2026 14:09
@Sumire-no-kai Sumire-no-kai reopened this Aug 27, 2026

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

Reviewed — no blockers.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • Teammate hint can fire for terminated teammates (send-message.ts:101) — already reported (round 2 deferral, review 5029062877)

Test Plan (not a blocker): 23 tests passed — this review observed 21610, 1702, 24740, 1659, 601, 4235, 630 passed.

Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:

  • packages/core/src/tools/send-message.ts:102 — [probe] disambiguation hint can never fire for leader (TeamFile.members excludes the leader)
  • packages/core/src/tools/send-message.ts:108 — [probe] corrective hint is one-directional; mirror confusion (background-task ID passed as to ) gets no task_id hint
中文说明

已审查——无阻断问题。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

Test Plan(非阻断):23 tests passed — this review observed 21610, 1702, 24740, 1659, 601, 4235, 630 passed

收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.2)

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — local real-stack run on macOS

I built and ran this PR end-to-end locally to supply the evidence the triage bot flagged as missing (CI never executed on this commit, and the PR self-reports macOS as untested).

Tree under test: origin/main bfda082 + PR head 99e47d5, merged locally — clean ort merge, effective diff unchanged at 97+/4-.
Host: macOS 26.6.2 (arm64), Node v24.18.1, npm 11.16.0.
Method: everything below goes through the bundled CLI (npm run build && npm run bundledist/cli.js), an isolated QWEN_HOME, QWEN_CODE_ENABLE_AGENT_TEAM=1, and a scripted fake OpenAI provider that drives a real Agent Team: team_create → named teammate qa-reviewer via agent({name}) → a real background task → 10 send_message probes. Assertions are made on the role: "tool" frames the model actually received, captured off the wire.

Three arms, identical trees except one file:

arm bundle
base merged tree with send-message.ts reverted to origin/main (production hunk only)
head merged tree as-is
witness head + one line (see Finding 2)

Harness, raw ledgers and full-size images: https://github.com/wenshao/qwen-code/tree/assets-pr10083


Verdict

claim result
1 ambiguous to + task_id is rejected before either dispatch path confirmed in production
2 a missing task_id matching a teammate suggests the to destination inert in production — never reaches model or user
3 single-destination calls unchanged ✅ no regression across 10 probes
4 packages/core suite / typecheck / lint / prettier on macOS ✅ all green

Finding 1 — the disambiguation half works, and #10073 reproduces on current main

Base reproduces the reported bug exactly: the call carries to: "qa-reviewer" and task_id, and the tool reports success while routing the message to the background task. The teammate's session never receives it.

base (origin/main) — silent misroute, green check:

base TUI

head (this PR) — rejected in build(), before the registry lookup and before either dispatch path:

head TUI

Dispatch was checked on the wire, not just in the message text: the ambiguous sentinel appears in 0 non-leader model requests in both arms, while the control probe send_message { to: "qa-reviewer" } (no task_id) appears in 1 — so the teammate path is genuinely live, and base genuinely dropped to. Across all 10 probes, base and head differ on exactly one line — this one.

Finding 2 — the teammate hint is unreachable in the shipped code path

This is the part CI could never have caught, because the PR's tests exercise a method with no production caller.

wire A/B

For send_message { task_id: "QA Reviewer" } against a roster holding qa-reviewer, base and head produce a byte-identical result — Task not found: QA Reviewer — on the wire and in the TUI. The hint never appears.

Mechanism, in packages/core/src/core/coreToolScheduler.ts:

  • when a ToolResult carries error, the model-visible payload is built by createErrorResponse (L928) as response: { error: error.message } (L943) — llmContent is discarded;
  • the user-visible payload is resultDisplay: resultDisplay ?? error.message (L947), and the error branch passes undefined whenever returnDisplay is a string (L5744) — returnDisplay is discarded too.

The PR appends the hint to llmContent and returnDisplay; the not-found result carries error. Both halves are dropped.

The tests pass because BaseDeclarativeTool.validateBuildAndExecute returns the tool's own ToolResult with llmContent intact — and that method has no production call site (grep -rn 'validateBuildAndExecute' packages --include='*.ts' | grep -v '\.test\.ts' returns only its own definition). So expect(result.llmContent).toContain(...) is green while the string does not exist on any real path.

Constructive proof, and the fix. A one-line witness build — the same hint appended to error.message instead — makes it appear on the wire immediately:

Task not found: QA Reviewer. Did you mean to message teammate "qa-reviewer"?
If so, use `to: "qa-reviewer"` instead of `task_id`.
           error: {
-            message: `Task not found: ${this.params.task_id}`,
+            message: `Task not found: ${this.params.task_id}.${teammateHint}`,
             type: ToolErrorType.SEND_MESSAGE_NOT_FOUND,
           },

main already follows this convention — the broadcast-failure change (#10081) puts the same string in both llmContent and error.message. Note this also moots the two deferred review findings about the hint (leader not in members, one-directional hint): I probed both live (task_id: "leader", and a background-task id passed as to) and neither produces a hint — but neither would today regardless, since no hint is reachable.

Finding 3 — no regression on the single-destination paths

Verified live in head: to-only delivers (Message sent to "qa-reviewer"., sentinel observed in the teammate's own model request), task_id-only still queues (Message queued for delivery to background task ...), the generic missing-task error is unchanged, and a task_id miss with no active team returns the same error without crashing on the getTeamManager() null guard.

Finding 4 — suites on macOS

check result
packages/core full suite 617 files / 22271 tests passed, 28 skipped, exit 0 (71s)
send-message.test.ts focused 24/24 (23 in the PR + 1 arriving from main)
npm run typecheck (all workspaces) pass
eslint + prettier --check on both changed files pass
merge into current main clean, no semantic conflict with #10081, which touched the same file

Mutation matrix (6 mutants over the fix): 6/6 killed — the tests do discriminate at the unit level. Worth reading alongside Finding 2 though: the three mutants covering the hint (M3 hint dropped, M4 returnDisplay wording, M5 un-normalized name) are killed only by assertions on that caller-less method, so a green matrix here does not imply the behaviour ships.


Merge reference

The disambiguation is real, correctly placed, and the smallest version of the fix — that half I'd take as-is. The hint half is dead code as written and should either move into error.message (one line, above) or come out.

One more input for the decision: #10090 is a competing fix for the same issue #10073, opened 36 minutes after this one. I verified it the same way; its disambiguation half also works, and its hint half fails for the identical reason. Whichever lands, the other should close, and the surviving one should carry the error.message change.

中文说明

维护者验证 —— macOS 上的本地真实栈实跑

我在本地构建并端到端跑通了这个 PR,用来补上分诊机器人指出的证据缺口(CI 从未在该提交上执行过,且 PR 自述 macOS 未测试)。

被测树: origin/main bfda082 + PR head 99e47d5 本地合并 —— ort 干净合并,有效 diff 仍是 97+/4-。
主机: macOS 26.6.2(arm64)、Node v24.18.1、npm 11.16.0。
方法: 以下全部经由打包后的 CLInpm run build && npm run bundledist/cli.js)、隔离的 QWEN_HOMEQWEN_CODE_ENABLE_AGENT_TEAM=1,以及一个脚本化的假 OpenAI provider,驱动一个真实 Agent Teamteam_create → 用 agent({name}) 拉起具名队友 qa-reviewer → 真实后台任务 → 10 个 send_message 探针。判据取自链路上抓到的、模型实际收到的 role: "tool" 帧。

三个臂,除一个文件外树完全相同:

bundle
base 合并树,仅把 send-message.ts 的生产 hunk 回退到 origin/main
head 合并树原样
witness head + 一行改动(见发现 2)

harness、原始流水账与大图:https://github.com/wenshao/qwen-code/tree/assets-pr10083

结论

声明 结果
1 同时给 to + task_id 会在任一分发路径前被拒绝 生产链路确认有效
2 task_id 未命中且撞上队友名时提示改用 to 生产上完全失效 —— 模型和用户都看不到
3 单目的地调用行为不变 ✅ 10 个探针无回归
4 macOS 上 packages/core 套件 / typecheck / lint / prettier ✅ 全绿

发现 1 —— 消歧这一半有效,且 #10073 在当前 main 上可复现

base 精确复现了报告的 bug:调用同时带 to: "qa-reviewer"task_id,工具报成功,消息却进了后台任务,队友会话什么也没收到。截图见英文部分:base 是静默误投(绿勾),head 是在 build() 内拒绝——早于注册表查找、早于任一分发路径。

投递与否是在链路上核的,不是只看文案:歧义 sentinel 在两个臂里都出现在 0 个非 leader 的模型请求中;而对照探针 send_message { to: "qa-reviewer" }(不带 task_id)出现在 1 个中——说明队友通道确实是活的,base 确实丢了 to。10 个探针里 base 与 head 只差这一行

发现 2 —— 队友提示语在已发布的代码路径上不可达

这一条是 CI 无论如何也抓不到的,因为 PR 的测试打的是一个没有生产调用方的方法。

send_message { task_id: "QA Reviewer" }(名册里是 qa-reviewer),base 与 head 的结果逐字节相同——Task not found: QA Reviewer——链路上如此,TUI 上也如此。提示语从未出现。

机制在 packages/core/src/core/coreToolScheduler.ts

  • ToolResulterror 时,面向模型的载荷由 createErrorResponse(L928)构造为 response: { error: error.message }(L943)——llmContent 被整体丢弃
  • 面向用户的载荷是 resultDisplay: resultDisplay ?? error.message(L947),而错误分支在 returnDisplay 是字符串时传入 undefined(L5744)——returnDisplay 同样被丢弃

本 PR 把提示语追加到 llmContentreturnDisplay,而未找到的结果是带 error 的。两半都被丢掉了。

测试之所以全绿,是因为 BaseDeclarativeTool.validateBuildAndExecute 返回的是工具自己的 ToolResultllmContent 原样保留——而该方法没有任何生产调用点grep -rn 'validateBuildAndExecute' packages --include='*.ts' | grep -v '\.test\.ts' 只命中它自己的定义)。于是 expect(result.llmContent).toContain(...) 能绿,而那段文案在真实链路上根本不存在。

建设性证明与修法。 一行见证补丁——把同一个提示语改追加到 error.message——提示语立刻出现在链路上(diff 见英文部分)。main 上已有这个约定:broadcast 失败那次改动(#10081)就把同一串同时写进 llmContenterror.message

顺带说明:这也让两条被延后的评审发现(leader 不在 members、提示单向)变得无意义——我对二者都做了实测探针(task_id: "leader",以及把后台任务 id 当 to 传),都不出提示;但按现状本来就没有任何提示可达。

发现 3 —— 单目的地路径无回归

head 上实测:只给 to 能投递(Message sent to "qa-reviewer".,且 sentinel 出现在队友自己的模型请求里);只给 task_id 仍能排队;通用的任务未找到错误不变;无活动团队时的 task_id 未命中返回同样的错误,未在 getTeamManager() 空值守卫上崩溃。

发现 4 —— macOS 上的套件

检查 结果
packages/core 全量套件 617 文件 / 22271 测试通过,28 跳过,exit 0(71 秒)
send-message.test.ts 聚焦 24/24(PR 的 23 条 + 从 main 合入的 1 条)
npm run typecheck(全工作区) 通过
两个改动文件的 eslint + prettier --check 通过
合入当前 main 干净,与同文件的 #10081 无语义冲突

变异矩阵(针对修复的 6 个变异体):6/6 被杀——单测层面确有辨别力。但要和发现 2 一起读:覆盖提示语的三个变异体(M3 去掉提示、M4 returnDisplay 措辞、M5 不做名称规范化)只被那个无调用方方法上的断言杀死,所以矩阵全绿并不意味着该行为真的会随包发布。

合并参考

消歧是真实有效的,位置也放对了,而且是这个修复的最小形态——这一半我倾向原样接受。提示语那一半按现状是死代码,要么移进 error.message(就上面那一行),要么去掉。

还有一条决策输入:#10090 是同一 issue #10073 的竞争修复,比本 PR 晚 36 分钟提出。我用同样的方法验过它:它的消歧那一半同样有效,提示语那一半也因完全相同的原因失效。无论哪个合入,另一个都该关闭,并由存活的那个带上 error.message 的改动。

Verified locally by @wenshao at 99e47d5307b442504582a607ce9a198b46b4dc3a. Harness and raw evidence: https://github.com/wenshao/qwen-code/tree/assets-pr10083

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

Reviewed.

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R6-1 to/task_id mutual exclusivity not stated in the schema descriptions (send-message.ts:341) — already reported (round 2 deferral, review 5029062877)
  • R6-2 teammate hint can never fire for leader (TeamFile.members excludes the leader) (send-message.ts:102) — already reported (round 5 deferral, review 5042676420)

Not explored to full depth (tool budget reached): "agent 6b": running packages/core vitest for send-message.test.ts (worktree has no node_modules; installing would mutate the shared review tree and exceed budget) — ass….

中文说明

已审查。

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent 6b"running packages/core vitest for send-message.test.ts (worktree has no node_modules; installing would mutate the shared review tree and exceed budget) — ass…

— qwen3.8-max via Qwen Code /review (v0.22.3)

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — round 2, on Linux, at d5f1106c23

Round 1 (macOS, at head 99e47d5) reported that the disambiguation half worked but the teammate hint was inert in production: coreToolScheduler forwards only error.message for a ToolResult that carries error, and the hint lived in llmContent / returnDisplay. The author then pushed d5f1106 — the one-line change that moves the hint into error.message.

This round re-verifies the whole PR at that new head, on the platform the PR still self-reports as untested.

Tree under test: PR head d5f1106. Also merged onto current origin/main (156 commits ahead of the merge-base): clean merge, no conflict with #10081, which touched the same file; focused suite stays 24/24 on the merged tree.
Host: Debian 13 (kernel 6.12.63, x86-64), Node v22.22.2, npm 10.9.7.
Method: everything below goes through the bundled CLI (npm run build && npm run bundledist/cli.js), an isolated HOME, QWEN_CODE_ENABLE_AGENT_TEAM=1, --approval-mode yolo, and a scripted fake OpenAI provider driving a real Agent Team inside tmux: team_create → named teammate qa-reviewer via agent({name}) → a real background task → six destination scenarios. Two arms, identical trees except packages/core/src/tools/send-message.ts (main = production hunk reverted to merge-base 0756be0ce7).

Harness, raw wire ledgers, pane captures and full-size images: https://github.com/wenshao/qwen-code/tree/assets-pr10083/round2


Verdict

claim result
1 round-1 Finding 2 (hint unreachable) is fixed by d5f1106c23 resolved — confirmed on the wire and in the TUI
2 ambiguous to + task_id rejected before either dispatch path ✅ confirmed, including when task_id is a live task
3 single-destination calls unchanged ✅ no regression
4 red Test (ubuntu-latest) check ⚠️ stale base, not this diff — passes after merging main
5 packages/core suite / typecheck / lint / prettier on Linux ✅ green (2 pre-existing env failures, proven at merge-base)

Recommendation: merge, after a rebase onto main. Two residual hint gaps below are real but out of the PR's stated scope — they were listed as deferred in round 1 and were moot then because no hint was reachable; now that hints do reach the model, they are live again. Neither blocks.


Finding 1 — the hint now reaches both the model and the user

Round 1's central objection is closed. Same probe, both arms, at the new head:

base (send-message.ts at merge-base) — the exact diagnostic #10073 reported:

base

head (this PR) — hint present, ambiguous call rejected, and the hint is actionable:

head

Scenario C is the part worth calling out: the run follows the hint it was just given (to: "qa-reviewer"), the message is delivered, and the teammate reports back — so the suggestion is not just wording, it points at a destination that actually works.

The user-visible half is the screenshot; the model-visible half was checked off the wire, in the role: "tool" frame the provider actually received:

head arm  — packages/core/.../send-message.ts as shipped
  role: tool
  content: Task not found: QA Reviewer. Did you mean to message teammate "qa-reviewer"?
           If so, use `to: "qa-reviewer"` instead of `task_id`.

base arm  — same probe, hint string appears in 0 of the model requests

validateBuildAndExecute still has no production call site (grep -rn 'validateBuildAndExecute' packages --include='*.ts' | grep -v '\.test\.ts' → only its own definition at tools.ts:364), so the llmContent / returnDisplay assertions remain assertions on a caller-less method. What changed is that d5f1106c23 also added an assertion on result.error?.message — the one field production forwards:

expect(result.error?.message).toContain('use `to: "qa-reviewer"` instead of `task_id`');

That is the assertion that now carries the behaviour, and it is the one my M5 mutant is killed by.

Finding 2 — disambiguation holds, including the silent-misroute case

The strongest form of the bug is a both-field call where task_id names a live background task: on base the tool reports success and the teammate is never told.

base — green check, message queued to the background task, to silently dropped:

base misroute

head — rejected in build(), before the registry lookup and before either dispatch path:

head misroute

Non-delivery was checked on disk as well as in the message text: after the base run, ~/.qwen/teams/verify-team/inboxes/ contains only leader.json — no qa-reviewer.json was ever created. The guard is also destination-agnostic and pre-team: it fires with to: "*", and with no active team at all. An empty-string to does not trip it, so a to: "" + task_id call still routes to the task rather than erroring.

Finding 3 — two hint gaps, now live again (non-blocking)

Round 1 deferred these as moot. With hints reachable they are observable, and both are the same class of confusing diagnostic that #10073 is about.

leader is a valid to: destination but is excluded from the hint. The leader is never in teamFile.members (TeamManager.ts says so explicitly), and leader is a reserved teammate name, so findMemberByName can never match it:

leader gap

LEADER_NAME is already imported in send-message.ts, so this is small. I built and ran the witness rather than proposing it untested — tsc -p packages/core exit 0, focused suite still 24/24, and the hint fires in the real CLI:

-import { findMemberByName } from '../agents/team/teamHelpers.js';
+import {
+  findMemberByName,
+  sanitizeName,
+} from '../agents/team/teamHelpers.js';
@@
-        const teammate = teamManager
-          ? findMemberByName(
-              teamManager.getTeamFile().members,
-              this.params.task_id,
-            )
-          : undefined;
+        const teammate = !teamManager
+          ? undefined
+          : sanitizeName(this.params.task_id) === LEADER_NAME
+            ? { name: LEADER_NAME }
+            : findMemberByName(
+                teamManager.getTeamFile().members,
+                this.params.task_id,
+              );

leader witness

One wording nit if you take it: the leader is not a teammate, so that branch reads better with its own sentence than by reusing the "teammate" phrasing.

The hint is one-directional. A live background-task id passed as to gets Teammate "..." not found. with no suggestion to use task_id — the mirror image of the case this PR fixes:

reverse gap

One more sharp edge, informational: findMemberByName normalizes through sanitizeName, which maps every non-[a-z0-9-] character to a hyphen. qa reviewer, QA_REVIEWER, qa.reviewer, qa/reviewer and --qa--reviewer-- all produce the qa-reviewer hint. That is the right call for a suggestion (generous beats silent), and a live task always wins the lookup before the hint path is reached — I verified that precedence with a task and a teammate sharing the name qa-reviewer.

Finding 4 — the red CI check is stale base, not this diff

Test (ubuntu-latest, Node 22.x) fails at step Check TUI dependency direction:

$ npm run check:tui-dep-direction
npm error Missing script: "check:tui-dep-direction"

The script exists in origin/main's package.json and not in the PR branch's — it was added after this PR's merge-base, while ci.yml:489 already invokes it:

PR head d5f1106c23   -> 0 occurrences
merge-base 0756be0ce -> 0 occurrences
origin/main          -> 1 occurrence

Merging main into the branch locally resolves it: clean merge, and the failing step then passes —

[rule] packages/core/src — framework-neutral business core
  scanned 1310 files, 9639 import specifiers
  OK: no violations
PASS — dependency direction holds.

A rebase should turn the check green with no code change.

Finding 5 — suites, and non-vacuity

check result
send-message.test.ts focused 24/24 at head; 24/24 on the tree merged with current main
packages/core full suite 21680 passed / 2 failed / 13 skipped (603 files)
the 2 failures skill-curator + session-writer-leasefail identically at merge-base 0756be0ce7 with no PR code in the tree; both inject failure via filesystem permissions and this host runs as uid 0
packages/core/src/tools + src/agents/team scoped 100 files / 3422 tests passed
npm run typecheck (all workspaces) pass
eslint + prettier --check on both changed files pass

Non-vacuity A/B — production hunk reverted to merge-base, tests kept from head: 2 failed | 22 passed. The third new test (returns error for non-existent task without an active team) survives, correctly — it is a negative control, not a fix-proving test.

Mutation matrix — 5/5 killed:

mutant result
M1 drop the teamManager null-guard killed — and only by the surviving A/B test above, which is what earns it its place
M2 to && task_idto || task_id killed (19 tests)
M3 drop name normalization killed
M4 drop the returnDisplay teammate branch killed
M5 drop the error.message hint append (the d5f1106c23 change) killed

Merge reference

The objection from round 1 is fixed, and fixed in the way that was suggested. The disambiguation half is real, correctly placed at the validateToolParamValues hook, and reaches production; the hint half now reaches both the model and the user. I would take this as-is after a rebase, and file the two hint gaps in Finding 3 as a follow-up rather than holding the PR for them.

#10090 is still open as a competing fix for the same issue; whichever lands, the other should close.

中文说明

维护者验证 —— 第 2 轮,Linux 平台,基于 d5f1106c23

第 1 轮(macOS,head 99e47d5)的结论是:消歧那一半有效,但队友提示语在生产上完全失效——ToolResulterrorcoreToolScheduler 只转发 error.message,而提示语写在 llmContent / returnDisplay 里。随后作者推了 d5f1106,正是把提示语移进 error.message 的那一行改动。

本轮在这个新 head 上重新验证整个 PR,平台选的是 PR 自述仍未测试的 Linux。

被测树: PR head d5f1106。同时本地合入当前 origin/main(比 merge-base 领先 156 个提交):干净合并,与改动同一文件的 #10081 无冲突,合并后聚焦套件仍是 24/24。
主机: Debian 13(内核 6.12.63,x86-64)、Node v22.22.2、npm 10.9.7。
方法: 以下全部经由打包后的 CLInpm run build && npm run bundledist/cli.js)、隔离的 HOMEQWEN_CODE_ENABLE_AGENT_TEAM=1--approval-mode yolo,以及一个脚本化的假 OpenAI provider,在 tmux 中驱动真实 Agent Teamteam_create → 用 agent({name}) 拉起具名队友 qa-reviewer → 真实后台任务 → 六个目的地场景。两个臂,除 packages/core/src/tools/send-message.ts 外树完全相同(main 臂把生产 hunk 回退到 merge-base 0756be0ce7)。

harness、原始链路流水账、pane 捕获与大图:https://github.com/wenshao/qwen-code/tree/assets-pr10083/round2

结论

声明 结果
1 第 1 轮发现 2(提示语不可达)已被 d5f1106c23 修复 已解决 —— 链路与 TUI 双向确认
2 同时给 to + task_id 会在任一分发路径前被拒绝 ✅ 确认,包括 task_id 指向存活任务的情况
3 单目的地调用行为不变 ✅ 无回归
4 变红的 Test (ubuntu-latest) 检查 ⚠️ 是基线过旧,不是本次 diff —— 合入 main 后通过
5 Linux 上 packages/core 套件 / typecheck / lint / prettier ✅ 全绿(2 个既有环境性失败,已在 merge-base 上证明)

建议:rebase 后合入。 下面第 3 条的两个提示语残留缺口是真实存在的,但不在本 PR 声明的范围内——它们在第 1 轮被列为「延后」,当时因为没有任何提示可达而无意义;现在提示语确实能到达模型,它们又变成可观测的了。两者都不构成阻断。

发现 1 —— 提示语现在同时到达模型和用户

第 1 轮的核心反对意见已关闭。同一探针、两个臂、新 head:base 臂复现 #10073 报告的那条诊断;head 臂提示语出现,歧义调用被拒绝,且提示可执行——截图见英文部分。

场景 C 值得单独点出:那一轮直接照着刚拿到的提示改用 to: "qa-reviewer",消息投递成功,队友回报——说明这条建议不只是文案,它指向的目的地是真能用的。

用户可见的那一半看截图;模型可见的那一半是在链路上核的,取的是 provider 实际收到的 role: "tool" 帧:

head 臂
  role: tool
  content: Task not found: QA Reviewer. Did you mean to message teammate "qa-reviewer"?
           If so, use `to: "qa-reviewer"` instead of `task_id`.

base 臂 —— 同一探针,提示语字符串在 0 个模型请求中出现

validateBuildAndExecute 仍然没有生产调用点(grep 只命中 tools.ts:364 它自己的定义),所以 llmContent / returnDisplay 上的断言依旧打在一个无调用方的方法上。变化在于 d5f1106c23 同时加了 expect(result.error?.message).toContain(...) —— 打在生产真正转发的那个字段上。现在是这条断言在承载行为。

发现 2 —— 消歧成立,包括静默误投这一档

这个 bug 最强的形态是:两个字段都给,且 task_id 指向一个存活的后台任务。base 上工具报成功,队友完全不知情。head 上在 build() 内被拒绝——早于注册表查找、早于任一分发路径。

未投递不仅看文案,也在磁盘上核过:base 跑完后 ~/.qwen/teams/verify-team/inboxes/只有 leader.jsonqa-reviewer.json 从未被创建。该守卫也与目的地无关、且早于团队存在:to: "*" 会触发,完全没有活动团队时也会触发。空字符串 to 不会误触发,所以 to: "" + task_id 仍然正常投给任务而不是报错。

发现 3 —— 两个提示语缺口,现在重新变成可观测(不阻断)

第 1 轮把它们判为无意义。提示语可达之后它们就能被观察到了,而且属于 #10073 所针对的同一类误导性诊断。

leader 是有效的 to: 目的地,却被排除在提示之外。 leader 从不在 teamFile.members 里(TeamManager.ts 有明确注释),且 leader 是保留的队友名,所以 findMemberByName 永远匹配不到它。LEADER_NAMEsend-message.ts 里已经 import 了,改动很小。我没有直接抛出未验证的建议,而是把 witness 构建跑通了:tsc -p packages/core exit 0、聚焦套件仍是 24/24、真实 CLI 中提示语正常触发——diff 与截图见英文部分。如果采纳,有个措辞小建议:leader 并不是 teammate,那个分支单独写一句话会比复用「teammate」的句式更贴切。

提示是单向的。 把一个存活的后台任务 id 当作 to 传进去,得到的是 Teammate "..." not found.,没有任何「改用 task_id」的建议——正是本 PR 所修那一档的镜像。

还有一个信息性的边角:findMemberByName 经由 sanitizeName 归一化,会把每一个[a-z0-9-] 字符都替换成连字符。qa reviewerQA_REVIEWERqa.reviewerqa/reviewer--qa--reviewer-- 全都会给出 qa-reviewer 的提示。作为「建议」这样处理是对的(宁可宽松也别沉默),而且存活任务总是在提示路径之前赢得查找——我用一个同名为 qa-reviewer 的任务和队友验过这个优先级。

发现 4 —— 变红的 CI 是基线过旧,不是本次 diff

Test (ubuntu-latest, Node 22.x) 挂在 Check TUI dependency direction 这一步:npm error Missing script: "check:tui-dep-direction"。该脚本存在于 origin/mainpackage.json,不存在于 PR 分支——它是在本 PR 的 merge-base 之后才加进 main 的,而 ci.yml:489 已经在调用它(PR head 0 处、merge-base 0 处、origin/main 1 处)。本地把 main 合进分支即可解决:干净合并,且那一步随后通过(PASS — dependency direction holds.)。rebase 一下应该就能让这个检查转绿,不需要改任何代码。

发现 5 —— 套件与非平凡性

检查 结果
send-message.test.ts 聚焦 head 上 24/24;与当前 main 合并后的树上同样 24/24
packages/core 全量套件 21680 通过 / 2 失败 / 13 跳过(603 文件)
那 2 个失败 skill-curator + session-writer-lease —— 在没有任何 PR 代码的 merge-base 0756be0ce7同样失败;两者都靠文件系统权限注入失败,而本机以 uid 0 运行
packages/core/src/tools + src/agents/team 范围 100 文件 / 3422 测试通过
npm run typecheck(全工作区) 通过
两个改动文件的 eslint + prettier --check 通过

非平凡性 A/B —— 把生产 hunk 回退到 merge-base、保留 head 的测试:2 失败 | 22 通过。第三条新测试(returns error for non-existent task without an active team)存活,这是对的——它是负向对照,不是用来证明修复的。

变异矩阵 —— 5/5 被杀: M1 去掉 teamManager 空值守卫(只被上面那条存活的 A/B 测试杀死,这正是它存在的理由)、M2 &&||(杀死 19 条)、M3 去掉名称归一化、M4 去掉 returnDisplay 队友分支、M5 去掉 error.message 提示追加(即 d5f1106c23 那行改动)。

合并参考

第 1 轮的反对意见已被修复,而且是按当时建议的方式修的。消歧那一半真实有效、位置放在 validateToolParamValues 钩子上是对的、且能到达生产;提示语那一半现在模型和用户都能收到。我倾向 rebase 之后原样接受,把发现 3 的两个提示语缺口作为后续 issue 处理,而不是为它们卡住本 PR。

#10090 作为同一 issue 的竞争修复仍处于 open;无论哪个合入,另一个都该关闭。

Verified locally by @wenshao at d5f1106c238f88d3844231a6dded2975b7ccc6a5 on Linux. Harness and raw evidence: https://github.com/wenshao/qwen-code/tree/assets-pr10083/round2

@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)为单个提交。

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge August 29, 2026 13:13
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 3267e34b10b964aa822e6d7740ff830594435f72 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 3267e34b10b964aa822e6d7740ff830594435f72既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 29, 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: 52 passed · 0 failed · 52 total

Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence

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

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

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

抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #10083 Deep Verification — fix(core): disambiguate send_message destinations

Verdict: merge-ready — 52/52 scripted assertions passed, 0 failures. Verified head: 3267e34b10b964aa822e6d7740ff830594435f72 (git rev-parse HEAD^2; merge commit 58e8132, base tip 37e7491).

中文摘要
  • 结论: merge-ready。52/52 条脚本化断言全部通过,无失败。
  • A/B 结论: 中心声明成立且是承重的。在 base(37e7491)上,同时携带 totask_id 的调用被静默排入后台任务队列、teammate 目的地被丢弃(S1/S5 base 单元格);在 PR head 上,同样的调用在任何投递路径执行前即以 INVALID_TOOL_PARAMS 拒绝(Only one of "to" or "task_id" may be provided.),任务队列与 teammate 投递均未收到消息。任务未命中但名称匹配 teammate 的场景(如 QA Reviewerqa-reviewer)在 head 上三个输出面(llmContent/returnDisplay/error.message)都附带改用 to 的提示,且错误类型保持 SEND_MESSAGE_NOT_FOUND;无关 ID 的通用错误与 base 逐字节一致(回归控制单元格)。
  • Findings: 无。两条非阻塞观察见正文(空白 to 的守卫边界、"机器可读消息" 措辞在匹配场景下略有放宽——类型保持不变且无任何消费者解析该消息文本)。
  • 未覆盖: 仓库级/全包测试套件(仅运行两个目标套件)、TUI 渲染层、真实 daemon/团队端到端、逐提交归因(depth-2 浅克隆不可达,只验证了聚合 diff)、集成测试目录。

Central claim and A/B proof

Central claim: a send_message call supplying both to and task_id is rejected by parameter validation before either dispatch path runs.
Secondary claims: (2) a task-miss whose ID matches a teammate by sanitizeName keeps SEND_MESSAGE_NOT_FOUND and adds a to: hint; (3) unrelated task misses are byte-identical to base.

Control: packages/core rebuilt at base tip 37e7491 in a scratch worktree (shared root node_modules — the PR touches no package.json/package-lock.json, and packages/core has no internal @qwen-code/* dependencies, so the two dists differ only by the compiled PR hunks; verified grep 'Only one of|Did you mean to message teammate' = 0 hits in base dist, 1+1 at head). Harness ab-harness.mjs drives the real compiled validateBuildAndExecute path with the real BackgroundTaskRegistry and call-recording delivery spies; expectations are arm-encoded. Witness captures: 01-ab-head-cells.png, 02-ab-base-cells.png.

# Scenario Oracle base (37e7491) head (3267e34)
S1 both to:'alice' + task_id:'agent-1', both valid error type; queued messages; teammate-send calls queued to task, teammate silently skipped, no error INVALID_TOOL_PARAMS, queue empty, 0 sends
S2 task_id:'QA Reviewer', teammate qa-reviewer exists, no such task error type/message; hint on 3 surfaces SEND_MESSAGE_NOT_FOUND, bare Task not found: QA Reviewer, no hint same type + hint on llmContent, returnDisplay, error.message
S3 task_id:'nope', unrelated byte-identity of all 4 result fields generic error byte-identical to base
S4 task miss with getTeamManager() → null no crash, generic error generic error generic error, no crash
S5 both fields, task missing, name matches teammate error type; sends plain miss error, to silently discarded INVALID_TOOL_PARAMS, 0 sends

Score: head arm 17/17 assertions, base arm 13/13 (each base cell asserts the predicted pre-fix behavior — the flip is 3 broken/ambiguous behaviors → 3 fixed, with S3/S4 held byte-identical as regression controls).

Production seam verified by reading, not only by the tool-level harness: CoreToolScheduler.buildInvocation (both call sites, coreToolScheduler.ts L1763 and L2591) wraps tool.build() and converts a validation throw into createErrorResponse(..., INVALID_TOOL_PARAMS, 'not_started') — the invocation is never constructed, so execute() cannot run. BaseDeclarativeTool.build runs schema validation first, then the new validateToolParamValues. The only instantiation site is the tool factory in config.ts; nothing constructs SendMessageInvocation directly. The secondary gate suite coreToolScheduler.test.ts (which exercises this seam) passed 382/382.

Corrections

None — no inaccurate claims found in prior review rounds (first round).

Findings

None. Two non-blocking observations from the shape probes (03-shape-probes.png, 14/14):

  1. Guard boundary is truthiness, not trim: to: '' + task_id routes as task-only (empty = absent, consistent with the to-only path), while to: ' ' + task_id hits the mutual-exclusion error. Both outcomes are non-delivery with an explicit error, so no miscast delivery exists; noted only for completeness.
  2. Hint matcher inherits sanitizeName: ASCII-adjacent spellings all reach the hint ('QA Reviewer', 'qa_reviewer', 'QA-Reviewer', padded forms); fullwidth Unicode (QA Reviewer) sanitizes to empty and falls through to the generic error — consistent with how TeamManager itself resolves names, and the hint is advisory. Related exactness checks: '*', 'leader', near-miss suffixes and substrings correctly get no hint; a registered task literally named 'QA Reviewer' is still queued, never hinted.

Also noted: the description says the change "preserves the machine-readable message" — strictly, error.message gains the hint suffix in the matching case. The type is preserved, and no code anywhere parses the message text (grepped all consumers of SEND_MESSAGE_NOT_FOUND and Task not found; the type has no branching consumer), so this is wording, not a defect.

Vacuity / mutation matrix

All runs against src/tools/send-message.test.ts at head (45 tests), each mutation a single-hunk edit reverted after the run (tree restored, git status clean). Witness captures: 04-matrix-m0-control.png, 05-matrix-m1-no-validation.png, 06-matrix-m2-no-hint.png, 07-matrix-ctrl-reword.png.

row mutation result killed by (verbatim assertion)
M0 none (control) 45/45 green
M1 validateToolParamValues override removed 44/45, exactly 1 red rejects ambiguous teammate and background-task destinationsexpected undefined to be 'invalid_tool_params' (behavioral: the call now succeeds and queues)
M2 teammate-hint hunk reverted to base block 44/45, exactly 1 red suggests the teammate destination for a matching task IDexpected 'Task not found: QA Reviewer' to contain 'use to: "qa-reviewer"...'
C validation message reworded (Exactly one of...), same file 44/45, exactly 1 red same test as M1 — positive control proving the vitest invocation collects and executes this file

No survivors among the PR's two guards; both hunks defend different hazards (no layered-guard combination row applies). Additional pinned axes: expect(result.error?.message).toBe('Task not found: nope') (exact match) pins the no-hint behavior for unrelated misses, and the no-team variant test pins the null-manager guard. The ambiguous test's sendMessage not called assertion does not by itself distinguish M1 (base also skips the teammate send when a task matches) — the pinning comes from the pendingMessages and error-type assertions, which is precisely the silent-ignore behavior the PR fixes. Note: the PR body cites "23/23 focused tests"; the file at head contains 45 tests, all green here (Linux/CI) — the author's count appears to reflect a focused subset on their Windows run.

Reviewer Test Plan walkthrough

All three steps executable and verified: step 1 → S1 head cell (+ scheduler seam above); step 2 → S2 + probes ('QA Reviewer'to: "qa-reviewer"); step 3 → S3 (byte-identical generic error, no hint). No unreachable step.

Not covered

  • Repo-wide suite and the full packages/core suite — only the two targeted files ran (send-message.test.ts 45/45, coreToolScheduler.test.ts 382/382). No other suite exercises the changed logic (others reference the tool only via MockTool/registration checks).
  • TUI/pixel-level rendering of the new returnDisplay strings — verified at the tool-result level only.
  • Real daemon / real TeamManager end-to-end: the harness uses the Config seam with a stub manager exposing getTeamFile(); the real one returns its in-memory teamFile with identical shape (read at TeamManager.ts:1442), but no live team was booted.
  • Per-commit attribution: checkout is depth 2 (git rev-list HEAD^1..HEAD^2 sees 1 of the 5 commits in the metadata), so only the aggregate HEAD^1..HEAD diff was verified.
  • integration-tests/ and the Windows-specific failures the author reported (unrelated suites on their host).
  • verify-capture.mjs worked for every capture; no evidence gaps.

Methodology

Environment: CI verify container (node:22-bookworm), working tree at refs/pull/10083/merge; npm ci + npm run build pre-done at head. Control build: git worktree add tmp/base-tree HEAD^1, then npm run build -w packages/core inside it after symlinking the root node_modules and packages/core/node_modules (per-package otel deps are not hoisted; first build attempt failed on that, second succeeded after the links — the link targets are third-party-only and identical for both cells since the lockfile is untouched). A/B and probe harnesses (ab-harness.mjs, probe-harness.mjs) import the compiled dists directly (mock-free for the unit under test; Config is the seam) and ran under scripts/verify-capture.mjs for the PNG witnesses. Mutation matrix via apply-mutation.mjs (exact one-occurrence string replacement, fails loud otherwise) + matrix-verify.mjs (asserts collected count and exact failed-test set from vitest JSON); all raw vitest JSON/stderr logs in logs/. Source restored with git checkout -- after every mutation; git status clean at exit.

Flakiness gate log

rounds=5 files=1 skipped=0
file packages/core/src/tools/send-message.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/send-message.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/core/src/tools/send-message.test.ts: PPPPP

verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/core/src/tools/send-message.test.ts: P (exit 0)
round 2 · packages/core/src/tools/send-message.test.ts: P (exit 0)
round 3 · packages/core/src/tools/send-message.test.ts: P (exit 0)
round 4 · packages/core/src/tools/send-message.test.ts: P (exit 0)
round 5 · packages/core/src/tools/send-message.test.ts: P (exit 0)

Evidence images

01-ab-head-cells

02-ab-base-cells

03-shape-probes

04-matrix-m0-control

05-matrix-m1-no-validation

06-matrix-m2-no-hint

07-matrix-ctrl-reword

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

Qwen Code · sandboxed verification

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

Partially reviewed — gaps disclosed.

4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R7-1 corrective hint is one-directional (to→task_id mirror confusion gets no hint) — already reported (round 5 deferral, review 5042676420)
  • R7-2 to/task_id mutual exclusivity not stated in the schema descriptions — already reported (round 2 deferral, review 5029062877; re-confirmed as R6-1 in round 6, review 5057551601)
  • R7-3 hint can never fire for leader (TeamFile.members excludes the leader) — already reported (round 5 deferral, review 5042676420; re-confirmed as R6-2 in round 6, review 5057551601)
  • R7-4 teammate hint can fire for terminated teammates — already reported (round 2 deferral, review 5029062877)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent 6b": run packages/core unit tests ( send-message.test.ts ) to confirm green — the review worktree has no node_modules , and npm ci + npm run build exceeds the…; "agent 6a": run packages/core/src/tools/send-message.test.ts (worktree has no node_modules ; vitest failed at config load) — judgment rests on the static trace above, wh….

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Test Plan (not a blocker): 23 tests passed — this review observed 1795 passed.

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):"agent 6b"run packages/core unit tests ( send-message.test.ts ) to confirm green — the review worktree has no node_modules , and npm ci + npm run build exceeds the…"agent 6a"run packages/core/src/tools/send-message.test.ts (worktree has no node_modules ; vitest failed at config load) — judgment rests on the static trace above, wh…

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

Test Plan(非阻断):23 tests passed — this review observed 1795 passed

— qwen3.8-max via Qwen Code /review (v0.22.3)

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — CI landed green after the review. ✅

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Local verification — Linux lane

I built a real verification environment for this PR on Linux and drove the change three ways: the PR's own suite with a negative control, an independent probe that shares no scaffolding with that suite, and a real TUI run with a real Agent Team. Result: merge-ready, no blocking findings. The PR's "Tested on" table lists Windows only, so this adds the 🐧 Linux lane.

Arms: base = merge-base 37e7491, PR = head 3267e34. Node v22.22.2, Debian 13 (kernel 6.12), both arms built from source (tsc --build for packages/core, then the esbuild bundle).

1. Real TUI, real Agent Team — the two shapes from #10073

Same scripted leader driven against a scripted OpenAI-compatible endpoint, in a real session with QWEN_CODE_ENABLE_AGENT_TEAM=1: team_create → spawn teammate qa-reviewer (its own AgentCore, its own model turns) → launch a background subagent and hold its model turn open so it stays running and messageable → list_agents → then the two calls.

Real TUI A/B

Before (base). send_message({ to: "qa-reviewer", task_id: "<live background agent>", … }) renders as SendMessage — Send message to task general-purpose-call_… and reports Message queued for background sleeper agent. The teammate destination was dropped without a word, and the message landed on the background agent. The second call, send_message({ task_id: "qa-reviewer" }), returns Task not found: qa-reviewer — the exact string from the issue report.

After (PR). The first call is refused before dispatch, and the second carries the hint:

Only one of "to" or "task_id" may be provided.
Task not found: qa-reviewer. Did you mean to message teammate "qa-reviewer"? If so, use `to: "qa-reviewer"` instead of `task_id`.

Worth noting for anyone reviewing the shape of the fix: the TUI surfaces error.message, not returnDisplay, so appending the hint to error.message is what actually makes it visible to the user as well as to the model.

2. Independent probe — real Config, real TeamManager, real registry

To avoid grading the PR against its own scaffolding, this probe uses a real Config (not an as unknown as Config literal), a real TeamManager over a real on-disk team file (QWEN_HOME redirect, ~/.qwen/teams/probe-team/config.json genuinely written), the Config's own BackgroundTaskRegistry, and a real teammate agent. Delivery is observed on the receiving side — the teammate's inbox and the registry's pendingMessages — never on a spy handed to the tool. Same script, both built arms.

Probe matrix

S1 is the one that matters: on base the ambiguous call is queued onto the background task while to is dropped; on the PR nothing reaches either destination. S9/S10 are the controls — single-destination sends still work unchanged on both arms.

3. Tests, controls, and static gates

Check Result
send-message.test.ts, PR arm 45/45 pass
src/tools + src/agents/team + background-tasks + src/ipc, PR arm 115 files / 3943 tests pass
Full packages/core, PR arm 620 files pass, 22496 tests pass; 2 fail
— those 2 failures skill-curator transient-rename and session-writer-lease unreadable-lock; both reproduce on base, both are artefacts of running the suite as root
Negative control — PR tests + send-message.ts reverted to merge-base exactly 2 fail (the ambiguity test, the hint test), 43 pass
Forward control — base test file + PR send-message.ts 42/42 pass, no pre-existing assertion disturbed
prettier --check, eslint --max-warnings 0 on both changed files clean
tsc --build on packages/core clean
esbuild bundle builds; both new strings present in dist/chunks/send-message-*.js

The negative control is the part I'd point a reviewer at: 3 test cases are added and 2 of them go red when only the production file is reverted, so the suite genuinely discriminates the fix rather than just being green.

I also checked that nothing can reach execute() around the new guard: coreToolScheduler.buildInvocation and agent-core both go through tool.build(), SendMessageInvocation is constructed in exactly one place, and no caller in packages/cli or packages/core supplies both destination fields — only a model can produce that shape. And since teammate names are sanitized to [a-z0-9-] at spawn, the new hint cannot echo arbitrary text back into the model's context.

Non-blocking notes

N1 — the model is never told the fields are exclusive. The tool description and both property descriptions still read as two independent optional fields, so a model's first ambiguous call is rejected rather than prevented; the rejection also doesn't say which field to keep. One clause in the send_message description ("provide exactly one of to or task_id") plus naming the right field in the error would turn a wasted round trip into a call that never happens. Given #7984 rules out a top-level oneOf, prose is the only lever left.

N2 — the hint covers roster members only. to also routes leader and teamFile.leadAgentId, and since #10158 it routes other sessions too. Measured: task_id: "leader" with a live team still returns the bare Task not found: leader on both arms. isInProcessRecipient(to, teamFile) — already imported and used a few lines below in this same file — is the canonical "would to route this in-process" predicate, and reusing it would cover the leader alias and the lead agent id for free. Peer sessions would need resolvePeerTarget and are a fair follow-up rather than something to hold this PR for.

N3 — the exclusivity test and the routing normalization disagree on whitespace. The guard is raw truthiness (params.to && params.task_id) while the routing path a few lines down normalizes with this.params.to?.trim(). Measured consequence: to: " " plus a live task_id is now rejected, while to: "" plus the same task_id still delivers to the task. Both outcomes are defensible, but params.to?.trim() in the guard would make the two halves agree.

N4 — two of the three added cases pin behaviour rather than prove it. "returns error for non-existent task without an active team" and the added not.toContain assertions on the existing test already hold on base. They're good regression pins for the no-hint path; they just aren't evidence of the fix, which is entirely carried by the other two.

None of these change my read: the fix is correct, minimal, and the runtime-validation route is the right call given #7984.

中文版本

本地验证 —— Linux lane

我在 Linux 上为这个 PR 搭了一套真实验证环境,从三个方向驱动这次改动:PR 自带的测试套件加反向对照、一套与该套件不共享任何脚手架的独立探针,以及一次带真实 Agent Team 的真实 TUI 运行。结论:可以合入,无阻塞问题。 PR 的 "Tested on" 表格只列了 Windows,这里补上 🐧 Linux。

两臂:base = merge-base 37e7491,PR = head 3267e34。Node v22.22.2,Debian 13(内核 6.12),两臂均从源码构建(packages/coretsc --build,再走 esbuild bundle)。

1. 真实 TUI、真实 Agent Team —— #10073 里的两种调用形态

同一份脚本化 leader,对着一个脚本化的 OpenAI 兼容端点运行,会话真实开启 QWEN_CODE_ENABLE_AGENT_TEAM=1team_create → spawn teammate qa-reviewer(有自己的 AgentCore、自己的模型轮次)→ 启动一个后台 subagent 并挂住它的模型轮次,让它保持 running 从而可被投递 → list_agents → 然后发出那两个调用。

真实 TUI A/B

修复前(base)。 send_message({ to: "qa-reviewer", task_id: "<存活的后台 agent>", … }) 在界面上显示为 SendMessage — Send message to task general-purpose-call_…,结果是 Message queued for background sleeper agent。teammate 这个目的地被无声丢弃,消息落到了后台 agent 上。第二个调用 send_message({ task_id: "qa-reviewer" }) 返回 Task not found: qa-reviewer —— 正是 issue 里报告的那句。

修复后(PR)。 第一个调用在投递前就被拒绝,第二个则带上了提示:

Only one of "to" or "task_id" may be provided.
Task not found: qa-reviewer. Did you mean to message teammate "qa-reviewer"? If so, use `to: "qa-reviewer"` instead of `task_id`.

有一点值得评审者注意:TUI 展示的是 error.message 而不是 returnDisplay,所以把提示追加到 error.message 上,才是让用户(而不只是模型)看得见提示的原因。

2. 独立探针 —— 真实 Config、真实 TeamManager、真实 registry

为了避免用 PR 自己的脚手架给 PR 打分,这套探针使用真实的 Config(不是 as unknown as Config 字面量)、基于真实落盘 team 文件的真实 TeamManager(通过 QWEN_HOME 重定向,~/.qwen/teams/probe-team/config.json 是真写出来的)、Config 自己持有的 BackgroundTaskRegistry,以及一个真实的 teammate agent。投递结果一律在接收侧观察 —— teammate 的收件箱和 registry 的 pendingMessages —— 而不是观察塞给工具的 spy。同一份脚本,跑两个已构建的臂。

探针矩阵

关键是 S1:base 上歧义调用被排进了后台任务队列、to 被丢弃;PR 上两个目的地都没收到任何东西。S9/S10 是对照组 —— 单目的地发送在两臂上行为完全一致。

3. 测试、对照与静态门禁

检查项 结果
send-message.test.ts,PR 臂 45/45 通过
src/tools + src/agents/team + background-tasks + src/ipc,PR 臂 115 个文件 / 3943 项测试通过
全量 packages/core,PR 臂 620 个文件通过,22496 项通过;2 项失败
—— 那 2 项失败 skill-curator 瞬时 rename 与 session-writer-lease 不可读锁;两项在 base 上同样复现,都是以 root 身份跑测试的产物
反向对照 —— PR 的测试 + send-message.ts 回退到 merge-base 恰好 2 项失败(歧义用例、提示用例),43 项通过
正向对照 —— base 的测试文件 + PR 的 send-message.ts 42/42 通过,未扰动任何既有断言
两个改动文件的 prettier --checkeslint --max-warnings 0 干净
packages/coretsc --build 干净
esbuild bundle 构建通过;两条新字符串都出现在 dist/chunks/send-message-*.js

反向对照是我最想让评审者看的一项:新增 3 个用例,其中 2 个在只回退生产文件后变红,说明这套测试真的能判别这次修复,而不只是"绿着"。

我还确认了没有任何路径能绕过新增的守卫直达 execute()coreToolScheduler.buildInvocationagent-core 都走 tool.build()SendMessageInvocation 全仓只有一处构造,且 packages/clipackages/core 中没有任何调用方会同时传两个目的地字段 —— 只有模型能产出这种形态。另外,teammate 名字在 spawn 时已被规范化为 [a-z0-9-],所以新增的提示不可能把任意文本回显进模型上下文。

非阻塞事项

N1 —— 模型从来没被告知这两个字段互斥。 工具描述和两个属性描述读起来仍然是两个彼此独立的可选字段,所以模型的第一次歧义调用是被拒绝、而不是被预防;拒绝信息里也没说该保留哪一个。在 send_message 描述里加一句("provide exactly one of to or task_id")、并在错误里点名该用哪个字段,就能把一次浪费的往返变成一次根本不会发生的调用。既然 #7984 排除了顶层 oneOf,措辞就是唯一还能用的杠杆。

N2 —— 提示只覆盖 roster 成员。 to 同时还能路由 leaderteamFile.leadAgentId,而自 #10158 起还能路由其他会话。实测:在有活跃团队的情况下,task_id: "leader" 在两臂上都仍然只返回裸的 Task not found: leaderisInProcessRecipient(to, teamFile) —— 本文件下面几行已经 import 并使用了 —— 正是"to 会不会把它路由到进程内"的规范判据,复用它就能顺带覆盖 leader 别名和 lead agent id。peer 会话需要 resolvePeerTarget,更适合作为后续项,不必为此卡住本 PR。

N3 —— 互斥判断与路由归一化在空白字符上不一致。 守卫用的是裸真值(params.to && params.task_id),而下面几行的路由路径用 this.params.to?.trim() 做归一化。实测后果:to: " " 加一个存活的 task_id 现在会被拒绝,而 to: "" 加同一个 task_id 仍然会投递到该任务。两种结果都说得通,但守卫里改成 params.to?.trim() 能让两半保持一致。

N4 —— 新增的三个用例里有两个是在钉行为、而不是在证明修复。 "returns error for non-existent task without an active team" 以及加到既有用例上的那几条 not.toContain 断言,在 base 上本来就成立。作为无提示路径的回归钉子它们是好的,只是它们不构成修复的证据 —— 证据完全由另外两个用例承担。

这些都不改变我的判断:修复是正确的、最小的,考虑到 #7984,走运行时校验这条路是对的。


🤖 Generated with Claude Code — Claude Opus 5 (1M context)

@wenshao
wenshao added this pull request to the merge queue Aug 29, 2026
Merged via the queue into QwenLM:main with commit 74735f6 Aug 29, 2026
75 of 76 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent Team: send_message reports a background task error for a teammate destination

3 participants