Skip to content

feat(web-shell): present structured shell execution results - #12311

Merged
ytahdn merged 12 commits into
QwenLM:mainfrom
ytahdn:codex/web-shell-command-card
Sep 21, 2026
Merged

ytahdn merged 12 commits into
QwenLM:mainfrom
ytahdn:codex/web-shell-command-card

Conversation

@ytahdn

@ytahdn ytahdn commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Organizes shell execution into Command, Output and Execution details, with command/output expanded by default, icon-only copy actions, explicit execution states, and 200px scrollable content regions. Running commands show elapsed time beside their status; supplied timeouts remain available in details.

Carries versioned structured shell results from execution through recording, ACP, SDK previews and Web Shell, so output and metadata are displayed without parsing a human-readable result envelope. Preserves model-facing content, terminal/text consumers, and string hook payloads, including full successful display text before PostToolUse. Applies bounded previews at downstream boundaries. Historical text and unknown versions retain a compatible fallback. Foreground completed results are structured; background acknowledgments and early returns such as pre-execution cancellation keep their existing string fallback. Producer failure text preserves the original human display; the scheduler keeps its legacy error-detail fallback for terminal and batch-hook consumers.

Why it's needed

The previous card repeated the command and mixed output with exit codes, empty errors and process metadata. Structured results make execution outcomes explicit without mistaking user output for metadata, while retaining the existing policy that grep/diff/test exit 1 can be a completed negative result.

Reviewer Test Plan

How to verify

  1. Run a foreground command that waits, then prints output and succeeds. Check running status/elapsed time, command and output expansion, timeout details, and the completed result. Ordinary intermediate stdout may remain unavailable until completion under the existing ACP transport.
  2. Run a command that writes stderr and exits 7; compare with grep returning 1. Check failure diagnostics for the former and a completed negative result for the latter. Check empty output, cancellation and timeout without invented success.
  3. Copy a multiline command and output independently; verify the copied text and temporary check icon. Expand long content and confirm internal scrolling and full-width content on narrow screens.
  4. Reopen completed results after daemon restart. An execution interrupted before a result was recorded must show the missing-result failure fallback rather than a fabricated result. Legacy records remain verbatim.
  5. For a successful output exceeding 32,000 characters, verify PostToolUse still receives text from the middle; history/recording and ACP remain bounded. Check terminal and exported transcript text compatibility.

Latest review fix verification (2026-09-21): 1,156 tests passed across the four affected core suites; 4 independent producer/recorder/scheduler/hook checks passed with mocked execution service/filesystem/hook runner, plus 1 Chromium command-card E2E using a mock daemon. Full build, bundle, workspace typechecks, formatting and lint passed. Root integration typecheck retains the local environment limitation below. These checks are not a fresh real-daemon or OS hook E2E run.

Latest follow-up on the updated main base: root npm run build exits 0; all workspace typechecks pass. On this local Node 22.14.0 environment, root integration typecheck still reports the process-registry string/NonSharedBuffer mismatch at line 773; the missing generated-template error is gone. The maintainer independently reported a clean full build and full typecheck on Node 24.18.1. This is not complete cross-platform validation.

Evidence (Before & After)

These are actual browser captures from the earlier local real-daemon validation, visually inspected again for this PR, not mocked-daemon screenshots or a fresh run of the final commit. Before uses the original frontend and global CLI 0.22.0; after uses this worktree's structured-result implementation. Commands and themes differ. The earlier running screenshot places elapsed time in details; the final UI moves it beside status. The earlier real-daemon recapture attempt was blocked by browser navigation policy; refreshed mock-daemon captures are separately labelled below. Screenshots demonstrate UI states, not Git backend operations.

Before: real health check After: real successful health check
Running: waiting for output Failed: exit 7

Restart evidence: restored success, restored failure, interrupted execution fallback. Completed structured fields matched after restart. Recovery required reopening the session; intermediate output for the interrupted command was not recorded. The separate E2E comment details these limits.

Tested on

OS Status
macOS ✅ Local real-daemon validation and scoped tests
Windows ⚠️ Not tested
Linux ⚠️ Not tested

Environment (optional)

Node.js 22.14.0; CLI/core from the isolated worktree; real daemon on 5328 and frontend on 5201. Existing export-template build artifacts were reused for the earlier real run because of the export bundle blocker. The earlier real-daemon screenshots used no mock; the supplemental 2026-09-21 captures below use a mock daemon.

Risk & Scope

  • Main risk or tradeoff: this changes the shell display contract across packages; version validation and text compatibility cover existing consumers. Maintainer review of the core contract is requested.
  • Not validated / out of scope: Windows/Linux, seamless reconnect, and additional intermediate-output transport. Latest hook verification is focused testing, not a new OS-level E2E run.
  • Breaking changes / migration notes: the internal shell display is now structured. Model-facing content and hook strings remain compatible; historical records require no migration. Legacy text is not parsed into guessed metadata.

Design: command card (English), 命令卡片(中文), structured results (English), 结构化结果(中文).

Follow-up review fixes: text-only exported documents render the complete fallback without empty category disclosures; terminated/cancelled/timed-out results do not show a synthetic exit code; resolved working-directory semantics are explicit; container cleanup notices remain visible in both compatible text and structured notices. Added assertions for the resolved directory, persisted output files and refused-promotion outcome. 385 core + 136 Web tests and 5 independent focused checks passed; formatting and lint passed. Container verification mocks the worker, not a real container runtime.

Supplemental browser captures below were refreshed on 2026-09-21 using Chromium and the existing command-card E2E with a mock daemon (1 test passed). They were visually inspected and show success/failure UI only; they do not prove real shell execution or the recorder/hook fixes. The earlier real-daemon evidence above remains separate.

Known deferred items: the structured ACP text/output fields share the 64KB budget, so daemon CLI long display text can be truncated earlier than the former string result. This is not full long-output display parity. F6 cosmetic cleanups and adding this UI to the dedicated visual-preview workflow remain follow-ups; existing browser regression tests are a separate harness.

Linked Issues

None.

中文说明

变更内容

将 Shell 执行整理为命令、输出和执行详情。命令及输出默认展开,使用纯图标复制按钮、明确的执行状态,以及最高 200px 的内部滚动内容区。运行耗时显示在状态旁,传入的超时设置可在详情中查看。

将版本化的结构化结果从执行端贯穿录制、ACP、SDK 预览和 Web Shell,直接展示输出与元数据,不解析人类可读的结果外壳。保留面向模型的内容、终端/纯文本消费者及字符串 hook 载荷,包括 PostToolUse 前完整的成功展示文本。在下游边界限制预览大小。历史文本及未知版本使用兼容回退。前台完成结果使用结构化格式;后台确认及执行前取消等提前返回保留既有字符串回退。生产端失败文本保持原有人类可读展示,调度器继续为终端和批次 hook 保留旧有错误详情回退。

为什么需要

原有卡片重复命令,并将输出、退出码、空错误和进程元数据混在一起。结构化结果能明确表达执行结果,避免将用户输出误识别为元数据,并沿用 grep/diff/test 退出码 1 可以表示已完成但结果为否的现有策略。

评审验证计划

如何验证

  1. 执行先等待、再输出并成功结束的前台命令。检查运行状态和耗时、命令及输出默认展开、超时详情和完成结果。现有 ACP 传输下,普通中间输出可能要到结束后才可见。
  2. 执行写入 stderr 并退出 7 的命令,与 grep 返回 1 对比。前者显示失败诊断,后者显示已完成但结果为否。检查空输出、取消和超时,不应虚构成功。
  3. 分别复制多行命令与输出,确认文本和短暂勾号反馈。展开长内容,确认内部滚动及窄屏下内容占满宽度。
  4. 重启 daemon 后重新打开已完成记录。尚未记录结果就被中断的执行,应显示缺失结果的失败回退,不应虚构结果。旧记录保持原文。
  5. 成功输出超过 32,000 字符时,确认 PostToolUse 仍收到中间文本,而历史、录制与 ACP 保持限额。检查终端和导出文本兼容性。

最新评审修复验证(2026-09-21):受影响的 4 个 core 测试文件共 1,156 项通过;4 项独立生产端、录制、调度器与 hook 验证通过,底层执行服务、文件系统和 hook runner 使用 mock;另有 1 项 Chromium 命令卡 E2E 使用模拟 daemon 通过。完整构建、bundle、workspace 类型检查、格式和 lint 通过。根集成类型检查仍有下述本地环境限制。这些结果不代表新一轮真实 daemon 或操作系统 hook E2E。

基于更新后 main 的本轮结果:根 npm run build 退出码 0,各 workspace 类型检查通过。本机 Node 22.14.0 下根集成类型检查仍报告 process-registry 第 773 行 string/NonSharedBuffer 类型不匹配,缺失生成模板错误已消失。维护者另在 Node 24.18.1 下报告完整构建及完整类型检查通过。尚未完成跨平台验证。

前后证据

以上图片来自此前本地真实 daemon 的浏览器验证,本次再次目视检查;不是模拟 daemon 截图,也不是最终提交的新一轮真机验证。修改前使用原前端与全局 CLI 0.22.0,修改后使用本 worktree 的结构化实现;命令及主题不同。早期运行态截图将耗时放在详情中,最终界面已移到状态旁。此前的真实 daemon 补拍被浏览器导航策略阻塞;本轮模拟 daemon 截图在下方单独标注。截图证明界面状态,不代表 Git 后端操作验证。

上方两组图片分别展示修改前后的真实健康检查,以及运行中等待输出和退出码 7 的失败状态。重启证据:恢复成功结果、恢复失败结果、中断执行回退。完成态结构化字段在重启前后保持一致;恢复需要重新打开会话,中断命令的中间输出未录制。单独的 E2E 评论详述这些限制。

测试平台

macOS:已进行本地真实 daemon 验证及针对性测试。Windows、Linux:未测试。

环境

Node.js 22.14.0;CLI/core 来自隔离 worktree,真实 daemon 使用 5328 端口,前端使用 5201。此前真机验证因导出包体积阻塞而复用了已有导出模板构建产物。截图没有使用模拟 daemon。

风险与范围

  • 主要风险或权衡:跨包修改了 Shell 展示契约,通过版本校验及文本兼容覆盖现有消费者。请求维护者评审核心契约。
  • 未验证或范围外:Windows/Linux、无缝重连及新增中间输出传输。最新 hook 验证属于定向测试,不是新一轮操作系统级 E2E。
  • 兼容与迁移:内部 Shell 展示改为结构化数据,模型内容及 hook 字符串保持兼容;旧记录无需迁移,也不会从文本猜测元数据。

设计文档包含同步的中英文版本:命令卡片英文、命令卡片中文、结构化结果英文、结构化结果中文。

本轮评审修复:仅有文本的导出文档展示完整回退,不渲染空分类;信号终止、取消和超时不显示可能由执行层补出的退出码;明确解析后的工作目录语义;容器清理提示同时保留于兼容文本及结构化 notices。补充了实际目录、输出文件和拒绝后台切换结果的断言。385 项 core、136 项 Web 和 5 项独立定向检查通过,格式和 Lint 通过。容器验证使用模拟 worker,未运行真实容器。

以下补充浏览器截图于 2026-09-21 使用 Chromium 和现有命令卡 E2E 重新采集,使用模拟 daemon,1 项测试通过并已目视检查。图片仅证明成功/失败 UI 展示,不证明真实 Shell 执行或本轮录制/hook 修复;与前述真机证据分开标注。

明确后续处理项:结构化 ACP 的 text/output 共用 64KB 预算,daemon CLI 长展示文本可能比原字符串结果更早截断,尚未做到长输出展示完全等价。F6 外观清理及接入专用视觉预览工作流留待后续;已有浏览器回归测试使用另一套入口。

关联问题

无。

@ytahdn

ytahdn commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

E2E report / 真机验证报告

Real local macOS daemon and real shell processes, captured on 2026-09-20 during implementation; screenshots are linked in the PR body. No mocked daemon was used. These captures precede the final hook compatibility fix and are not claimed as a fresh final-commit E2E run.

Scenario Observed result
Running Gate-waiting foreground command displayed running status, elapsed time and explicit timeout. Ordinary intermediate stdout was unavailable over the existing ACP path; the card showed Waiting for output.
Success Actual health request returned exit 0 and three output lines; long-running advisory appeared separately. Copy success icon was checked.
Failure Browser-submitted command slept 12 seconds, printed stdout/stderr and exited 7. The browser settled automatically without reload and displayed the failure and output.
Restart with completed results Success/failure structured fields matched after restart.
Restart during execution A 240-second command was interrupted after its 180-second heartbeat. After restart there was no active prompt or remaining probe process. The card showed the missing-result failure fallback, without fabricated output or exit code.

Limits: the initial success command was submitted from a separate real client and required browser reload, so it does not prove automatic live completion. Reconnection navigated home and required reopening the session. Intermediate output of the interrupted run was not recorded. Its restored step summary showed 3 seconds rather than the observed approximately 180 seconds, so that summary is not elapsed-time evidence. Existing export-template artifacts were reused for CLI startup because the full export renderer build exceeds its budget. A new screenshot attempt during PR creation was blocked by browser navigation policy.

Final hook fix verification: a 40,022-character successful stdout retains its middle sentinel; PostToolUse receives all 40,103 display characters including the saved-output notice. History/recording still cap each text field at 32,000 characters and ACP rawOutput remains within 65,536 UTF-8 JSON bytes. Four focused checks passed using actual producer/handler code with mocked execution service, filesystem and hook runner; this is not an OS hook E2E test. The 599-test core suite passed. Workspace typechecks passed; full build and root integration typecheck retain the blockers listed in the PR.

中文

2026-09-20 实现过程中使用 macOS 本地真实 daemon 和 Shell 进程验证,截图见 PR 正文,没有模拟 daemon。图片采集早于最终 hook 兼容修复,不代表最终提交的新一轮真机 E2E。

  • 运行中:等待门控文件的前台命令显示运行状态、耗时及明确超时。现有 ACP 链路未提供普通中间输出,因此卡片显示“等待输出”。
  • 成功:真实健康检查退出 0,展示三行输出,长运行提示单独呈现,检查了复制后的勾号。
  • 失败:浏览器提交的命令等待 12 秒、输出 stdout/stderr 后退出 7,页面无需刷新即自动进入失败态并保留输出。
  • 完成后重启:成功和失败记录的结构化字段与重启前一致。
  • 运行中重启:240 秒命令在 180 秒心跳后被中断。重启后没有活跃请求或残留探针进程,卡片显示缺失结果的失败回退,没有虚构输出或退出码。

限制:最初成功命令由另一个真实客户端提交,浏览器需要刷新,因此不能证明自动实时完成。重连曾回到首页,需要重新打开会话。被中断命令的中间输出没有录制;恢复后的步骤摘要显示 3 秒,与约 180 秒实际观察不符,不能作为耗时证据。因完整导出渲染包超限,启动 CLI 时复用了已有导出模板产物。本次创建 PR 时的新截图尝试被浏览器导航策略阻塞。

最终 hook 修复验证:40,022 字符成功 stdout 的中间标记完整保留,PostToolUse 收到包含文件提示的全部 40,103 字符展示文本。历史/录制文本字段仍限制为 32,000 字符,ACP rawOutput 仍不超过 65,536 UTF-8 JSON 字节。4 项针对性检查使用真实生产端/handler,底层执行服务、文件系统和 hook runner 使用 mock,不是操作系统 hook E2E。599 项 core 测试通过;各 workspace 类型检查通过;完整构建及根集成类型检查仍有 PR 中列明的阻塞。

Update: rebased onto main as f492336; PR now contains only the 55 feature files and is mergeable. Reran 599 core + 132 Web + 38 ACP tests: all 769 passed. The independent 4-check hook verification and full-build/typecheck results above were collected before this rebase; they were not rerun.

更新:已迁移到 main,提交 f492336;PR 仅包含 55 个功能文件,无合并冲突。重新执行 599 项 core、132 项 Web 和 38 项 ACP 测试,合计 769 项通过。上面的 4 项独立 hook 验证及完整构建/类型检查结果采集于此次 rebase 前,未重新执行。

@ytahdn
ytahdn force-pushed the codex/web-shell-command-card branch from c874b5e to f492336 Compare September 20, 2026 06:25
@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 Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real local environment, PR #12311

Verified f492336b26 (base be9ed41afd, 0 commits behind) on macOS 15 / Node 24.18.1, in two isolated worktrees built from scratch (pnpm install --frozen-lockfile + root npm run build).

Rig — everything is real except the model. Two production daemons, each serving its own built packages/web-shell/dist (not vite dev, not a mocked daemon): node packages/cli/dist/index.js serve --port 5311|5312 --workspace <ws> --web, each with an isolated QWEN_HOME. The only stub is a local OpenAI-compatible server on 127.0.0.1:5399 that returns scripted run_shell_command tool calls, so scenarios are deterministic. Every shell process, the ACP child, the SSE transport, chat recording, the export pipeline and the browser (Playwright/Chromium against the daemon origin) are real. Arm A = this PR, arm B = origin/main, same commands, same scripted calls.

Verdict

The feature works as described on a real daemon, and I found no correctness regression in any consumer I could exercise. Two statements in the PR description are stale and should be corrected, one CI job is red for a reason that is not this PR, and I have one Minor finding worth a decision before merge (exported documents), plus four nits.


1. The red Test (ubuntu-latest) job is inherited from main

packages/web-shell → client/live/messages.test.ts > Live Voice messages > are defined and used nowhere outside client/live fails with:

AssertionError: expected [ 'components/composer/AddMenu.tsx' ] to deeply equal []

I reproduced it on origin/main (be9ed41afd) with byte-identical output. AddMenu.tsx:684 calls t('live.open'), added by #12252; the guard test landed one commit later in #12305, so main is currently red. Full packages/web-shell suite, same machine, back to back:

arm result
main be9ed41 1 failed / 9,120 passed (9,121) — live/messages.test.ts
PR f492336 2 failed / 9,142 passed (9,144) — same one, plus BranchPickerPopover.test.tsx

BranchPickerPopover.test.tsx passes in isolation on both arms (141/141) and CI's run of this PR reported only the one failure, so it is full-suite load flake, not PR-induced. This PR adds no new test failures; main needs a separate fix (drop t('live.open') from AddMenu.tsx, or move the key out of the live. namespace).

2. Two stale claims in the PR description

Claim in the description Measured here
"Full build remains blocked by the export renderer budget (1,989,901 bytes versus 1,930,000)" Root npm run build exits 0. Document export renderer JS = 1,926,710 B (main: 1,916,801 B, so this PR adds 9,909 B) against warning 1,970,000 and cap 2,030,000 — #12295 raised the cap and #12305 shrank the bundle. Lint & Static is green on this PR, which runs the same gate.
"Root integration typecheck still reports the existing process-registry string/buffer mismatch and a missing generated export template" npm run typecheck (all workspaces + typecheck:integration) exits 0, zero error TS.

Please update the description — as written it reads as if the PR is build-blocked, which it is not.

3. Reviewer Test Plan — all five steps, real daemon

# Step Result
1 Foreground command that waits, then prints and succeeds Pass. Running + live elapsed (6s → 14s), Command and Output expanded by default, Timeout / Use default in details, then Succeeded. Waiting for output… until completion, as documented.
2 stderr + exit 7 vs grep exit 1; empty output, cancel, timeout Pass. exit 7 → Exited with code 7, stderr in Output; grep exit 1 → Completed with Exit code 1 in details (negative result, not a failure); true → Succeeded / No output; cancel mid-run → Cancelled + partial output + Signal: 15; timeout → Timed out + red error line + partial output. No invented success anywhere.
3 Copy command and output independently; expand long content Pass. Two icon-only buttons (Copy command, Copy output), clipboard read back byte-exact including embedded newlines. Content panes measured max-height: 200px; overflow-y: auto, scrollHeight 11,086 vs clientHeight 200. At 390×844 documentElement.scrollWidth == clientWidth == 390 (no horizontal page scroll) and cards are full width.
4 Reopen after daemon restart; interrupted execution Pass. Killed and restarted the daemon on the same QWEN_HOME: all six cards re-rendered with identical text. A run interrupted before its result was recorded shows Failed + "Tool result missing from saved history; the previous run likely ended before this tool completed" — no fabricated result.
5 >32,000-char successful output Pass. 204,000-char stdout: PostToolUse received the full 208,252-char display string (main: 208,254; the 2-byte delta is only the QWEN_HOME path inside the notice). ACP bounded it to 31,896 chars with [... truncated for ACP transport ...] spliced in the middle; card shows Output preview truncated and the persisted .output file under Output files.

4. Compatibility A/B against main (the part I would most want proven)

Surface Method Result
Model-facing ACP content 12/12 tool_call / tool_call_update events from GET /session/:id/transcript, normalized for PGID/paths/uuids/timestamps Identical. Only rawOutput changes: string → {"type":"shell_result","version":1,...}.
PostToolUse real hook script dumping stdin, 6 scenarios, both arms tool_response.returnDisplay is a string on both arms, equal modulo PGID/path.
PostToolBatch same tool_calls[].tool_response.result_display is a string on both arms, equal modulo PGID.
PostToolUseFailure same Unchanged shape, error string equal modulo PGID.
Non-interactive -o stream-json 4 scenarios, both arms tool_result blocks byte-identical (PGID only).
Terminal (Ink TUI) real pty, 120×40, 5 scenarios, both arms, final frame diffed Identical rendering. No [object Object].

5. Findings

F1 — Minor — exported HTML documents get a half-populated card. sanitizeResultPreview maps shell_result → { kind: 'text', text }, so the exported envelope carries no structured fields and ShellToolOutput runs with result === null. Measured on a real export rendered with this build's renderer:

  • Execution details contains only Timeout / Use default — no exit code, directory, signal, notices or output files.
  • A failed run puts the whole Command: / Directory: / Output: / Error: / Exit Code: / Signal: / Process Group PGID: envelope inside the Output pane — re-creating exactly the output/metadata mixing the card removes in the live UI.
  • The same run is labelled Succeeded live but Completed in the export, because result?.exitCode === 0 cannot be evaluated.

docs/design/structured-shell-results.md does say the v1 document schema "uses its sanitized text fallback", so the mechanism is intentional — but the visible result is a card that looks empty/inconsistent. Either carry the structured result through the document schema, or suppress the Execution details disclosure (and the failure-envelope Output) when result is null. See figure 6.

F2 — Minor — directory changed meaning and the (root) branch is dead. shell.ts sets directory: cwd (resolved absolute path), but ShellToolOutput renders result.directory === '(root)' ? t('shell.result.defaultDirectory') : result.directory. The producer never emits '(root)' — that is the legacy text label, built from this.params.directory || '(root)' at shell.ts:2835 — so shell.result.defaultDirectory is unreachable and the card always shows an absolute host path where main showed Directory: (root) or the requested relative subdirectory. Verified in the transcript: "directory":"/private/tmp/.../rig/ws". Either emit the requested-directory semantics, or drop the dead comparison.

F3 — Minor — Exit code 0 is shown for timed-out and cancelled runs. Measured: timeout → {outcome:'timed_out', exitCode:0, signal:15}; user cancel → {outcome:'cancelled', exitCode:0, signal:15}. The card then prints Exit code 0 in Execution details right next to Timed out / Cancelled. main never surfaced an exit code in either case (its display text was Command timed out after 2000ms… / Command cancelled by user.), so this is newly misleading. Suggest suppressing Exit code when signal is set or outcome is timed_out / cancelled. See figure 7.

F4 — Nit — three test-coverage gaps, found by mutation. 19 mutants over the new logic, 15 killed. Survivors:

Mutation Survives because
directory: cwd → directory: '' the toEqual assertion uses expect.any(String)
outputFiles: persistedOutputFiles ?? [] → [] the only assertion expects []
drop !wasPromoteRefused from the cancelled branch promote-refused is never exercised; it would be labelled Cancelled
drop result?.outcome === 'timed_out' from failed in ShellToolOutput equivalent — redundant with tool.status === 'failed' and result.error; no action needed

Both directory and outputFiles are correct in the live rig, so these are pinning gaps rather than defects — but they are exactly the two fields F2 is about.

F5 — Nit — container-execution-environment.ts:702. The "Container cleanup failed after tool execution" notice is still appended only if (typeof result.returnDisplay === 'string'), so with a structured shell result it is now silently dropped from the display (it survives in llmContent and error.message). Code read only — I did not exercise a container runtime.

F6 — Nit — small cleanups. shell.result.elapsed is added to both locales but referenced nowhere (dead weight in a byte-budgeted renderer); transcriptToMessages.ts re-implements isShellToolName as an inline regex in two places instead of importing the existing helper (same four names today, easy to drift); the error <pre> (styles.shellFailure) has no max-height/scroll unlike Command and Output (bounded in practice by the ACP budget, so cosmetic).

6. Test runs on this head

Suite Result
core: shell + shell-result + hookEventHandler + coreToolScheduler + toolResultDisplayCompaction 1,026 passed
cli: acp projection, ToolMessage, daemon-tui-adapter, event-adapter, export normalize/export-transcript-document, doctorCommand 347 passed
web-shell: ShellToolOutput, ToolGroup, transcriptToMessages 348 passed
sdk-typescript daemonUi / acp-bridge transcript-replay 388 / 71 passed
npm run typecheck (all workspaces + integration) exit 0
root npm run build exit 0
i18n all 26 new shell.result.* keys present in both en and zh-CN

7. Evidence

All captures are from the real daemons above; the "before" images come from the same rig running origin/main.

Successful command — before / after

Running command — before / after. main renders the raw tool input JSON as the card body; the PR suppresses it and shows Running with live elapsed time.

Long-run notice — before / after. This is the clearest demonstration of the PR's premise: process metadata leaves the stdout blob.

Outcomes: exit 7, grep exit 1, timeout, cancellation

Truncation and restart recovery

F1 — exported HTML transcript, before / after

F3 — Exit code 0 on a timed-out run

Narrow viewport and copy feedback

8. Not verified

Windows and Linux (macOS only). Live PTY ansiOutput streaming into the card — the current ACP path does not deliver ordinary intermediate stdout, so parseShellLiveOutput was never reached in this rig. Container execution environment (F5). The model was a local scripted stub, so nothing here exercises real model behaviour. Real-world rendering of an exported document still pins the renderer to the published 0.24.1 asset on unpkg; I rendered the export against this build's renderer instead.

Recommendation

Merge-ready on correctness, with a decision needed on F1 (exported documents) and, ideally, one-line fixes for F2 and F3. The red Test (ubuntu-latest) job should not block this PR — it needs a separate fix on main. Please also refresh the "build blocked" and "typecheck fails" paragraphs in the description.

中文说明

维护者真机验证报告 — PR #12311

在本地搭建真实环境验证了 f492336b26(基线 be9ed41afd,未落后 main)。环境:macOS 15 / Node 24.18.1,两个全新工作树,各自 pnpm install --frozen-lockfile + 根 npm run build。

装置说明 —— 除模型外全部为真。 两个生产 daemon,各自托管自己构建出的 packages/web-shell/dist(不是 vite dev,也不是 mock daemon):node packages/cli/dist/index.js serve --port 5311|5312 --workspace <ws> --web,各带独立 QWEN_HOME。唯一的桩是 127.0.0.1:5399 上的本地 OpenAI 兼容服务,用于返回脚本化的 run_shell_command 调用,以保证场景确定。Shell 进程、ACP 子进程、SSE 传输、会话录制、导出链路和浏览器(Playwright/Chromium 直连 daemon 源站)都是真实的。A 臂 = 本 PR,B 臂 = origin/main,同样的命令、同样的脚本调用。

结论

功能在真实 daemon 上与描述一致,在我能实测到的所有消费者上未发现正确性回归。但 PR 描述中有两处说法已过期;一个 CI 任务为红,原因不在本 PR;另有一个建议合并前拍板的 Minor 问题(导出文档),以及四个小问题。

1. Test (ubuntu-latest) 变红是从 main 继承来的

packages/web-shell 的 client/live/messages.test.ts > "are defined and used nowhere outside client/live" 失败:

AssertionError: expected [ 'components/composer/AddMenu.tsx' ] to deeply equal []

我在 origin/main(be9ed41afd)上逐字复现了同样的报错。AddMenu.tsx:684 使用了 t('live.open')(来自 #12252),而这道守卫测试晚一个提交才由 #12305 合入,所以 main 目前本身就是红的。同一台机器上前后跑完整 packages/web-shell 套件:

臂 结果
main be9ed41 1 失败 / 9,120 通过(9,121) —— live/messages.test.ts
PR f492336 2 失败 / 9,142 通过(9,144) —— 同上,外加 BranchPickerPopover.test.tsx

BranchPickerPopover.test.tsx 单独跑在两臂都通过(141/141),并且 CI 上本 PR 也只报了前一个失败,所以那是全量并发下的抖动,不是 PR 引入的。本 PR 没有新增任何测试失败;需要在 main 上单独修(把 t('live.open') 从 AddMenu.tsx 移走,或让该 key 离开 live. 命名空间)。

2. PR 描述中的两处过期说法

描述中的说法 实测
「完整构建仍被导出渲染包体积限制阻塞(1,989,901 字节,限额 1,930,000)」 根 npm run build 退出码 0。导出渲染器 JS = 1,926,710 字节(main 为 1,916,801,本 PR 增加 9,909),告警线 1,970,000、硬上限 2,030,000 —— #12295 已抬高上限,#12305 又缩小了包体。本 PR 的 Lint & Static 为绿,它跑的就是同一道门。
「根集成类型检查仍有既存的 process-registry 字符串/Buffer 类型不匹配及缺失生成模板错误」 npm run typecheck(全部 workspace 加上 typecheck:integration)退出码 0,error TS 计数为 0。

建议更新描述 —— 按现在的写法,读者会以为这个 PR 被构建阻塞,实际并没有。

3. 评审验证计划五步,真实 daemon 逐条实测

# 步骤 结果
1 先等待、再输出并成功结束的前台命令 通过。Running + 实时耗时(6s → 14s),命令与输出默认展开,详情里是 Timeout / Use default,结束后 Succeeded。结束前显示 Waiting for output…,与文档所述一致。
2 stderr + 退出 7 对比 grep 退出 1;空输出、取消、超时 通过。退出 7 → Exited with code 7,stderr 落在 Output;grep 退出 1 → Completed,详情里保留 Exit code 1(已完成但结果为否,不是执行失败);true → Succeeded / No output;运行中取消 → Cancelled + 部分输出 + Signal: 15;超时 → Timed out + 红色错误行 + 部分输出。没有任何一处虚构成功。
3 分别复制命令与输出;展开长内容 通过。两个纯图标按钮(Copy command、Copy output),剪贴板读回逐字节一致,含换行。内容区实测 max-height: 200px; overflow-y: auto,scrollHeight 11,086 对 clientHeight 200。390×844 下 documentElement.scrollWidth == clientWidth == 390(无横向滚动),卡片占满宽度。
4 重启 daemon 后重新打开;中断的执行 通过。用同一个 QWEN_HOME 杀掉并重启 daemon:六张卡片文本完全一致。结果尚未记录就被中断的执行显示 Failed + 「Tool result missing from saved history; the previous run likely ended before this tool completed」,没有虚构结果。
5 超过 32,000 字符的成功输出 通过。204,000 字符 stdout:PostToolUse 收到完整的 208,252 字符展示串(main 为 208,254,2 字节之差只是通知里的 QWEN_HOME 路径长度)。ACP 侧限到 31,896 字符,中部插入 [... truncated for ACP transport ...];卡片显示 Output preview truncated,落盘的 .output 文件出现在 Output files。

4. 与 main 的兼容性 A/B(我最想证明的部分)

面 方法 结果
面向模型的 ACP 内容 GET /session/:id/transcript 的 12/12 个 tool_call / tool_call_update 事件,归一化 PGID/路径/uuid/时间戳后比对 完全一致。只有 rawOutput 从字符串变成 {"type":"shell_result","version":1,...}。
PostToolUse 真实 hook 脚本转存 stdin,6 个场景,两臂 两臂的 tool_response.returnDisplay 都是字符串,除 PGID/路径外完全相同。
PostToolBatch 同上 两臂的 tool_calls[].tool_response.result_display 都是字符串,除 PGID 外相同。
PostToolUseFailure 同上 结构未变,error 字符串除 PGID 外相同。
非交互 -o stream-json 4 个场景,两臂 tool_result 块逐字节相同(仅 PGID 不同)。
终端(Ink TUI) 真实 pty,120×40,5 个场景,两臂,比对最终帧 渲染完全一致,没有 [object Object]。

5. 发现

F1 — Minor — 导出的 HTML 文档里卡片只填了一半。 sanitizeResultPreview 把 shell_result 映射成 { kind: 'text', text },导出信封里没有结构化字段,于是 ShellToolOutput 在 result === null 下渲染。用本次构建的渲染器渲染真实导出后实测:

  • Execution details 里只有 Timeout / Use default —— 没有退出码、目录、信号、通知和输出文件。
  • 失败的运行会把整段 Command: / Directory: / Output: / Error: / Exit Code: / Signal: / Process Group PGID: 外壳塞进 Output 区 —— 正好把卡片在实时界面里已经消除的「输出与元数据混杂」又复原了。
  • 同一次运行在实时界面是 Succeeded,在导出里变成 Completed,因为无法判断 result?.exitCode === 0。

docs/design/structured-shell-results.md 确实写了 v1 文档 schema「使用其清洗后的文本回退」,所以机制是有意为之 —— 但呈现出来就是一张看起来空掉、且与实时界面不一致的卡片。建议要么让结构化结果进入文档 schema,要么在 result 为空时不渲染 Execution details(以及失败态的外壳 Output)。见图 6。

F2 — Minor — directory 语义变了,且 (root) 分支是死代码。 shell.ts 写的是 directory: cwd(解析后的绝对路径),而 ShellToolOutput 渲染的是 result.directory === '(root)' ? t('shell.result.defaultDirectory') : result.directory。生产端永远不会给出 '(root)' —— 那是旧文本的标签,由 shell.ts:2835 的 this.params.directory || '(root)' 生成 —— 所以 shell.result.defaultDirectory 不可达,卡片总是显示宿主机绝对路径,而 main 显示的是 Directory: (root) 或请求的相对子目录。transcript 中实测为 "directory":"/private/tmp/.../rig/ws"。建议要么补上请求目录的语义,要么删掉这个死比较。

F3 — Minor — 超时和取消的运行会显示 Exit code 0。 实测:超时 → {outcome:'timed_out', exitCode:0, signal:15};用户取消 → {outcome:'cancelled', exitCode:0, signal:15}。卡片于是在 Timed out / Cancelled 旁边的 Execution details 里打出 Exit code 0。main 在这两种情况下从不展示退出码(其展示文本分别是 Command timed out after 2000ms… 和 Command cancelled by user.),所以这是新引入的误导。建议在 signal 非空或 outcome 为 timed_out / cancelled 时不显示 Exit code。见图 7。

F4 — Nit — 变异测试发现三处测试未钉住。 对新增逻辑做了 19 个变异,杀掉 15 个。存活的:

变异 存活原因
directory: cwd → directory: '' toEqual 断言用的是 expect.any(String)
outputFiles: persistedOutputFiles ?? [] → [] 唯一的断言期望就是 []
去掉 cancelled 分支里的 !wasPromoteRefused promote-refused 这条路径没有用例,它会被标成 Cancelled
去掉 ShellToolOutput 中 failed 的 result?.outcome === 'timed_out' 等价变异 —— 与 tool.status === 'failed'、result.error 冗余,无需处理

directory 和 outputFiles 在真机上都是正确的,所以这是钉不住而非缺陷 —— 但恰好就是 F2 涉及的那两个字段。

F5 — Nit — container-execution-environment.ts:702。 「Container cleanup failed after tool execution」这条通知仍然只在 typeof result.returnDisplay === 'string' 时追加,因此结构化 shell 结果下它会从展示中被静默丢掉(在 llmContent 和 error.message 里仍然保留)。仅为读代码所得,我没有跑容器运行时。

F6 — Nit — 零碎清理。 shell.result.elapsed 两种语言都加了但没有任何引用(在有字节预算的渲染器里是白白占体积);transcriptToMessages.ts 用内联正则重复实现了两处 isShellToolName,而不是直接引用已有的 helper(今天四个名字一致,但容易漂移);错误块的 <pre>(styles.shellFailure)没有像命令和输出那样的 max-height / 滚动(实际受 ACP 限额约束,属于观感问题)。

6. 本 head 上的测试运行

套件 结果
core:shell + shell-result + hookEventHandler + coreToolScheduler + toolResultDisplayCompaction 1,026 通过
cli:ACP 投影、ToolMessage、daemon-tui-adapter、event-adapter、导出 normalize / export-transcript-document、doctorCommand 347 通过
web-shell:ShellToolOutput、ToolGroup、transcriptToMessages 348 通过
sdk-typescript daemonUi / acp-bridge transcript-replay 388 / 71 通过
npm run typecheck(全 workspace + 集成) 退出码 0
根 npm run build 退出码 0
i18n 新增的 26 个 shell.result.* key 在 en 与 zh-CN 中都齐全

7. 证据

所有截图都来自上述真实 daemon;「修改前」的图来自同一装置运行 origin/main。图片依次为:成功命令前后对比、运行中前后对比(main 把工具入参 JSON 当作卡片正文渲染,PR 将其屏蔽并显示 Running 与实时耗时)、长耗时通知前后对比(最能体现本 PR 的价值:进程元数据离开了 stdout 区块)、四种结局(退出 7 / grep 退出 1 / 超时 / 取消)、截断与重启恢复、F1 的导出文档前后对比、F3 的超时 Exit code 0、窄屏与复制反馈。图片见上方英文部分。

8. 未验证

Windows 与 Linux(仅 macOS)。PTY 实时 ansiOutput 流入卡片 —— 当前 ACP 链路不投递普通中间 stdout,本装置中 parseShellLiveOutput 始终未被触达。容器执行环境(F5)。模型是本地脚本化的桩,因此本报告不涉及真实模型行为。真实世界中导出文档仍会去 unpkg 拉取已发布的 0.24.1 渲染器;我是用本次构建的渲染器渲染的导出件。

建议

正确性上已可合并,但建议先就 F1(导出文档)拍板,并顺手修掉 F2 与 F3(都是一行改动)。红色的 Test (ubuntu-latest) 不应阻塞本 PR —— 那需要在 main 上单独修。另请更新描述中「构建被阻塞」和「类型检查失败」两段。

@ytahdn

ytahdn commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Review follow-up in c34536c2fe (additive commit; no rebase):

  • F1 fixed: text-only document exports render the complete fallback without the live categorized card or inferred execution details. Kept the v1 document schema and its sanitization intact.
  • F2 fixed: retain resolved absolute execution-directory semantics; remove the unreachable (root) comparison and unused default-directory translations.
  • F3 fixed: hide exit code for cancellation, timeout and signal termination; preserve outcome and signal.
  • F4 covered: pin the actual directory, nonempty outputFiles and completed outcome when background promotion loses the race. No action for the equivalent mutant.
  • F5 fixed: cleanup warnings are appended to compatible text and structured notices, leaving stdout unchanged. Verification uses a mocked worker; no real container-runtime claim.
  • F6 deferred: cosmetic cleanup only; no further unrelated refactoring in this review cycle. The dedicated visual-preview workflow coverage gap is also a follow-up, separate from the existing component/browser regression tests.

Validation: the new assertions first failed on the old implementation (4 UI + 1 container case), then passed. The complete affected runs passed 385 core + 136 Web = 521 tests, plus 5 independent focused checks. Formatting, lint and whitespace checks passed. Two post-fix source/test/call-path audit passes found no further issue in this correction.

Root build now exits 0; all workspace typechecks pass. On local Node 22.14.0, root integration typecheck still fails at process-registry.ts:773 with string/NonSharedBuffer incompatibility; the generated-template error is gone. This is reported separately from the maintainer's successful full check on Node 24.18.1. Updated both languages in the PR description accordingly.

The PR body includes a new browser screenshot of fixed component fixtures, explicitly marked as fixed test data, not a daemon or real shell execution. Existing real-daemon screenshots remain labelled separately. Images are in the fork's independent asset branch, not the implementation branch.

Known deferred compatibility difference: structured ACP text and output share 64KB. Daemon CLI long display text may truncate earlier than the former string payload (40,021 characters became 32,667 in the targeted projection check). Normal Ink rendering parity does not cover this boundary. We retain the existing transport limit in this PR and record CLI long-output budget compatibility as a follow-up, rather than claiming exact parity.


中文:追加提交 c34536c2fe 已处理 F1–F5:导出纯文本回退、明确实际目录并删死分支、隐藏异常终止退出码、补强三处断言、恢复容器清理提示。F6 外观清理及专用视觉预览覆盖留待后续,避免继续扩大改动。

新增断言先在旧实现上失败(4 项 UI、1 项容器),修复后通过;完整相关测试 385 core + 136 Web = 521 项,另有 5 项独立验证通过。格式、Lint、空白检查通过;两轮修复后源码/测试/调用链复查未发现本次修正的新问题。容器验证使用 mock worker,没有声称真机容器测试。

完整构建已通过,各 workspace 类型检查通过。本机 Node 22.14.0 的根集成类型检查仍报 process-registry.ts:773 string/NonSharedBuffer 不兼容,缺失生成模板错误已消失;与维护者 Node 24.18.1 全量通过的结果分别说明。PR 中英文描述已更新。新增浏览器截图明确标为固定组件测试数据,不是真实 Shell/daemon;原有真机截图单独保留,图片不进入实现分支。

明确后续项:结构化 ACP 的 text/output 共用 64KB,daemon CLI 长文本可能提前截断(定向检查中 40,021 字符变为 32,667)。普通 Ink 显示一致性不覆盖此边界。本 PR 保留当前传输限额,将 CLI 长输出预算兼容记录为后续工作,不声称完全等价。

@ytahdn

ytahdn commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed the two failing scripts tests from the Ubuntu Node 22 test job in 6c2ee93. The new shellResult export was missing from the integration-test source aliases and the review capture loader's named source map. Added both mappings (4 lines); no product behavior changes.

Validation: reproduced the two failures before the fix; the same two test files now pass all 4 tests. Prettier, ESLint, diff check, and full build pass. All workspace typechecks pass. Local integration typecheck still reports the previously documented packages/acp-bridge/src/process-registry.ts:773 string/NonSharedBuffer mismatch on Node 22.14.0; full typecheck is therefore not claimed green. Remote CI for the new commit is pending.

已修复本轮 Ubuntu Node 22 Test 中的两项失败:新增 shellResult 导出时漏同步集成测试源码别名和 review capture loader 映射。仅补齐 4 行映射。修复前复现失败,修复后相同测试 4 项全部通过;格式、lint、构建通过。workspace 类型检查全部通过,本地集成类型检查仍存在此前记录的 process-registry.ts:773 类型错误。新提交的远端 CI 待确认。

@ytahdn

ytahdn commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@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 added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Sep 20, 2026
@wenshao

wenshao commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

Both sides added a distinct `paths` entry at the same spot in
integration-tests/tsconfig.json: this branch mapped
`@qwen-code/qwen-code-core/shellResult` and main's QwenLM#12304 mapped
`@qwen-code/qwen-code-core/telemetryConstants`. Keep both; the map has no
ordering constraint. Every other overlapping file auto-merged as an exact
union.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge resolution — PR #12311 ← main

Merge commit ac01af9d1d (PR head 6c2ee93ae0 × origin/main b85264be79). One conflict: integration-tests/tsconfig.json.

Root cause. Main's b3dee94b64 (PR #12304, transcript-replay timing) added a telemetryConstants entry to that file's paths map; this PR added shellResult at the same anchor, after transcriptRecords.

Textual, not semantic — adjacent insertions, no shared logic. Union (both targets exist; JSONC parses):

"@qwen-code/qwen-code-core/shellResult": ["../packages/core/src/utils/shell-result.ts"],
"@qwen-code/qwen-code-core/telemetryConstants": ["../packages/core/src/telemetry/constants.ts"],

Load-bearing.

  • Both explicit entries must stay. The map ends in a @qwen-code/qwen-code-core/* wildcard, so dropping one does not fail the typecheck — it silently resolves against dist instead of source. A future "dedupe" here is a break. Order is irrelevant.
  • The same union holds in the sibling registries (core exports, cli/sdk tsconfigs and vitest aliases, text-capture.tsx), all auto-merged with both keys and verified. acp-bridge's carry only telemetryConstants, correctly: the PR's new test there adds no imports. Otherwise the merge is an exact union both ways; only the conflicted file was edited.

Not verified — no build/typecheck/lint/tests run. All three sit in NON-conflicted files, so any break is CI's:

  1. The PR widens ToolResultDisplay with ShellResultDisplay. feat(acp-bridge): surface request and tool timing on paged transcript replay #12304's acp-bridge/src/transcript-replay.ts is the only base change touching resultDisplay in production code; it types it unknown, adds 261 pure insertions and never rewrites the pass-through, so no new consumer assumes string.
  2. core/src/tools/shell.test.ts: both sides added tests to one suite — main's per-shell description/schema length budgets vs the PR's structured returnDisplay. The PR touches neither description, so budgets should hold; unconfirmed.
  3. The PR's new acp-bridge test asserts toHaveLength(1); main's machine now emits timing frames, but only for ui_telemetry records, and that test feeds one tool_result.
中文说明

合并提交 ac01af9d1d(PR 头 6c2ee93ae0 × origin/main b85264be79)。唯一冲突:integration-tests/tsconfig.json。

根因:main 的 b3dee94b64(PR #12304,回放计时)在该文件 paths 新增 telemetryConstants,本 PR 在同一锚点(紧跟 transcriptRecords)新增 shellResult。

文本冲突,非语义冲突:仅相邻、无共同逻辑,取并集(见上)。

关键约束:

  • 两条显式映射都必须保留:末尾有 @qwen-code/qwen-code-core/* 通配项,删掉任一条不会报错,而是静默解析到 dist 而非源码。日后在此「去重」即为破坏。顺序无关。
  • 其余注册表(core exports、cli/sdk tsconfig 与 vitest 别名、text-capture.tsx)同样须为并集,已核实两侧键俱在。acp-bridge 只含 telemetryConstants 属正确:本 PR 该处新测试未加 import。其余为双向严格并集,只改了冲突文件。

未能验证:未运行构建/类型检查/lint/测试。三点均在未冲突文件,问题须由 CI 暴露:

  1. 本 PR 把 ShellResultDisplay 加入 ToolResultDisplay。feat(acp-bridge): surface request and tool timing on paged transcript replay #12304 的 acp-bridge/src/transcript-replay.ts 是生产代码中唯一触及 resultDisplay 的基线改动:声明为 unknown、261 行全新增、未改写透传,故无新消费者假定其为 string。
  2. core/src/tools/shell.test.ts:双方在同一套件加测试——main 的描述/schema 长度预算 vs 本 PR 的结构化 returnDisplay。本 PR 未动描述,预算应仍成立,未运行无法确认。
  3. 本 PR 新增 acp-bridge 测试断言 toHaveLength(1);main 现会发 timing 帧,但仅对 ui_telemetry 记录,此测试只喂一条 tool_result。

wenshao pushed a commit to wenshao/qwen-code that referenced this pull request Sep 20, 2026
@wenshao

wenshao commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Maintainer re-verification (round 2) — PR #12311 @ ac01af9d1d

Re-ran the same real-daemon rig against the updated head (ac01af9d1d, merge-base b85264be79; the earlier round was f492336b26 on be9ed41afd). Both worktrees were reinstalled from the lockfile and rebuilt from scratch; arm A = this head, arm B = b85264be79 (the PR's own merge-base, so the A/B is fair).

All five findings from the previous round are fixed and pinned by tests. I found no fix-induced regression in any consumer. Two small export-only nits remain from the F1 fix, and the deferred-compatibility note in the description overstates the real impact — my measured number is much smaller than the one quoted.

Findings from round 1 — re-verified on a real daemon

# Fix Verified how Result
F1 document exports drop the card and render the intact text fallback real daemon GET /session/:id/export, rendered offline with this build's export-transcript-document.js Fixed. No more empty Execution details, no Completed/Succeeded mismatch, no metadata envelope stuffed into an Output section.
F2 (root) comparison removed, absolute directory kept live card, details expanded Fixed. Directory renders the resolved absolute path; shell.result.defaultDirectory is gone from both locales.
F3 exit code hidden for cancel / timeout / signal live cards, all three states Fixed. Timeout → Timeout / Directory / PGID (no Exit code); SIGTERM → same; user cancel → same. Signal: 15 is still shown in all three.
F4 the three surviving mutants re-ran them on this head All three killed now (directory: '', outputFiles: [], dropping !wasPromoteRefused) — 780 core tests, 1 failure each.
F5 container cleanup notice reaches structured results code read + mutation Fixed at unit level. Removing the new else if (isShellResultDisplay(...)) branch fails the container boundary suite. I did not run a real container runtime, same caveat as yours.

I also mutated the three new fixes to confirm the new tests actually pin them, rather than just passing:

Mutation Result
disable the documentMode && !result early return 1 failed / 136
relax the exit-code guard back to result?.exitCode != null 3 failed / 136
delete the container structured-notice branch 1 failed / 46

Two small nits left over from the F1 fix (export only)

N1 — the command is printed twice in an exported document, three times on a failure. The new documentMode && !result branch renders <pre>{command}</pre> above the text fallback, but the collapsed row header already shows the command, and on a failure the fallback text itself opens with Command: …. docs/design/shell-command-card.md opens by naming command repetition as the problem this PR fixes, so it is worth closing the loop — dropping the extra <pre> (the header already carries it) or restoring getToolDescription(tool) there, as the base build showed, would do it.

N2 — a successful command with no output now exports as a card containing only the command. true → base exported Command: true / Directory: (root) / Output: (empty) / Exit Code: 0 / …; this head exports just true and nothing else. The live card correctly says No output; the export says nothing at all, so a reader cannot tell "ran and produced nothing" from "we lost the result". grep exit 1 is fine (Command exited with code: 1 still carries the status).

Neither blocks merge; both are one-line changes in the same branch you just added.

The deferred ACP-budget note is much smaller than stated

The description says "the structured ACP text/output fields share the 64KB budget, so daemon CLI long display text can be truncated earlier than the former string result (40,021 characters became 32,667 in the targeted projection check)". Measured end-to-end on the real daemon, same 204,000-char stdout, both arms:

display text reaching ACP ACP rawOutput frame
base b85264be79 32,000 chars (plain string) 32,613 B
PR ac01af9d1d 31,900 chars (text) + 31,896 chars (output) 65,536 B

So the display-text loss is 100 characters (0.3 %), not ~18 %: an upstream 32,000-char display cap already binds before the ACP budget does, and raising tools.truncateToolOutputThreshold to 200,000 changed neither arm. The real cost is the other half of that table — because text and output are near-duplicates for a successful command (text is the output plus the "saved to" line), the ACP frame for a large result doubles, from 32,613 B to exactly the full 65,536 B budget. That is worth a sentence in the description in place of the current one; if you ever want it back, giving output priority in allocatePayloadBudgets rather than an equal share would be the lever.

The process-registry.ts:773 typecheck failure is a local tree artifact

It does not reproduce here and it does not reproduce in CI:

  • npm run typecheck (all workspaces + typecheck:integration) on Node 24.18.1: exit 0, zero error TS.
  • tsc -p integration-tests/tsconfig.json on Node 22.23.2: exit 0.
  • CI runs npm run typecheck:integration inside Integration Tests (no-AK, No Sandbox) on a Node 22.x runner — pass on this head.

Line 773 is return result.stdout in a string-returning helper. spawnSync(...).stdout only widens to NonSharedBuffer when @types/node ≥ 22 is resolved at that import; this lockfile pins @types/node 20.19.1 at the root (TypeScript 5.8.3), which is what CI and both of my trees resolve. Your tree is almost certainly hoisting a newer @types/node. Nothing to fix in the PR — I'd just drop the caveat from the description.

Regression sweep on this head

Surface Method Result
Model-facing ACP content 20 tool_call / tool_call_update events across two sessions, normalized 20/20 identical. The one nominal diff is the per-workspace hash inside the persisted output-file path (ws vs ws-main).
PostToolUse / PostToolBatch real hook script, 6 scenarios, both arms 4/4 and 6/6 identical strings. Still typeof === 'string' on both arms.
-o stream-json 6 scenarios, both arms 6/6 equivalent. SCEN-BIG differs only by a 2-character head/tail splice offset, caused by the 2-character difference in the isolated QWEN_HOME path names.
Terminal (Ink TUI) real pty, 120×40, 6 scenarios, final frame diffed 6/6 identical.
Daemon restart / interrupted run kill + restart on the same QWEN_HOME Cards re-render identically; an interrupted run still shows the missing-result fallback.
Build root npm run build, both arms exit 0 both. Export renderer JS 1,927,689 B (base 1,917,548, +10,141) vs warning 1,970,000 and cap 2,030,000.
Tests core scoped (incl. the container boundary suite) 1,086 · cli scoped 347 · sdk 402 · acp-bridge 113 all passed
Full packages/web-shell suite both arms, same machine PR 1 failed / 9,197 (BranchPickerPopover), base 1 failed / 9,170 (ChatEditor) — different files, both pass in isolation on both arms (341/341). Load flake, not PR-induced; CI's Test (ubuntu) is green on this head.

The live/messages.test.ts failure I reported last round is gone: #12316 and #12319 fixed it on main, and every CI job on this head now passes except the still-running review-pr.

Verdict

The corrections are real, minimal and well pinned, and nothing else moved. From my side this is ready to merge; N1/N2 are cosmetic and export-only, and the two description sentences above are worth a touch-up. One housekeeping note: the branch is now 5 commits behind main (dfdbbace1f) again — no conflicts expected, just re-run the gate after the next merge.

中文说明

维护者复验(第 2 轮)—— PR #12311 @ ac01af9d1d

用同一套真机装置针对更新后的 head(ac01af9d1d,merge-base b85264be79;上一轮是 f492336b26 / be9ed41afd)重新验证。两个工作树都按 lockfile 重装并从零重建;A 臂 = 本 head,B 臂 = b85264be79(即本 PR 自己的 merge-base,保证 A/B 公平)。

上一轮的五个发现全部已修复,且都被测试钉住。 未发现任何修复引入的新回归。F1 的修复遗留两个仅影响导出文档的小问题;另外描述里那条「已知兼容性差异」的量级被高估了 —— 我实测到的数字远小于文中引用的。

上一轮发现的逐条复验

# 修复 验证方式 结果
F1 导出文档去掉卡片、改为完整文本回退 真实 daemon 的 GET /session/:id/export,用本次构建的 export-transcript-document.js 离线渲染 已修复。 不再有空的 Execution details,不再有 Completed/Succeeded 不一致,也不再把元数据外壳塞进 Output 区。
F2 去掉 (root) 比较,保留绝对目录 实时卡片,展开详情 已修复。 Directory 显示解析后的绝对路径;两种语言中的 shell.result.defaultDirectory 均已删除。
F3 取消 / 超时 / 信号终止时隐藏退出码 实时卡片,三种状态全测 已修复。 超时 → Timeout / Directory / PGID(无 Exit code);SIGTERM 同;用户取消同。三者都仍保留 Signal: 15。
F4 三个存活变异 在本 head 上重跑 三个全部被杀(directory: ''、outputFiles: []、去掉 !wasPromoteRefused)—— 780 项 core 测试,每个变异各挂 1 项。
F5 容器清理通知进入结构化结果 读代码 + 变异 单元层面已修复。 删掉新增的 else if (isShellResultDisplay(...)) 分支会让容器边界套件变红。我没有跑真实容器运行时,与你的说明一致。

另外对三处新修复做了变异,确认新增测试是真的钉住了行为,而不只是碰巧通过:

变异 结果
关闭 documentMode && !result 提前返回 1 失败 / 136
把退出码守卫放宽回 result?.exitCode != null 3 失败 / 136
删除容器结构化通知分支 1 失败 / 46

F1 修复遗留的两个小问题(仅影响导出)

N1 —— 导出文档里命令被打印两次,失败时三次。 新增的 documentMode && !result 分支在文本回退上方渲染了 <pre>{command}</pre>,但折叠行的表头本来就显示命令,失败时回退文本自身又以 Command: … 开头。docs/design/shell-command-card.md 开篇就把「重复命令」列为本 PR 要解决的问题,所以值得顺手闭环 —— 去掉那个多余的 <pre>(表头已经有了),或者像基线那样改回渲染 getToolDescription(tool),都可以。

N2 —— 成功但无输出的命令,导出后只剩一行命令。 true 在基线上导出的是 Command: true / Directory: (root) / Output: (empty) / Exit Code: 0 / …;本 head 只导出 true,再无其它。实时卡片正确显示 No output,而导出里什么都没有,读者无法区分「跑过且没有输出」和「结果丢了」。grep 退出 1 没问题(Command exited with code: 1 仍带出状态)。

两个都不阻塞合并,且都是你刚动过的同一个分支里的一行改动。

描述中那条「已知兼容性差异」的量级被高估了

描述写的是「结构化 ACP text/output 共享 64KB 预算,daemon CLI 的长展示文本可能比原先的字符串更早被截断(定向投影检查中 40,021 字符变成 32,667)」。在真实 daemon 上端到端实测,同样的 204,000 字符 stdout,两臂对比:

到达 ACP 的展示文本 ACP rawOutput 帧大小
基线 b85264be79 32,000 字符(纯字符串) 32,613 B
PR ac01af9d1d 31,900 字符(text)+ 31,896 字符(output) 65,536 B

也就是说展示文本只少了 100 个字符(0.3%),而不是约 18%:上游本来就有一道 32,000 字符的展示上限先生效,而且把 tools.truncateToolOutputThreshold 调到 200,000 后两臂都没有变化。真正的代价在表格的另一半 —— 成功命令下 text 与 output 近乎重复(text 就是输出再加一行「saved to」),于是大结果的 ACP 帧翻了一倍,从 32,613 B 涨到正好用满 65,536 B 预算。建议用这句话替换描述中现有的那句;若将来想把文本额度要回来,在 allocatePayloadBudgets 里让 output 优先于均分就是那个杠杆。

process-registry.ts:773 的类型错误是本地环境产物

它在我这里和 CI 里都复现不了:

  • Node 24.18.1 上 npm run typecheck(全 workspace 加 typecheck:integration):退出码 0,error TS 计数为 0。
  • Node 22.23.2 上 tsc -p integration-tests/tsconfig.json:退出码 0。
  • CI 在 Integration Tests (no-AK, No Sandbox) 里用 Node 22.x runner 跑 npm run typecheck:integration —— 本 head 上通过。

第 773 行是一个返回 string 的辅助函数里的 return result.stdout。只有当该处解析到 @types/node ≥ 22 时,spawnSync(...).stdout 才会放宽为 NonSharedBuffer;而本 lockfile 在根上钉的是 @types/node 20.19.1(TypeScript 5.8.3),CI 和我的两个树解析到的都是它。你的树几乎可以确定提升到了更新版本的 @types/node。PR 里没有需要改的东西 —— 建议直接把这条注意事项从描述里去掉。

本 head 上的回归扫查

面 方法 结果
面向模型的 ACP 内容 两个会话共 20 个 tool_call / tool_call_update 事件,归一化后比对 20/20 一致。 唯一名义上的差异是持久化输出文件路径里按 workspace 生成的哈希(ws 对 ws-main)。
PostToolUse / PostToolBatch 真实 hook 脚本,6 个场景,两臂 分别 4/4 与 6/6 完全相同,两臂都仍是 typeof === 'string'。
-o stream-json 6 个场景,两臂 6/6 等价。SCEN-BIG 只差 2 个字符的头尾拼接偏移,源自两个隔离 QWEN_HOME 路径名长度相差 2 个字符。
终端(Ink TUI) 真实 pty,120×40,6 个场景,比对最终帧 6/6 完全一致。
daemon 重启 / 中断的执行 同一 QWEN_HOME 上杀掉并重启 卡片重渲染一致;被中断的执行仍显示缺失结果回退。
构建 两臂根 npm run build 均退出 0。导出渲染器 JS 1,927,689 B(基线 1,917,548,+10,141),告警线 1,970,000、上限 2,030,000。
测试 core 定向(含容器边界套件)1,086 · cli 定向 347 · sdk 402 · acp-bridge 113 全部通过
packages/web-shell 全量套件 两臂,同一台机器 PR 1 失败 / 9,197(BranchPickerPopover),基线 1 失败 / 9,170(ChatEditor)—— 是不同的文件,且两臂单独跑都通过(341/341)。属于并发负载抖动,不是 PR 引入;CI 的 Test (ubuntu) 在本 head 上是绿的。

上一轮我报告的 live/messages.test.ts 失败已经消失:#12316 与 #12319 在 main 上修掉了它,本 head 上除仍在运行的 review-pr 外,全部 CI 任务通过。

结论

这几处修正真实、克制且钉得住,其它面没有移动。从我这边看可以合并;N1/N2 属于仅影响导出的观感问题,上面提到的两句描述值得顺手更新。另有一条事务性提醒:分支现在又落后 main 5 个提交(dfdbbace1f),预期无冲突,下次合并后重跑门禁即可。


Round-2 evidence (same rig; "before the fix" images are this PR at f492336b26):

F3 — exit code suppressed for timeout and cancellation

F1 — exported document, before and after the fix (N1 visible: the command appears twice)

N2 — grep exit 1 and a no-output success in the export, base vs this head

@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

⚠️ AutoFix round 5 ended without publishing a report — view run.

中文说明

⚠️ AutoFix 第 5 轮结束但未发布报告 —— 查看运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (7200000ms)) (attempt 1/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: timeout (7200000ms).

See the Qwen Autofix agent step logs for model/tool output.

中文说明

🤖 AutoFix 在完成前耗尽了时间(timeout (7200000ms))(第 1/100 次尝试)—— 将在下次扫描时重试。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/35520279908


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.1

Qwen Autofix and others added 2 commits September 20, 2026 19:35
)

- Documents: stop duplicating the command above the fallback text and
  label an empty legacy result as 'No output' (maintainer N1/N2)
- Escape control and bidi characters at every shell-card render site
  while keeping clipboard copies byte-exact
- Render a string rawOutput display instead of model-facing content so
  background-promotion and refusal messages stay visible
- Show a finished command's elapsed time from startTime/endTime
- Suppress the Command section when the legacy envelope already leads
  with the same command, keeping it once per expanded card
- Pin the wasCancelled status, the background timeout gates, the
  synthetic exit-zero headline and the status icons; freeze the
  running-elapsed fixture's clock
- Tag the card smoke test and wait for the SSE connection before
  driving live frames
- Drop the dead .expandedBash selector and align both design docs with
  the undisclosed live-frame transport for line/byte counts

Co-authored-by: Qwen-Coder <[email protected]>
The deterministic verification gate ran the core suite on a persistent
runner whose real ~/.gitconfig carries remote.pushDefault=gd, and the
three gitPush tests that resolve the push remote failed with
'fatal: gd does not appear to be a git repository'. gitEnv strips only
the GIT_CONFIG_* env overrides, so the code under test still honors the
host HOME config — the exact gap the file's hermeticEnv() helper
documents and every gitPull test already guards against.

Pass hermeticEnv() at all six gitPush call sites so host config can no
longer steer push resolution. Reproduced with a poisoned HOME
(remote.pushDefault=gd): the three gate failures appear on the pre-fix
tree and the file is 107/107 green after, in both clean and poisoned
environments.

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

Co-authored-by: Qwen-Coder <[email protected]>
…LM#12311)

The deterministic verification gate rebuilds core with a scoped
npm run build --workspace packages/core, but the review-address job
installs with QWEN_SKIP_PREPARE=1 and restores only the root and core
dist artifacts, so packages/browser-use/dist is absent and the core
build dies in copyBrowserUseAssets. The dependency on the built
browser-use runtime was only expressed through the root build's
ordering, leaving every scoped core build (the gate's, or a
developer's on a prepare-skipped install) broken.

Add a prebuild that stages the browser-use runtime only when its
dist/index.js sentinel is missing. The root build keeps building
browser-use first, so the prebuild is a no-op skip there and the
fallback fires exactly when the runtime is absent.

Probe matrix: scoped core build fails without the runtime pre-fix
(the gate rejection, reproduced locally), passes with the runtime
absent (fallback stages it), and passes unchanged with the runtime
present (skip path).

Co-authored-by: Qwen-Coder <[email protected]>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address-review round — PR #12311

Feedback triage

  • Reviews / inline comments / issue-level comments: none new since the last evaluation.
  • Failed checks — "Signal the reviewed fork PR: CANCELLED" (×21): workflow signal jobs cancelled by the harness, not code failures on this branch. Nothing actionable in the PR's code.
  • Same-run verification repair — "core rebuild failed on the agent-committed fix": the gate's scoped npm run build --workspace packages/core died in copyBrowserUseAssets because packages/browser-use/dist did not exist in the verify environment.

Root cause of the deterministic rejection

The rejection was environmental, not caused by the rejected commit (a test-only change): the review-address job installs with QWEN_SKIP_PREPARE=1 and restores only the repo-root dist/ and packages/core/dist artifacts, so packages/browser-use/dist is never present there. Core's build (scripts/build_package.js → copy_files.js) unconditionally stages the browser-use runtime when the bundled skill exists, so any scoped core build without that runtime fails. The dependency was expressed only through the root scripts/build.js ordering (browser-use builds first), which a scoped --workspace packages/core invocation never traverses. Reproduced locally before the fix: identical Browser-use runtime not found failure with packages/browser-use/dist absent.

Fix (commit d23096e)

packages/core/package.json gains a one-line prebuild that builds @qwen-code/browser-use only when its dist/index.js sentinel is missing. Scoped core builds are now self-sufficient; the root build still builds browser-use first, so the prebuild is a no-op skip on that path and adds no cost there. One file, one line; the manifest-scripts area class is already within this PR's own footprint (the PR earlier added the ./shellResult export to the same manifest).

Mutation probe (build wiring — no unit-test surface applies)

  • Guard absent + runtime absent → scoped core build fails (the gate rejection, reproduced locally pre-fix).
  • Guard present + runtime absent → scoped core build passes; the fallback staged packages/browser-use/dist.
  • Guard present + runtime present → scoped core build passes on the skip path.
  • No test files touched; nothing deleted, skipped, or weakened.

Verification

  • npm run build --workspace packages/core with packages/browser-use/dist removed — passed (the gate's exact failing command).
  • npm run build --workspace packages/core with packages/browser-use/dist present — passed (skip path).
  • npm run build (full) — passed, exit 0; working tree stayed clean apart from the intended manifest change.
  • npm run typecheck — passed, exit 0.
  • npm run lint — passed, exit 0.
  • npx prettier --check packages/core/package.json — passed.
  • Core package tests, mirroring the gate (npm run test --workspace packages/core -- --changed origin/main --passWithNoTests with the gate's vitest load clamps) under a gate-equivalent environment (env -i, writable HOME, no SANDBOX/QWEN_* variables): 29,295 passed, 10 skipped, 0 failed across 772 files.
    • Note: the same command fails inside this agent sandbox (155 failures in workflow-snapshot, config, agent-headless, editor, and four other files) because the sandbox provides a read-only $HOME and exports SANDBOX/QWEN_HOME; none of those files' subjects are in the PR diff, all pass with a writable clean environment, and the gate's env -i allowlist strips those variables on the runner.
中文说明

Address-review 轮次 — PR #12311

反馈分类

  • Reviews / 行内评论 / issue 级评论:自上次评估以来没有新增。
  • 失败检查 —— “Signal the reviewed fork PR: CANCELLED”(×21):这是被 harness 取消的工作流 signal 任务,不是本分支上的代码失败。PR 代码中没有可处理的内容。
  • Same-run verification repair —— “core rebuild failed on the agent-committed fix”:gate 执行的限定范围命令 npm run build --workspace packages/core 在 copyBrowserUseAssets 中失败,因为验证环境中不存在 packages/browser-use/dist。

确定性拒绝的根因

该拒绝是环境导致的,并非由被拒绝的提交(一个纯测试改动)引起:review-address 作业以 QWEN_SKIP_PREPARE=1 安装依赖,并且只恢复仓库根目录的 dist/ 和 packages/core/dist 构件,因此 packages/browser-use/dist 在该环境中从不存在。core 的构建(scripts/build_package.js → copy_files.js)在 bundled skill 存在时会无条件地装配 browser-use 运行时,所以任何缺少该运行时的限定范围 core 构建都会失败。该依赖此前仅通过根目录 scripts/build.js 的构建顺序(browser-use 最先构建)来表达,而限定范围的 --workspace packages/core 调用根本不会经过这一编排。修复前已在本地复现:在 packages/browser-use/dist 缺失时出现完全相同的 Browser-use runtime not found 失败。

修复(提交 d23096e)

packages/core/package.json 新增一行 prebuild:仅当 @qwen-code/browser-use 的 dist/index.js 哨兵文件缺失时才构建它。限定范围的 core 构建由此实现自举;根构建仍然最先构建 browser-use,因此在根构建路径上该 prebuild 是无操作跳过,不增加任何开销。改动为一个文件、一行;manifest 的 scripts 区域类别已处于本 PR 自身足迹之内(该 PR 此前已在同一 manifest 中添加过 ./shellResult 导出)。

变异探针(构建装配 —— 无适用的单元测试面)

  • 无防护 + 运行时缺失 → 限定范围 core 构建失败(即 gate 的拒绝,修复前已在本地复现)。
  • 有防护 + 运行时缺失 → 限定范围 core 构建通过;回退逻辑装配出了 packages/browser-use/dist。
  • 有防护 + 运行时存在 → 限定范围 core 构建在跳过路径上通过。
  • 未改动任何测试文件;没有删除、跳过或削弱任何测试。

验证

  • 删除 packages/browser-use/dist 后执行 npm run build --workspace packages/core —— 通过(即 gate 失败时使用的原始命令)。
  • packages/browser-use/dist 存在时执行 npm run build --workspace packages/core —— 通过(跳过路径)。
  • npm run build(完整构建)—— 通过,退出码 0;工作树除预期的 manifest 改动外保持干净。
  • npm run typecheck —— 通过,退出码 0。
  • npm run lint —— 通过,退出码 0。
  • npx prettier --check packages/core/package.json —— 通过。
  • core 包测试,与 gate 对齐(npm run test --workspace packages/core -- --changed origin/main --passWithNoTests,并施加 gate 的 vitest 负载钳制),在与 gate 等价的环境(env -i、可写 HOME、无 SANDBOX/QWEN_* 变量)下:29,295 通过,10 跳过,0 失败,共 772 个文件。
    • 说明:同一命令在本 agent 沙箱内会失败(workflow-snapshot、config、agent-headless、editor 等 8 个文件共 155 项),原因是沙箱提供只读 $HOME 并导出 SANDBOX/QWEN_HOME;这些文件的被测对象均不在本 PR 的 diff 中,在干净可写的环境下全部通过,且 gate 的 env -i 白名单会在 runner 上剥离这些变量。

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/模型 kimi-k3 · CLI 0.24.2

@wenshao

wenshao commented Sep 21, 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 #12311 does not currently have merge conflicts with main.

wenshao pushed a commit to wenshao/qwen-code that referenced this pull request Sep 21, 2026
@wenshao

wenshao commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification (round 3) — PR #12311 @ d23096e857

Re-verified the head that the three post-round-2 commits produced (69a6f7cd4a → af4e1217f1 → d23096e857), merge-base b85264be79 (unchanged since round 2). macOS 15 / Node 24.18.1 / pnpm 11.24.0.

Rig. Three git worktrees — main b85264be79, previous head ac01af9d1d, current head d23096e857 — each fully built and each running its own production daemon self-hosting its own packages/web-shell/dist (node packages/cli/dist/index.js serve --web), with an isolated QWEN_HOME and a scripted fake OpenAI endpoint that returns a deterministic run_shell_command per scenario marker. Real shell processes, real ACP/SSE transport, real Playwright captures, real export endpoint rendered offline against each arm's own export-transcript-document.{js,css}. Pre-PR ("legacy") records were produced by the main daemon and then replayed by the PR daemons from a copy of the same QWEN_HOME.

Verdict: mergeable. Everything the three new commits claim reproduces independently. Two new Minor issues, both cosmetic/completeness rather than correctness, one with a verified one-word patch. Nothing blocking.


1. Round-3 fixes — reproduced

Control/bidi escaping (R1-8 / R1-38). A real command whose argument carries U+202E … U+202C plus BEL. On ac01af9d1d the card renders the override live, so gnp.exe is displayed as exe.png — a textbook trojan-source spoof. On d23096e857 it renders as inert \u202e…\u202c.

bidi

Clipboard stays byte-exact. Mutating either copy handler to copy the sanitized string is killed by the suite (both mutants), so the "escape for display, copy raw bytes" contract is pinned.

N1 / N2 (my round-2 nits) — both fixed, confirmed in a rendered export. Left: ac01af9d1d repeats the command above the fallback (3 occurrences per turn: header, <pre>, and the envelope's own Command: line) and exports a successful-but-silent run as a bare command line. Right: d23096e857 prints it twice (header + envelope) and exports No output.

export

String rawOutput display (R1-50). Background-start now shows the human display string instead of the model-facing blob (id / pid / output-file / status-file paths).

background

Unadvertised win: pre-PR history renders better. Replaying a main-produced session through the PR daemon, the same persisted record stops dumping the whole Command:/Directory:/Output:/Exit Code:/PGID envelope into Output and shows the command once plus clean output. This is the single biggest visible improvement for existing transcripts and it is not called out in the PR description.

legacy

hermeticEnv() (af4e1217f1). Reproduced the gate failure rather than taking it on trust: with HOME poisoned by remote.pushDefault=gd, the pre-fix tree fails exactly 3/107 with fatal: 'gd' does not appear to be a git repository; the fixed tree is 107/107 in both the poisoned and the clean environment.

prebuild (d23096e857). Full matrix re-run locally, including the case the commit message does not cover — the repo's declared package manager:

runtime present guard command result
absent pre-fix npm run build in packages/core fail — Browser-use runtime not found, exit 1
absent fix npm run build in packages/core pass, stages packages/browser-use/dist
absent fix pnpm --filter @qwen-code/qwen-code-core run build pass, stages it (pnpm 11.24.0 does run prebuild)
present fix npm run build in packages/core pass, skip path — browser-use not rebuilt

Root npm run build also confirms the skip path: browser-use builds first, then core's prebuild no-ops.


2. New findings

F1 (Minor) — a pre-PR record whose display string is empty falls back to the model-facing envelope.
ShellToolOutput.tsx:43 gates the new string branch on typeof raw === 'string' && raw, so an empty display string ('', which is what a silent command persists) is treated as "no string" and the card falls back to extractText(tool) — the envelope. The new legacyOutputRepeatsCommand guard then also suppresses the Command panel, because the envelope starts with Command: <cmd>. Net effect: in one transcript, sibling cards render clean output while the silent one renders a raw envelope with no Command section and no copy button.

Verified candidate patch — one word, A/B/A on the running daemon:

-    typeof raw === 'string' && raw
+    typeof raw === 'string'

legacy-empty

The focused suites (145 tests) stay green both with and without the patch, so neither behaviour is pinned.

F2 (Minor) — the new finished-command elapsed is live-only; it disappears on reload.
Same session, same command: while the page stays open the card reads Succeeded 3s; after F5 it reads Succeeded. Cause is not in the component — the daemon's compacted replay emits one tool_call event per tool, already in its terminal status (a 6-tool session replays as exactly 6 events, zero in_progress), so the client has no start timestamp and formatElapsed returns ''. Not a regression (main never showed elapsed at all), but R1-39 is only half delivered, and the displayed duration is inconsistent between a live and a reloaded view of the same run.

elapsed

F3 (informational, not introduced here) — the trojan-source hardening covers the expanded card only.
The collapsed tool summary — the line a user sees before expanding anything, and the header line in exported documents — still renders model-supplied text with the raw override. Byte-identical on both arms, so this PR did not introduce it; flagging it because it is the surface the escaping was meant to protect.

collapsed

F4 (Minor, coverage) — 4 of the 6 new sanitizeControlChars call sites are unpinned.
12 targeted mutants against the round-3 code, 8 killed / 4 survived:

mutant result
legacyOutputRepeatsCommand → false killed
drop the finished-elapsed branch killed
drop the document No output label killed
drop the string-rawOutput branch killed
Command panel renders raw killed
Output segments render raw killed
copy handlers copy sanitized text (×2) killed
document fallback renders raw survived
notices render raw survived
result.directory renders raw survived
result.outputFiles renders raw survived

3. Regression & gate status on this head

  • Full packages/web-shell suite, same machine, both arms: head 349 files / 9207 tests, 0 failed; main b85264be79 348 / 9171, 0 failed. Round 2's 2 failures and main's own live/messages.test.ts red are both gone.
  • npm run build (root) exit 0; npm run typecheck (all workspaces + typecheck:integration) exit 0 on Node 24.18.1 — the description's remaining process-registry.ts:773 note still does not reproduce here.
  • eslint + prettier --check clean on all five files changed since ac01af9d1d.
  • All PR checks green; mergeStateStatus is BLOCKED only on REVIEW_REQUIRED.
  • Cancel/timeout headlines stay correct: a SIGTERM-killed run reads Failed / Signal 15 with no synthetic exit code, where main shows Exit Code: 0 next to Signal: 15.

signal

4. Checked and not reproduced

  • R1-30 (an empty shell display no longer collapsing to {type:'none'} in the TUI): drove the real TUI through @lydell/node-pty on both arms with a silent command. Final frames are the same — ✓ Shell true (Produce no output), one line, no empty box. I could not produce the symptom.

5. Still open, for the record

The autofix bot's round-2 comment defers 21 of its own review findings as "real and scoped". Two of them are visible in this run and worth a follow-up rather than a block:

  • R1-1 — exported documents of failed runs still render the model-facing envelope (Command:/Directory:/Exit Code:), while the live card for the same run shows clean output. A live/export inconsistency, not a correctness bug.
  • R1-31 — measured end to end in round 2: the display-text cost is ~0.3 %, not the 40,021→32,667 the description implies; the real cost is the ACP frame going to the full 64 KiB because text and output are near-duplicates.

Recommendation: merge. F1 is a one-word change worth taking first since it is already verified; F2, F3 and F4 are follow-ups.

中文说明

维护者复验(第 3 轮)—— PR #12311 @ d23096e857

针对第 2 轮之后新增的三个提交(69a6f7cd4a → af4e1217f1 → d23096e857)重新验证,合并基线 b85264be79(与第 2 轮相同)。macOS 15 / Node 24.18.1 / pnpm 11.24.0。

装置。 三个 git 工作树——main b85264be79、上一轮 head ac01af9d1d、当前 head d23096e857——各自完整构建,各自跑一个生产 daemon 自托管本树的 packages/web-shell/dist,配独立 QWEN_HOME 和一个按场景标记返回确定性 run_shell_command 的自写 fake OpenAI。真实 shell 进程、真实 ACP/SSE 链路、真实 Playwright 截图、真实导出端点并用各臂自己的 export-transcript-document.{js,css} 离线渲染。"旧格式"记录由 main 的 daemon 产生,再用同一份 QWEN_HOME 的副本交给 PR 的 daemon 回放。

结论:可以合并。 三个新提交声称的效果都独立复现。新发现两个 Minor,都属于观感/完成度问题而非正确性问题,其中一个已给出实测过的一词修复。无阻塞项。

1. 第 3 轮修复——已复现

控制字符/双向字符转义(R1-8 / R1-38)。 真实命令的参数里带 U+202E … U+202C 和 BEL。在 ac01af9d1d 上卡片按双向覆盖渲染,gnp.exe 被显示成 exe.png,是典型的 trojan-source 伪装;在 d23096e857 上渲染为惰性文本 \u202e…\u202c(图 01)。

剪贴板保持逐字节原样。 把任一 copy 处理器改成复制"已转义"文本,两个变异体都被测试杀掉——"显示转义、复制原字节"的契约是钉住的。

N1 / N2(我第 2 轮提的两个 nit)均已修复,并在真实渲染的导出文档中确认。 左:ac01af9d1d 在回退文本之上重复打印命令(每个回合共 3 次:标题行、<pre>、信封自带的 Command: 行),且把"成功但无输出"的运行导出成只有一行命令的空块。右:d23096e857 只出现 2 次(标题 + 信封),空输出导出为 No output(图 03)。

字符串 rawOutput 显示(R1-50)。 后台启动卡片现在显示人类可读的显示字符串,而不是带 id / pid / 输出文件 / 状态文件路径的面向模型文本(图 07)。

未被宣传的收益:历史记录渲染变好了。 把 main 产生的会话交给 PR 的 daemon 回放,同一条持久化记录不再把整段 Command:/Directory:/Output:/Exit Code:/PGID 信封塞进 Output,而是命令出现一次 + 干净输出。这是对存量会话最大的可见改善,PR 描述里没有提(图 04)。

hermeticEnv()(af4e1217f1)。 没有采信门禁日志,而是自己复现:把 HOME 用 remote.pushDefault=gd 污染后,修复前的树恰好 3/107 失败,报错签名完全一致(fatal: 'gd' does not appear to be a git repository);修复后的树在污染与干净两种环境下都是 107/107。

prebuild(d23096e857)。 完整矩阵本地重跑,并补上提交说明没覆盖的那一格——仓库声明的包管理器:

运行时 守卫 命令 结果
缺失 修复前 packages/core 里 npm run build 失败 —— Browser-use runtime not found,退出 1
缺失 修复后 packages/core 里 npm run build 通过,装配出 packages/browser-use/dist
缺失 修复后 pnpm --filter @qwen-code/qwen-code-core run build 通过并装配(实测 pnpm 11.24.0 会执行 prebuild)
存在 修复后 packages/core 里 npm run build 通过,走跳过路径,不重建 browser-use

根构建也确认了跳过路径:先构建 browser-use,随后 core 的 prebuild 空转。

2. 新发现

F1(Minor)—— 显示字符串为空的旧记录会回退到面向模型的信封。
ShellToolOutput.tsx:43 把新的字符串分支写成 typeof raw === 'string' && raw,于是空显示字符串('',正是静默命令持久化下来的值)被当成"没有字符串",卡片回退到 extractText(tool),也就是信封。而新加的 legacyOutputRepeatsCommand 守卫又会同时隐藏 Command 面板(因为信封以 Command: <cmd> 开头)。结果是:同一份会话里,兄弟卡片渲染干净输出,静默那张却是一段没有 Command 段、也没有复制按钮的裸信封。

实测过的候选修复(一个词,在运行中的 daemon 上做了 A/B/A):

-    typeof raw === 'string' && raw
+    typeof raw === 'string'

见图 05。聚焦套件(145 项)在打与不打这个补丁时都是全绿——两种行为都没有被测试钉住。

F2(Minor)—— 新增的"完成后耗时"只在实时视图生效,刷新即消失。
同一会话、同一条命令:页面保持打开时卡片显示 Succeeded 3s,按 F5 之后变成 Succeeded。根因不在组件里——daemon 的压缩回放对每个工具只发一条 tool_call 事件、且已经是终态(6 个工具的会话回放出的正好是 6 条,零条 in_progress),客户端拿不到起始时间戳,formatElapsed 返回 ''。这不是回归(main 从来就不显示耗时),但 R1-39 只完成了一半,而且同一次运行在实时视图与刷新后视图里显示的耗时不一致(图 06)。

F3(信息性,非本 PR 引入)—— trojan-source 加固只覆盖了展开后的卡片。
折叠态的工具摘要行——用户在展开任何东西之前先看到的那一行,以及导出文档里的标题行——仍然以原样渲染模型提供的文本。两臂逐字相同,所以不是本 PR 引入;提出来是因为它恰恰是转义想要保护的那个界面(图 02)。

F4(Minor,覆盖率)—— 新增的 6 个 sanitizeControlChars 调用点里有 4 个没被钉住。
针对第 3 轮代码做了 12 个定向变异,杀掉 8 个 / 存活 4 个:存活的是"文档回退分支不转义""notices 不转义""result.directory 不转义""result.outputFiles 不转义"。被杀掉的包括:legacyOutputRepeatsCommand 恒假、去掉完成耗时分支、去掉文档 No output 标签、去掉字符串 rawOutput 分支、Command 面板不转义、Output 分段不转义,以及两个"复制按钮改成复制转义后文本"的变异。

3. 本 head 的回归与门禁状态

  • 同机两臂跑完整 packages/web-shell 套件: head 349 个文件 / 9207 项,0 失败;main b85264be79 是 348 / 9171,0 失败。第 2 轮的 2 个失败、以及 main 自身 live/messages.test.ts 的红,都已消失。
  • 根 npm run build 退出 0;npm run typecheck(全部 workspace + typecheck:integration)在 Node 24.18.1 上退出 0——描述里残留的 process-registry.ts:773 说明在这里仍然复现不出来。
  • 自 ac01af9d1d 以来改动的 5 个文件,eslint 与 prettier --check 均干净。
  • 所有 PR 检查为绿;mergeStateStatus 显示 BLOCKED 仅因为 REVIEW_REQUIRED。
  • 取消/超时的标题依旧正确:被 SIGTERM 杀掉的运行显示 Failed / Signal 15,没有合成的退出码;而 main 会在 Signal: 15 旁边显示 Exit Code: 0(图 08)。

4. 查了但没有复现的

  • R1-30(空的 shell display 不再折叠成 {type:'none'},会在 TUI 里留下空框):用 @lydell/node-pty 在两臂上驱动真实 TUI 跑静默命令,最终帧一致——都是 ✓ Shell true (Produce no output) 一行,没有空框。这个症状我复现不出来。

5. 仍然未决,供记录

autofix bot 在第 2 轮评论里把它自己的 21 条评审发现标记为"属实且范围明确"后顺延。其中两条在本轮实测中可见,建议作为后续项而非阻塞项:

  • R1-1 —— 失败运行的导出文档仍然渲染面向模型的信封(Command:/Directory:/Exit Code:),而同一次运行的实时卡片显示的是干净输出。属于实时/导出不一致,不是正确性缺陷。
  • R1-31 —— 第 2 轮已端到端量过:展示文本的代价约 0.3%,而不是描述里暗示的 40,021→32,667;真正的代价是 ACP 帧涨到满额 64 KiB,因为 text 与 output 近乎重复。

建议:合并。 F1 是一个词的改动且已实测,值得先带上;F2、F3、F4 作为后续项。


Evidence: figures at wenshao/qwen-code@236dbbb06c524add571be5a6740a2aceb71277af under pr12311-r3/. Rounds 1–2 reports: #issuecomment-5748505797, #issuecomment-5750340118.

@ytahdn

ytahdn commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

…enLM#12311)

A persisted empty display string from a silent command is falsy, so the
card fell back to the model-facing envelope in content and the
legacyOutputRepeatsCommand guard then suppressed the Command panel,
rendering a bare envelope with no copy action. Gate the string branch on
the type alone so an empty string renders as empty output next to the
command.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round — PR #12311 @ 22f6db30d5

Round-3 maintainer verification (@wenshao, [ic:5754429976]) reported the PR
mergeable with one recommended in-PR fix (F1) and three follow-ups (F2–F4).

F1 (Minor) — empty display string fell back to the model-facing envelope — FIXED

A pre-PR record of a silent command persists an empty display string
(rawOutput: ''). ShellToolOutput.tsx gated the string branch on
typeof raw === 'string' && raw, so the empty string was treated as "no
string" and the card fell back to extractText(tool) — the
Command:/Directory:/Output:/Exit Code: envelope — and the
legacyOutputRepeatsCommand guard then also suppressed the Command panel,
leaving a bare envelope with no copy button.

Reproduced before fixing with a new focused test
(ShellToolOutput.test.tsx › treats an empty string rawOutput as authoritative empty output): on the pre-fix code the card rendered the
envelope (Directory: (root) present, no Copy command button) — the test
failed exactly as the report describes.

Fix (the maintainer's verified one-word patch): gate on the type alone,
typeof raw === 'string'. A persisted empty display string now renders the
Command section plus a No output label, matching its sibling cards. The
new test passes post-fix; since it fails with the pre-fix expression and
passes with the fix, it pins the branch (mutation probe: removing the fix
reverts the test to red).

Changes: ShellToolOutput.tsx (+1/−1), ShellToolOutput.test.tsx (+13).

F2 (Minor) — finished-command elapsed is live-only — deferred to follow-up

Root cause is not in the component: the daemon's compacted replay emits one
terminal tool_call event per tool with no in_progress/start timestamp,
so after reload formatElapsed has no inputs. The fix belongs in the replay
path (packages/acp-bridge/src/transcript-replay.ts), outside this PR's
web-shell footprint, and the maintainer explicitly assigned it as a
follow-up. Recorded in deferred-findings.json.

F3 (informational) — collapsed summary renders model text unsanitized — deferred

Byte-identical on main and this head; this PR did not introduce it. The
collapsed row (ToolGroup.tsx) and export headers are a separate surface
from the expanded card this PR hardens. Recorded in
deferred-findings.json per the maintainer's follow-up assignment.

F4 (Minor, coverage) — 4 of 6 new sanitize call sites unpinned — deferred

The surviving mutants (document fallback, notices, result.directory,
result.outputFiles rendering raw) are a coverage gap on round-3 code, not
a defect; the maintainer explicitly assigned it as a follow-up. Recorded in
deferred-findings.json.

Notes

  • R1-1 and R1-31 remain tracked in the deferred-findings issue from round 2;
    the maintainer confirmed both as follow-ups, not blockers.
  • The maintainer observed that this PR also improves rendering of pre-PR
    (legacy) transcript records — the biggest visible win for existing
    sessions — and that it is not called out in the PR description. Flagging
    for the author; the PR body is not this round's to edit.
  • The second issue-level comment ([ic:5754462890]) is a CI-bot status note
    (sandboxed verification running); no action.
  • No review threads are resolved by this round (the feedback arrived as one
    issue-level comment), and no inline replies are owed.

Verification

All commands were actually run locally on this head (22f6db30d5). The
sandbox caps any single command at ~120s and reaps detached processes, so
the monorepo-wide commands were run as equivalent per-package sequences in
dependency order; the deterministic gate re-runs the full commands post-push.

  • Reproduction probe (pre-fix): npx vitest run client/components/messages/tools/ShellToolOutput.test.tsx in packages/web-shell — 1 failed / 35 passed, failing exactly on the new F1 test (envelope rendered, no copy button), confirming the defect.
  • Post-fix: same command — 36/36 passed.
  • Focused related suites: ToolGroup.test.tsx, transcriptToMessages.test.ts, toolFormatting.test.ts — 389 passed.
  • Full packages/web-shell suite via 6 shards (npx vitest run --shard=i/6): 8229 passed, 0 failed across 349 files — except App.test.tsx, which alone exceeds the sandbox's 120s command cap and could not be run locally. It never uses an empty-string rawOutput (verified by grep), so the change cannot affect it; the maintainer's run of the same suite on this head was fully green (349 files / 9207 tests).
  • npm run build equivalent — every workspace built in the scripts/build.js order (generate, browser-use, core, channels, audio-capture, node-repl, acp-bridge, sdk-typescript, web-shell, web-templates, cli, generate-settings-schema, qwen-live, vscode-ide-companion, chrome-extension, both integrations) — all exit 0, with web-shell/web-templates/cli rebuilt after the fix.
  • npm run typecheck equivalent — tsc in every workspace with a typecheck script (acp-bridge, audio-capture, browser-use, chrome-extension, cli, core, node-repl, qwen-live, sdk-typescript, web-shell, both integrations) plus npm run typecheck:integration — all exit 0.
  • npm run lint equivalent — eslint --ext .ts,.tsx over packages/*, packages/channels, integrations, scripts, integration-tests, root vitest.config.ts and eslint-rules (patches/ is globally ignored by the eslint config) — all exit 0.
  • npx prettier --check on both changed files — clean.
  • Settings schema untouched (no settings source changed); generate-settings-schema run as part of the build sequence above — exit 0.
中文说明

Autofix 本轮处理 —— PR #12311 @ 22f6db30d5

第 3 轮维护者复验(@wenshao,[ic:5754429976])结论为可合并,并给出一条建议带入本
PR 的修复(F1)和三条后续项(F2–F4)。

F1(Minor)—— 空显示字符串回退到了面向模型的信封 —— 已修复

静默命令的旧格式记录持久化下来的显示字符串是空的(rawOutput: '')。
ShellToolOutput.tsx 把字符串分支写成 typeof raw === 'string' && raw,空字符串被
当成"没有字符串",卡片于是回退到 extractText(tool) —— 即
Command:/Directory:/Output:/Exit Code: 信封 —— 随后
legacyOutputRepeatsCommand 守卫又把 Command 面板一并隐藏,最终渲染成一段没有复制
按钮的裸信封。

先复现后修复:新增聚焦测试(ShellToolOutput.test.tsx › treats an empty string rawOutput as authoritative empty output),在修复前的代码上卡片渲染出了信封(出现
Directory: (root),且没有 Copy command 按钮),测试按报告描述精确失败。

修复(维护者已实测的一词补丁):只按类型判断,即 typeof raw === 'string'。
持久化的空显示字符串现在渲染为 Command 段 + No output 标签,与同会话的兄弟卡片
一致。新测试在修复后通过;由于它在旧表达式下失败、在修复后通过,它钉住了该分支
(变异探针:撤掉修复,测试回到红色)。

改动:ShellToolOutput.tsx(+1/−1),ShellToolOutput.test.tsx(+13)。

F2(Minor)—— 完成后的耗时只在实时视图显示 —— 顺延至后续

根因不在组件里:daemon 的压缩回放对每个工具只发一条终态 tool_call 事件,没有
in_progress/起始时间戳,刷新后 formatElapsed 没有输入。修复点在回放链路
(packages/acp-bridge/src/transcript-replay.ts),超出本 PR 的 web-shell 范围,且
维护者明确把它列为后续项。已记录进 deferred-findings.json。

F3(信息性)—— 折叠摘要行未转义模型文本 —— 顺延

main 与该 head 逐字节一致,并非本 PR 引入。折叠行(ToolGroup.tsx)与导出标题属于
本 PR 加固的展开卡片之外的另一个界面。按维护者的后续项安排记录进
deferred-findings.json。

F4(Minor,覆盖率)—— 新增 6 个 sanitize 调用点中 4 个未被钉住 —— 顺延

存活的变异体(文档回退、notices、result.directory、result.outputFiles 渲染原文)
属于第 3 轮代码的覆盖率缺口而非缺陷;维护者明确列为后续项。已记录进
deferred-findings.json。

说明

  • R1-1 与 R1-31 仍由第 2 轮起的 deferred-findings 跟踪;维护者确认二者均为后续项,
    不阻塞合并。
  • 维护者指出本 PR 还顺带改善了旧格式(pre-PR)会话记录的渲染——这是对存量会话最大
    的可见收益——但 PR 描述中没有提及。在此提示作者;PR 描述不在本轮的改动范围内。
  • 第二条 issue 级评论([ic:5754462890])是 CI 机器人的状态提示(沙箱验证运行中),
    无需处理。
  • 本轮没有可解决的评审线程(反馈以一条 issue 级评论形式送达),也没有需要回复的
    行内评论。

验证

以下命令都在本 head(22f6db30d5)上实际运行过。沙箱把单条命令限制在约 120 秒
并回收分离进程,因此跨整个 monorepo 的命令按依赖顺序拆成了等价的逐包序列;确定性
门禁在推送后会重新运行完整命令。

  • 复现探针(修复前):在 packages/web-shell 运行
    npx vitest run client/components/messages/tools/ShellToolOutput.test.tsx ——
    1 失败 / 35 通过,失败的正是新的 F1 测试(渲染出信封、无复制按钮),确认了缺陷。
  • 修复后:同一命令 —— 36/36 通过。
  • 相关聚焦套件:ToolGroup.test.tsx、transcriptToMessages.test.ts、
    toolFormatting.test.ts —— 389 通过。
  • packages/web-shell 完整套件(6 个分片,npx vitest run --shard=i/6):
    349 个文件共 8229 通过、0 失败 —— 除 App.test.tsx 外;该文件单独就超过
    沙箱 120 秒的命令上限,无法在本地跑完。它从不使用空字符串 rawOutput
    (已用 grep 验证),本改动不可能影响它;维护者在同一 head 上跑过该套件全绿
    (349 文件 / 9207 测试)。
  • npm run build 等价流程 —— 按 scripts/build.js 的顺序构建所有 workspace
    (generate、browser-use、core、channels、audio-capture、node-repl、acp-bridge、
    sdk-typescript、web-shell、web-templates、cli、generate-settings-schema、
    qwen-live、vscode-ide-companion、chrome-extension、两个 integrations)——
    全部退出码 0,其中 web-shell/web-templates/cli 在修复后重新构建。
  • npm run typecheck 等价流程 —— 在所有带 typecheck 脚本的 workspace
    (acp-bridge、audio-capture、browser-use、chrome-extension、cli、core、node-repl、
    qwen-live、sdk-typescript、web-shell、两个 integrations)运行 tsc,外加
    npm run typecheck:integration —— 全部退出码 0。
  • npm run lint 等价流程 —— 对 packages/*、packages/channels、integrations、
    scripts、integration-tests、根 vitest.config.ts 和 eslint-rules 运行
    eslint --ext .ts,.tsx(patches/ 被 eslint 配置全局忽略)—— 全部退出码 0。
  • 对两个改动文件运行 npx prettier --check —— 干净。
  • 未改动 settings 源(无需更新 schema);generate-settings-schema 已随上述构建
    序列运行 —— 退出码 0。

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/模型 kimi-k3 · CLI 0.24.2

@ytahdn

ytahdn commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round: no actionable feedback

No code changes were made in this round.

What was reviewed

  • Reviews / inline comments: none newer than the last evaluation (2026-09-21T02:04:15Z).
  • Issue-level comments: the only new item is an automated status comment from @qwen-code-ci-bot announcing that sandboxed verification is running. It is a progress notification from the verification workflow, not a request for changes, so there is nothing to address in code.
  • Failed checks: none. All 25 checks on the current head are green (22 SUCCESS, 3 SKIPPED).

Outcome

With no review feedback, no inline findings, and no failing checks, there is no in-scope work for this round. The branch is left untouched and nothing was committed.

中文说明

Autofix 本轮:无可处理的反馈

本轮未做任何代码修改。

已检查的内容

  • 评审 / 行内评论:自上次评估(2026-09-21T02:04:15Z)以来没有新的评审或行内评论。
  • PR 评论区:唯一的新条目是 @qwen-code-ci-bot 发布的自动状态评论,说明沙箱验证正在运行。这只是验证工作流的进度通知,并非修改请求,因此代码层面没有需要处理的内容。
  • 失败的检查:无。当前 head 上的全部 25 项检查均为通过状态(22 项 SUCCESS,3 项 SKIPPED)。

结论

由于没有评审反馈、没有行内发现、也没有失败的检查,本轮没有需要处理的工作。分支保持原样,未提交任何内容。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.2

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

Round 1 — COMMENT, 2 blockers remain (head 9b470cc2, base 1cc63cfe)

Progress since last analysis

Commit What it fixed
22f6db30 ShellToolOutput: empty-string display now treated as authoritative ✅
69a6f7cd Web-shell UI: control/bidi escaping, elapsed time, string-fallback render, smoke tag ✅
Various coreToolScheduler and chatRecordingService input side migrated to shellResultText() ✅
coreToolScheduler:6651 Error artifact now updates text: errorMessage ✅ (partial — see R1-34 below)

Blocker 1 — R1-1: text field carries LLM-facing content on exit-error / signal-termination path

File: packages/core/src/tools/shell.ts line 3157
Anchor: executionError.error && !timeoutSummary

executionError is built three ways:

  • timeout → error.message = timeoutSummary
  • spawn error (result.error) → error.message = result.error.message (raw ENOENT / EPERM etc.)
  • exit-error / signal-termination → error.message = typeof llmContent === "string" ? llmContent : returnDisplayMessage — the code comment explicitly labels this "model-facing response"

ShellResultDisplay.text is then:

executionError.error && !timeoutSummary
  ? executionError.error.message   // ← on exit-error path this IS llmContent
  : returnDisplayMessage

On the exit-error / signal path, text therefore carries the LLM-formatted output — truncation markers, "Model Context: …" prefixes, advisory text — directly to every web-shell card and shellResultText() caller. Fix: use returnDisplayMessage unconditionally.


Blocker 2 — R1-4: chatRecordingService output boundary still uses typeof … === "string"

File: packages/core/src/services/chatRecordingService.ts
Anchor: shellResultText(inputDisplay) !== undefined (line 2387, input side correctly migrated)
Unfixed line: 2440

The recorder_input boundary was correctly migrated. The recorder_output boundary at line 2440 was not:

// line 2440 — UNCHANGED, never matches ShellResultDisplay
...(typeof outputDisplay === 'string'
  ? [{ representation: 'display', value: outputDisplay }]
  : []),

Every shell output-stage observation silently produces an empty values array. Fix mirrors the input-side change at 2387: replace typeof outputDisplay === 'string' with shellResultText(outputDisplay) !== undefined.


Non-blocking (deferred per dev-bot, confirming open)

Id Note
R1-34 coreToolScheduler:6651 — text: errorMessage added ✅; outcome still shows pre-error value (e.g. completed) while text describes a failure. Cosmetic, not a crash.
R1-3 acp-tool-result-text-projection.ts:316 — allocatePayloadBudgets(…)! non-null assertion safe in practice (64 KB budget, skeleton ≈ 200 B), but should be guarded.

The 19 other CI-bot round-1 findings that dev-bot deferred are not re-listed; they stand from that review.

Comment thread packages/core/src/tools/shell.ts Outdated
type: 'shell_result',
version: 1,
text:
executionError.error && !timeoutSummary

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.

Blocker R1-1: On the exit-error/signal-termination branch, executionError.error.message was set above to typeof llmContent === 'string' ? llmContent : returnDisplayMessage (the comment reads 'Schedulers use error.message as the model-facing response'). So ShellResultDisplay.text here receives the LLM-formatted output — truncation markers, Model-Context prefixes, advisory text — rather than the user-facing returnDisplayMessage.

Fix: set text: returnDisplayMessage unconditionally (the error field already holds executionError.error.message for scheduler use).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed the producer in aacd8e5: text now always uses returnDisplayMessage, pinned for exit errors with/without output and signal termination. The scheduler override is intentionally retained to match the legacy createErrorResponse fallback (resultDisplay ?? error.message), including failure-hook context. Tests cover both legacy and structured scheduler results and failed batch hook normalization; both design languages explain the distinction.

const inputValues = () => [
...toolResultPartDiagnosticValues(message),
...(typeof inputDisplay === 'string'
...(shellResultText(inputDisplay) !== undefined

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.

Blocker R1-4 (sibling at line 2440 not migrated): This recorder_input boundary was correctly updated to shellResultText(). The recorder_output boundary at line ~2440 still reads typeof outputDisplay === 'string' and silently emits an empty values array for every ShellResultDisplay. All shell output-stage observations in downstream pipelines now lose their display content.

Fix: apply the same change — shellResultText(outputDisplay) !== undefined / shellResultText(outputDisplay)!.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in aacd8e5. recorder_output now uses shellResultText exactly as recorder_input does. Regression coverage checks empty, small and compacted 40,000-character results against the actual saved display. All three cases fail when restoring the old typeof-string check and pass with the fix. Persisted results were intact; this repairs output-boundary diagnostic visibility.

@ytahdn

ytahdn commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the latest review's two code findings and verified the failure-path compatibility end to end through the scheduler and hook handler.

  • R1-1: The producer now always retains its original human-facing display text. Tests cover exit 3 with/without output and signal termination. The scheduler intentionally retains its final error-text override: the pre-PR createErrorResponse already used resultDisplay ?? error.message, so removing that override would change terminal error details and failure-hook context. Both design languages now distinguish producer text from the scheduler error fallback.
  • R1-4: recorder_output now projects structured display text with the same helper as recorder_input. Tests cover empty, small and compacted 40,000-character inputs, comparing the output diagnostic with the actual saved display. This fixes diagnostic visibility; persisted tool results were not lost.
  • Correction to the sandbox report's Finding A: The claimed failed-PostToolBatch undefined → string change is not supported by the complete path. Base 1cc63cfe already has resultDisplay: resultDisplay ?? error.message. Independently captured actual scheduler batch payloads for legacy and structured results, then passed those captures through the real hook handler: both produce the same error string. Feeding undefined directly to the handler bypasses the scheduler fallback. Added legacy/structured scheduler and failed-batch normalization coverage rather than changing compatible behavior.
  • Documented that background acknowledgments and pre-execution cancellation retain their string fallback. Previously deferred suggestions remain deferred.

Validation: 1,156 tests passed across the four affected core suites; 4 independent boundary checks passed. Temporarily restoring both old production implementations made all 6 targeted regression cases fail; restoring the fixes made all six pass. Full build and bundle passed; the bundled CLI starts and reports 0.24.2. All workspace typechecks passed. Root integration typecheck still reports the previously documented process-registry.ts:773 string/NonSharedBuffer error on local Node 22.14.0. ESLint, Prettier and diff checks passed. Two self-audit passes and an independent incremental review found no further defect.

Browser evidence: 1 Chromium command-card E2E passed, using the existing mock daemon. Success/failure screenshots were visually inspected, uploaded to the separate asset branch, and replace the earlier supplemental fixtures in the PR description. These are UI checks, not real shell/OS hook E2E or proof of the recorder fix. The independent boundary checks use mocked execution/filesystem/hook runner; the scheduler and hook handler under observation are actual implementation code.

中文:已修复生产端失败展示文本混入模型外壳,以及录制输出边界漏掉结构化展示文本的问题。调度器保留旧有错误详情与失败 hook 上下文。沙箱报告关于失败 PostToolBatch 从缺省变为字符串的结论漏看了基线中的 resultDisplay ?? error.message 默认值;完整 scheduler→序列化→hook 链路验证旧、新结果最终字符串一致,因此补兼容测试而不修改正确行为。已同步中英文设计说明和提前返回/后台场景范围。

验证:1,156 项测试、4 项独立边界检查、1 项使用模拟 daemon 的 Chromium E2E 通过;撤回修复时 6 项回归测试全部失败,恢复后全部通过。构建、bundle、workspace 类型检查、lint/格式通过;根集成类型检查仍有已记录的本地 process-registry:773 类型错误。成功/失败截图已上传独立素材分支并更新 PR,明确仅作为模拟 daemon 的 UI 证据。

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

Round 2 — APPROVE (head cb50a750, base 0f2be9a4)

Round-1 blockers resolved

Id Fix Verified
R1-1 shell.ts: text field now uses returnDisplayMessage unconditionally — the conditional that routed exit-error / signal paths through executionError.error.message (LLM content) is removed ✅ Confirmed at line 3152: text: returnDisplayMessage
R1-4 chatRecordingService.ts: output boundary (recorder_output stage, ~2440) migrated to shellResultText() — mirrors the input-side fix ✅ Confirmed live at line 2443: shellResultText(outputDisplay) !== undefined

New test coverage (commit aacd8e52):

  • chatRecordingService.test.ts: it.each(['', 'small display', 'x'.repeat(40_000)]) drives both recorder_input and recorder_output observations and asserts display values present — covers the R1-4 regression directly
  • shell.test.ts: non-zero exit test parameterised over empty and non-empty output; asserts outcome: 'failed', text, and the full returnDisplay shape

Remaining open items (non-blocking, no change since round 1)

Id Status
R1-3 allocatePayloadBudgets(…)! non-null assertion still present; safe at 64 KB budget (skeleton ≈ 200 B) — deferred per maintainer measurement
R1-34 Error artifact keeps original outcome while text carries errorMessage; cosmetic only, deferred per dev-bot
19 CI-bot findings All remain in "deferred to next round" state; no new blockers in that set

@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 — second maintainer vote, with the one open question independently re-checked.

The only reason the 04:14 sandbox round was findings rather than merge-ready was Finding A (failed-PostToolBatch seeing undefined → string). The author's rebuttal checks out on the code: the scheduler's failure-path fallback resultDisplay: resultDisplay ?? error.message exists identically at base 0f2be9a4 (:1101) and head cb50a750 (:1105), so the complete path produces the same error string on both arms — the report's payload construction bypassed that fallback. The added legacy/structured scheduler and failed-batch normalization coverage in aacd8e52 pins it.

Everything else on the gate: CI green (review-pr queued is the daytime runner window), 12k+ targeted tests green at verify time, mutation matrix 0 survivors, and the remaining review-ledger items are all Suggestion-level under the autofix loop. Approving the code as read at cb50a750.

@ytahdn
ytahdn added this pull request to the merge queue Sep 21, 2026
Merged via the queue into QwenLM:main with commit eda1c1d Sep 21, 2026
70 of 71 checks passed
pull Bot pushed a commit to bhardwajRahul/qwen-code that referenced this pull request Sep 24, 2026
* feat(web-shell): present structured shell execution results

* fix(shell): preserve cleanup notices and clarify result fallbacks

* fix(test): sync shell result source mappings

* fix(web-shell): address shell command card review findings (QwenLM#12311)

- Documents: stop duplicating the command above the fallback text and
  label an empty legacy result as 'No output' (maintainer N1/N2)
- Escape control and bidi characters at every shell-card render site
  while keeping clipboard copies byte-exact
- Render a string rawOutput display instead of model-facing content so
  background-promotion and refusal messages stay visible
- Show a finished command's elapsed time from startTime/endTime
- Suppress the Command section when the legacy envelope already leads
  with the same command, keeping it once per expanded card
- Pin the wasCancelled status, the background timeout gates, the
  synthetic exit-zero headline and the status icons; freeze the
  running-elapsed fixture's clock
- Tag the card smoke test and wait for the SSE connection before
  driving live frames
- Drop the dead .expandedBash selector and align both design docs with
  the undisclosed live-frame transport for line/byte counts

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

* test(core): isolate gitPush tests from host git config (QwenLM#12311)

The deterministic verification gate ran the core suite on a persistent
runner whose real ~/.gitconfig carries remote.pushDefault=gd, and the
three gitPush tests that resolve the push remote failed with
'fatal: gd does not appear to be a git repository'. gitEnv strips only
the GIT_CONFIG_* env overrides, so the code under test still honors the
host HOME config — the exact gap the file's hermeticEnv() helper
documents and every gitPull test already guards against.

Pass hermeticEnv() at all six gitPush call sites so host config can no
longer steer push resolution. Reproduced with a poisoned HOME
(remote.pushDefault=gd): the three gate failures appear on the pre-fix
tree and the file is 107/107 green after, in both clean and poisoned
environments.

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

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

* fix(core): stage browser-use runtime on demand in core prebuild (QwenLM#12311)

The deterministic verification gate rebuilds core with a scoped
npm run build --workspace packages/core, but the review-address job
installs with QWEN_SKIP_PREPARE=1 and restores only the root and core
dist artifacts, so packages/browser-use/dist is absent and the core
build dies in copyBrowserUseAssets. The dependency on the built
browser-use runtime was only expressed through the root build's
ordering, leaving every scoped core build (the gate's, or a
developer's on a prepare-skipped install) broken.

Add a prebuild that stages the browser-use runtime only when its
dist/index.js sentinel is missing. The root build keeps building
browser-use first, so the prebuild is a no-op skip there and the
fallback fires exactly when the runtime is absent.

Probe matrix: scoped core build fails without the runtime pre-fix
(the gate rejection, reproduced locally), passes with the runtime
absent (fallback stages it), and passes unchanged with the runtime
present (skip path).

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

* fix(web-shell): treat empty string shell display as authoritative (QwenLM#12311)

A persisted empty display string from a silent command is falsy, so the
card fell back to the model-facing envelope in content and the
legacyOutputRepeatsCommand guard then suppressed the Command panel,
rendering a bare envelope with no copy action. Gate the string branch on
the type alone so an empty string renders as empty output next to the
command.

* fix(core): preserve shell display and recording diagnostics

* feat(web-shell): add session tool calls panel with prompt selection

* fix(web-shell): address tool calls review findings

* fix(web-shell): keep prompt text out of persisted dock state

* fix(web-shell): restore sender tool call identity and review fixes

* docs(web-shell): record real daemon sender verification

---------

Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Autofix <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
qwen-code-dev-bot added a commit to banned2054/qwen-code that referenced this pull request Sep 26, 2026
Keep both additive test blocks in normalize.test.ts: this PR's usage-metadata identity tests and main's structured shell output test (QwenLM#12311) were inserted at the same anchor inside describe('normalizeSessionData').
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+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants