Skip to content

perf(core): Lazy-load first-use dependencies - #7686

Merged
doudouOUC merged 5 commits into
QwenLM:mainfrom
doudouOUC:perf/lazy-first-use-dependencies
Jul 26, 2026
Merged

perf(core): Lazy-load first-use dependencies#7686
doudouOUC merged 5 commits into
QwenLM:mainfrom
doudouOUC:perf/lazy-first-use-dependencies

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR moves iconv-lite, @xterm/headless, and simple-git out of the ACP child's eager static import closure and loads each dependency once at its first real use. It preserves the synchronous package-root encoding helpers through a compatibility entry, uses asynchronous lazy variants on internal file-service paths, defers terminal construction until the PTY path is selected, and keeps Git service construction side-effect-free until an operation actually needs Git.

Narrow deferred Core runtime entries prevent namespace imports from retaining the synchronous encoding compatibility path. The ACP bundle guard now rejects any static path from the ACP runtime to these three packages while allowing dynamic-only chunks.

Why it's needed

Issue #7264 identified these dependencies as approximately 890 KiB of direct package input in every cold ACP child even though most sessions do not use non-UTF-8 codecs, PTY terminal replay, or Git worktree operations during bootstrap. On the measured prototype artifact, the ACP static closure decreased from 13,405,027 to 12,314,617 bytes and all three packages reached zero bytes in the static closure.

On the 2-vCPU reference host, 30 alternating cold pairs measured process-to-first-session P50 improving from 1877.7 ms to 1733.3 ms, channel.initialize P50 improving from 896.2 ms to 831.5 ms, and peak process-tree RSS P50 decreasing from 417.0 MB to 408.1 MB. These performance numbers apply to the recorded prototype artifact SHA-256 f0ac7edc7665752efac7b7bfbb4fb055ce2d8ef1a8ae5dd1af630305a2c84d28; the final commit was rebased onto current main and locally rebuilt and guarded, but was not presented as a fresh 30-pair remote rerun.

Reviewer Test Plan

How to verify

Build and bundle the CLI, then run the serve fast-path bundle check. The check should pass and a metafile traversal from the ACP runtime should attribute zero static-closure bytes to iconv-lite, @xterm/headless, and simple-git.

Read and write ordinary UTF-8 text, then read and write a GBK file while preserving its encoding metadata. UTF-8 should complete without the codec chunk; the non-UTF-8 operation should load the codec once and preserve the decoded text and encoded bytes, including existing BOM behavior.

Run a shell command through the PTY path and through the child-process fallback. PTY output and terminal replay should remain unchanged, an abort during first use should not spawn a process, and a terminal chunk failure should retain the existing child-process fallback policy.

Exercise repository detection, worktree create/diff/apply/cleanup, and a Git-backed extension operation. The Git package should load once on the first real operation, structured failure-returning methods should retain their failure contracts, and construction alone should not load Git.

Evidence (Before & After)

N/A — no user-visible or TUI changes.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux

Environment (optional)

Local verification used macOS 26.4.1 arm64, Node.js 22.22.3, and npm 10.9.8. It passed 461 focused Core tests, 374 focused CLI tests with one existing skip, 29 bundle-guard tests, targeted ESLint and Prettier checks, the CLI-only production build and bundle, the real startup bundle closure guard, and the full workspace typecheck.

Remote performance verification used Linux on a 2-vCPU host with 3.5 GiB RAM, no swap, and Node.js 22.23.1. It completed a smoke run, 30 alternating serial cold pairs, 30 alternating preheated pairs, concurrent first-session checks, telemetry-disabled startup, legacy single-session startup, and residual-process checks. The remote host did not have a Git executable, so it verified the simple-git dynamic chunk and factory there; real Git initialization and service behavior were verified locally.

Risk & Scope

  • Main risk or tradeoff: A missing or corrupt dynamic chunk now appears at the first operation that needs it instead of during process startup. Read paths retain their decoding fallback, shell execution retains its child-process fallback, and structured Git methods keep their existing failure-return semantics. Git diff/apply now acquire the lazy module and construct clients inside those existing diagnostic/structured failure envelopes, so setup failures that previously rejected are returned in the methods' existing error forms.
  • Not validated / out of scope: Windows was not manually exercised, the final rebased commit did not receive a fresh 30-pair remote benchmark, and replacing any of the three dependencies is out of scope.
  • Breaking changes / migration notes: No package-root API migration is required; retaining the synchronous encoding helper signatures and behavior is an intentional compatibility decision for external consumers. This leaves a small synchronous compatibility entry backed by iconv-lite, while the targeted bundle plugin prevents that entry from entering the ACP startup closure.

Linked Issues

Relates to #7264

中文说明

本 PR 做了什么

本 PR 将 iconv-lite@xterm/headlesssimple-git 移出 ACP 子进程的启动期静态导入闭包,并在首次真实使用时分别只加载一次。它通过兼容入口保留包根级同步编码辅助函数,通过异步懒加载变体处理内部文件服务路径,在选中 PTY 路径后才加载终端实现,并确保 Git 服务构造本身无副作用,直到实际 Git 操作才加载依赖。

窄化的延迟 Core runtime 入口避免 namespace import 保留同步编码兼容路径。ACP bundle guard 现在会拒绝 ACP runtime 到这三个包的任何静态路径,同时允许仅通过动态 import 到达的 chunk。

为什么需要

#7264 识别出这些依赖在每个冷启动 ACP 子进程中合计占用约 890 KiB 的直接包输入,尽管大多数会话在启动阶段不会使用非 UTF-8 编解码、PTY 终端回放或 Git worktree 操作。在已测 prototype artifact 上,ACP 静态闭包从 13,405,027 bytes 降至 12,314,617 bytes,三个包在静态闭包中的归因字节均降为 0。

在 2 vCPU 参考机器上,30 组交替冷启动配对测试显示:process-to-first-session P50 从 1877.7 ms 改善到 1733.3 ms,channel.initialize P50 从 896.2 ms 改善到 831.5 ms,进程树峰值 RSS P50 从 417.0 MB 降至 408.1 MB。这些性能数据仅对应已记录的 prototype artifact SHA-256 f0ac7edc7665752efac7b7bfbb4fb055ce2d8ef1a8ae5dd1af630305a2c84d28;最终提交已 rebase 到当前 main 并在本地重新构建和执行门禁,但没有被描述为一次新的远程 30 组配对复测。

Reviewer 测试计划

如何验证

构建并 bundle CLI,然后运行 serve fast-path bundle 检查。检查应通过,并且从 ACP runtime 出发遍历 metafile 后,iconv-lite@xterm/headlesssimple-git 在静态闭包中的归因字节应全部为 0。

先读写普通 UTF-8 文本,再读写一个保留编码元数据的 GBK 文件。UTF-8 路径应无需加载 codec chunk;非 UTF-8 操作应只加载一次 codec,并保持解码文本和编码字节不变,包括既有 BOM 行为。

分别通过 PTY 路径和 child-process fallback 运行 shell 命令。PTY 输出和终端回放应保持不变,首次使用期间发生 abort 时不应启动进程,终端 chunk 加载失败时应保留既有 child-process fallback 策略。

验证仓库识别、worktree 创建/diff/apply/cleanup 以及 Git 支持的扩展操作。Git 包应在首次真实操作时只加载一次,仅构造服务不应加载 Git,具有结构化失败返回值的方法应保留原有失败契约。

证据(修改前后)

N/A — 没有用户可见或 TUI 变化。

已测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux

环境(可选)

本地验证环境为 macOS 26.4.1 arm64、Node.js 22.22.3 和 npm 10.9.8。已通过 461 个聚焦 Core 测试、374 个聚焦 CLI 测试(另有 1 个既有 skip)、29 个 bundle guard 测试、目标 ESLint 与 Prettier 检查、CLI-only 生产构建与 bundle、真实启动 bundle 闭包门禁,以及全 workspace typecheck。

远程性能验证使用 Linux 2 vCPU、3.5 GiB RAM、无 swap、Node.js 22.23.1 的机器。已完成 smoke、30 组交替串行冷启动配对、30 组交替 preheated 配对、并发首次 session、关闭 telemetry、legacy 单 session 和残留进程检查。远程机器没有 Git 可执行文件,因此远程只验证了 simple-git 动态 chunk 和 factory;真实 Git 初始化与服务行为已在本地验证。

风险与范围

  • 主要风险或权衡:动态 chunk 缺失或损坏时,错误现在会在首次需要该依赖的操作中出现,而不是在进程启动时出现。读取路径保留解码 fallback,shell 执行保留 child-process fallback,具有结构化返回值的 Git 方法保留既有失败语义。Git diff/apply 现在会在这些既有的诊断/结构化失败包络内获取懒加载模块并构造客户端,因此此前会 reject 的初始化失败现在会以这些方法原有的错误形式返回。
  • 未验证 / 范围外:未手动验证 Windows,最终 rebase 后的提交没有重新执行一次远程 30 组配对 benchmark,替换这三个依赖不在本 PR 范围内。
  • Breaking changes / 迁移说明:包根级 API 无需迁移;保留同步编码辅助函数的签名与行为,是面向外部使用者的有意兼容性决策。这会留下一个由 iconv-lite 支持的小型同步兼容入口,而定向 bundle 插件会阻止该入口进入 ACP 启动闭包。

关联 Issue

Relates to #7264

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

E2E and performance report

Final commit 065e7c3de003c817439a30f2dae9a5eb35898b21 was rebuilt from current main and passed:

  • 461 focused Core tests.
  • 374 focused CLI tests, with 1 existing skip.
  • 29 bundle-guard tests.
  • Full workspace typecheck.
  • Targeted ESLint and Prettier checks.
  • CLI-only production build, bundle, and the real serve startup closure check. The guard reports no static ACP path to iconv-lite, @xterm/headless, or simple-git.

The 2C4G acceptance run applies to prototype artifact SHA-256 f0ac7edc7665752efac7b7bfbb4fb055ce2d8ef1a8ae5dd1af630305a2c84d28, built from baseline febb43bc9266cc7a3363539df87d90d752ad782c plus candidate 5. It completed a smoke run, 30 alternating serial cold pairs, 30 alternating preheated pairs, concurrent first sessions, telemetry-disabled startup, legacy single-session startup, and residual-process checks.

Scenario Metric Baseline P50 / P95 Candidate P50 / P95 P50 delta Candidate wins
Cold channel.initialize 896.2 / 915.5 ms 831.5 / 848.5 ms −64.7 ms 30/30
Cold POST /session 1273.8 / 1305.3 ms 1156.5 / 1181.1 ms −117.4 ms 30/30
Cold process → first session 1877.7 / 1921.0 ms 1733.3 / 1763.8 ms −144.4 ms 30/30
Cold peak process-tree RSS 417.0 / 451.4 MB 408.1 / 419.2 MB −8.9 MB 18/30
Preheated channel.initialize 895.3 / 926.3 ms 837.2 / 861.6 ms −58.1 ms 30/30
Preheated POST /session 90.0 / 94.2 ms 83.3 / 86.7 ms −6.7 ms 28/30
Preheated process → first session 3697.3 / 3723.0 ms 3666.0 / 3676.6 ms −31.3 ms 30/30
Preheated peak process-tree RSS 430.5 / 433.1 MB 403.0 / 419.3 MB −27.5 MB 19/30

The remote host had 2 vCPUs, 3.5 GiB RAM, no swap, and Node.js 22.23.1. It had no Git executable, so remote first-use verification covered the simple-git dynamic chunk and factory; a real Git repository initialization and the full Git service paths passed locally.

The prototype artifact reduced the ACP static closure from 13,405,027 to 12,314,617 bytes and attributed zero static bytes to all three target packages. The final rebased commit retains the zero-byte condition through the production bundle guard; its latency numbers are not being represented as a fresh 30-pair rerun.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head fc99f59, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Fixed the Ubuntu test failure in ac96c57.

The lazy simple-git loader now sees the complete named-export shape in the extension manager test mock, matching the production module contract and the repository's other loader-aware mock. This restores the two extension tests that CI reported without changing production behavior.

Validation:

  • npm run build
  • npm run typecheck
  • cd packages/core && npx vitest run src/extension/extensionManager.test.ts src/extension/github.test.ts src/utils/load-simple-git.test.ts (191 passed)

@doudouOUC
doudouOUC marked this pull request as ready for review July 25, 2026 00:19
@doudouOUC
doudouOUC enabled auto-merge July 25, 2026 00:19
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@doudouOUC doudouOUC self-assigned this Jul 25, 2026

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review — perf(core): lazy-load first-use dependencies

Overall this is a clean, well-scoped change. The lazy-loader pattern (module-scoped single-flight promise + CommonJS interop unwrap) is consistent across all three packages, the sync encoding API is preserved via the sync-file-encoding compatibility module, and the bundle guard + metafile-based test make the invariant hard to regress. Static value imports of iconv-lite, @xterm/headless, and simple-git are all gone from the source (only import type remains), and the async read/write GBK paths and loader edge cases are covered by tests. Nice work.

A few non-blocking observations below; none of them block landing.

Correctness / behavior

  • The read-side fallback correctly folds a failed loadIconvLite() into the existing UTF-8-replacement path (with the warn), and the write side rejects rather than corrupting bytes — matches the stated failure contract.
  • Loader rejections are cached for the process lifetime (??= on a rejected promise). This is intentional per the design doc (a missing bundled chunk can't recover), so no change requested — just calling it out for reviewers.

Minor / maintainability

  • Some inline notes below on duplication and a couple of small error-handling scope changes.

Nit

  • On the non-UTF-8 write path, prepareTextFileContentAsync calls prepareTextFileContent twice, so CRLF normalization runs twice before the iconv retry. Negligible (rare path), just noting.


const debugLogger = createDebugLogger('SYNC_FILE_ENCODING');

export function decodeBufferWithEncodingInfo(full: Buffer): FileReadResult {

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.

The decode logic here is now a near-verbatim copy of decodeBufferWithEncodingInfoAsync in fileUtils.ts (BOM → valid-UTF-8 → chardet+iconv → UTF-8 fallback). Two copies of the same branching will drift over time — a future fix/edge-case in one won't reach the other.

Given the sync/async split is inherent (dynamic import() is async), full dedup is awkward, but you could at least factor out the shared non-iconv parts (detectBOM/decodeBOMBuffer/bomEncodingToName/isValidUtf8/detectEncodingFromBuffer sequencing) into a helper that takes the resolved iconv module as a parameter, so only the load step differs. Not blocking, but worth a follow-up.

try {
const { simpleGit } = await loadSimpleGit();
const worktreeGit = simpleGit(worktreePath);
const base =

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.

Behavior change worth confirming is intentional: resolveBaseline() / getCurrentBranch() were previously computed outside the try, so an error there propagated to the caller. They're now inside the try, so such an error is swallowed into the "Error getting diff: …" string return. Same pattern applies to applyWorktreeChanges (baseline/merge-base resolution moved inside the try). This is probably fine / arguably an improvement, but it does change the contract from "throws" to "returns a diagnostic string" for those sub-steps.

// against the *correct* directory.
const probe = new GitWorktreeService(this.targetDir);
const root = (await probe.getRepoTopLevel()) ?? this.targetDir;
const root = findGitRoot(this.targetDir) ?? this.targetDir;

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.

Swapping new GitWorktreeService(...).getRepoTopLevel() (git rev-parse --show-toplevel) for findGitRoot() removes an eager simple-git construction here, which is good. Note the two aren't strictly equivalent: findGitRoot walks up for a .git entry and returns that directory as-is, whereas --show-toplevel returns the real (symlink-resolved) working-tree root. Under a symlinked repo path the resulting .qwen/worktrees directory could differ from before. For the stale-sweep fast-bail this is almost certainly harmless (the fs.access just no-ops on mismatch), but flagging in case symlink-normalization was relied upon.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR — re-running the gate at the current head.

Template looks good ✓

Problem: this is a measured, observed problem, not a theoretical one. Issue #7264 (candidate 5) tracks iconv-lite, @xterm/headless, and simple-git as ~890 KiB of direct package input in every cold ACP child, and the PR carries concrete bundle-closure and 2-vCPU latency measurements. Real and well-evidenced.

Direction: aligned. Cold-start latency and RSS are things users actually feel, and this is one bounded candidate inside a tracked, gated effort — not a solution looking for a problem. CHANGELOG has no direct reference, but the startup-performance area is clearly relevant.

Size: this touches core paths, so the breakdown matters — 647 production logic lines, 290 test lines, 160 docs lines (35 files). Because production logic is ≥500 lines on a core change, this carries a maintainer-awareness flag per our core-module policy. It is a perf change, not a refactor, so there is no hard block — but the merge call belongs to a maintainer rather than auto-approval. (Under the 1000-line large-PR advisory, so no split suggestion.)

Approach: the scope feels right for the goal. The interesting constraint is iconv-lite: its two public encoding helpers are synchronous, so a naive lazy import would be an API break. The compatibility module + the targeted esbuild tree-shake plugin (marking only the Core-root re-export edge side-effect-free) is the minimum machinery that removes the package from the ACP closure without changing the public API, and the narrow CLI runtime entries avoid retaining the whole Core namespace. I don't see a materially simpler path that still satisfies candidate 5. Every edit traces back to the stated goal — no drive-by churn.

Moving on to code review. 🔍

中文说明

感谢贡献!在当前 head 上重新跑门禁。

模板完整 ✓

问题:这是一个已测量、已观测到的问题,而非理论性问题。Issue #7264(候选 5)指出 iconv-lite@xterm/headlesssimple-git 在每个冷启动 ACP 子进程中合计约 890 KiB 的直接包输入,PR 中附带了具体的 bundle 闭包与 2-vCPU 延迟测量数据。真实且证据充分。

方向:对齐。冷启动延迟与 RSS 是用户真实能感受到的,且这是一个有边界、属于已跟踪门禁工作的候选,不是"为问题找方案"。CHANGELOG 无直接引用,但启动性能这个方向显然相关。

规模:触及核心路径,因此需要拆分统计——647 行生产逻辑、290 行测试、160 行文档(35 个文件)。由于核心改动的生产逻辑 ≥500 行,按核心模块策略带有"维护者关注"标记。它是 perf 而非 refactor,所以没有硬阻断——但合并决定权归维护者,而非自动批准。(低于 1000 行大 PR 建议线,故不建议拆分。)

方案:范围对目标而言合理。最关键的约束是 iconv-lite:它的两个公开编码辅助函数是同步的,朴素懒加载会破坏 API。兼容模块 + 定向 esbuild tree-shake 插件(仅把 Core 根级 re-export 边标记为无副作用)是既能把该包移出 ACP 闭包、又不改动公开 API 的最小机制;窄化的 CLI runtime 入口避免保留整个 Core 命名空间。我没有看到更简单、同时仍能满足候选 5 的路径。每处改动都能追溯到既定目标——没有顺手夹带的改动。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal: for this problem I would (1) write a small memoized loader per package (promise ??=, CJS-default unwrap, shape validation), (2) keep a synchronous compat facade for iconv-lite's public encoding helpers and mark only the Core-root re-export edge side-effect-free so esbuild tree-shakes it from the ACP entry, (3) load @xterm/headless inside the already-async execute() right before the PTY path, re-checking the abort signal after the await, (4) make GitWorktreeService construction side-effect-free with a lazy getGit(), and (5) extend the ACP bundle guard to reject static paths to all three packages. Narrow CLI runtime entries would replace the full-namespace dynamic imports.

Comparison with the diff: the PR does exactly this. No simpler path was missed.

Findings — no critical blockers. Two observations:

  1. isGitRepository() calls loadSimpleGit() twice — once through getGit() (for the SimpleGit instance) and once directly (for CheckRepoActions). This is correct because the loader memoizes, but a single destructure from getGit() + the module would be marginally cleaner. Not blocking.

  2. cleanupWorktrees merged try-catch — branch deletion and worktree prune now share one try-catch with the getGit() call. Previously prune ran independently of branch-listing failures. In practice both are best-effort cleanup and the loader failure case makes the old separation moot, so this is fine.

Everything else is clean: the loaders are uniform and defensive, the abort-during-load race is handled and tested, the sync compat module is proven to agree with the async path (table test), the config.ts simplification to findGitRoot avoids loading simple-git for a startup filesystem walk, and the bundle guard tests cover both rejection and dynamic-only allowance.

Files changed (30 of 35 shown)
File What changed
docs/design/lazy-first-use-dependencies.md Design doc: motivation, closure measurements, failure audit, verification plan
esbuild.config.js Tree-shake plugin marking the sync-file-encoding re-export side-effect-free from Core index
packages/cli/src/cli.ts Narrow deferred-core-runtime import replaces full Core namespace
packages/cli/src/serve/core-runtime.ts New narrow re-export entry for serve-path Core symbols
packages/cli/src/serve/fs/workspace-file-system.ts Switches to async encode/decode variants
packages/cli/src/serve/run-qwen-serve.ts CoreRuntime type and loader point at narrow entry
packages/cli/src/serve/workspace-registration-store.ts Deferred import via narrow entry
packages/cli/src/ui/utils/export/collect.ts Deferred import via narrow entry
packages/cli/src/utils/cleanup.ts Deferred import via narrow entry
packages/cli/src/utils/deferred-core-runtime.ts New narrow re-export entry for CLI-path Core symbols
packages/core/src/config/config.ts Startup worktree sweep uses lightweight findGitRoot instead of GitWorktreeService
packages/core/src/extension/extensionManager.test.ts Mock adds CheckRepoActions for loader compat
packages/core/src/extension/github.ts Git operations load simple-git via loader at first use
packages/core/src/index.ts Re-exports sync encoding helpers from compat module
packages/core/src/services/fileSystemService.ts Async encode path with lazy iconv-lite; sync prepareTextFileContent accepts optional iconv
packages/core/src/services/gitWorktreeService.ts Side-effect-free construction; lazy getGit(); all git ops await the loader
packages/core/src/services/shellExecutionService.ts xterm loaded after PTY selection; abort re-check; Terminal threaded through
packages/core/src/services/worktreeCleanup.ts Git loaded via loader at first real operation
packages/core/src/utils/encoding.ts Extracted isUtf8CompatibleEncoding (no iconv-lite dependency)
packages/core/src/utils/fileUtils.ts Async decode path with lazy iconv-lite; exports BOM/UTF-8 helpers for compat module
packages/core/src/utils/iconvHelper.ts Removed isUtf8CompatibleEncoding (moved to encoding.ts)
packages/core/src/utils/load-iconv-lite.ts New memoized loader with CJS unwrap and shape validation
packages/core/src/utils/load-simple-git.ts New memoized loader with CJS unwrap and shape validation
packages/core/src/utils/load-xterm-headless.ts New memoized loader with CJS unwrap and shape validation
packages/core/src/utils/read-text-range.ts Import isUtf8CompatibleEncoding from encoding.ts
packages/core/src/utils/sync-file-encoding.ts New sync compat facade backed by iconvHelper (tree-shakeable from ACP)
scripts/check-serve-fast-path-bundle.js Bundle guard adds three forbidden static packages
scripts/tests/serve-fast-path-bundle-check.test.js Guard tests: static rejected, dynamic allowed
packages/core/src/utils/load-iconv-lite.test.ts Loader tests: single-flight, CJS unwrap, shape rejection
packages/core/src/utils/load-simple-git.test.ts Loader tests: single-flight, CJS unwrap, shape rejection
…and 5 more files

CI test evidence

All checks on fc99f5975 are complete. No failures. macOS/Windows unit tests and integration tests were skipped (normal for fork PRs — secrets not available). A collaborator (gwinthis) independently verified all three lazy-load paths end-to-end on Linux with a real build + tmux.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
precheck-pr / precheck ✅ success
Classify PR ✅ success

Real-scenario testing

N/A — no user-visible or TUI changes. The PR is a startup-path optimization; the collaborator's Linux E2E report (GBK encode/decode, PTY shell, Git worktree via the real bundle) covers the behavioral surface.

中文说明

代码审查

独立方案: 针对此问题,我会 (1) 为每个包写一个小的记忆化加载器(promise ??=、CJS default 解包、形状验证),(2) 为 iconv-lite 的公开同步编码辅助函数保留同步兼容 facade,并仅将 Core 根级 re-export 边标记为无副作用以便 esbuild 从 ACP 入口 tree-shake 掉,(3) 在已经异步的 execute() 中、PTY 路径之前加载 @xterm/headless,await 后重新检查 abort 信号,(4) 使 GitWorktreeService 构造无副作用,使用懒 getGit(),(5) 扩展 ACP bundle 门禁以拒绝到三个包的任何静态路径。窄化的 CLI runtime 入口替代全命名空间动态导入。

与 diff 对比: PR 完全这样做了。没有遗漏更简单的路径。

发现——无关键阻断项。两个观察:

  1. isGitRepository() 调用了两次 loadSimpleGit()——一次通过 getGit()(获取 SimpleGit 实例),一次直接调用(获取 CheckRepoActions)。因为加载器是记忆化的,所以正确,但从 getGit() + 模块做一次解构会更简洁。不阻断。

  2. cleanupWorktrees 合并了 try-catch——分支删除和 worktree prune 现在与 getGit() 调用共享一个 try-catch。之前 prune 独立于分支列表失败运行。实际上两者都是尽力清理,加载器失败的情况下旧的分离已无意义,所以没问题。

其余均干净:加载器统一且稳健,加载期间的 abort 竞态已处理并有测试,同步兼容模块被证明与异步路径一致(表格测试),config.ts 简化为 findGitRoot 避免在启动文件系统遍历中加载 simple-git,bundle 门禁测试覆盖了拒绝和仅动态允许两种情况。

CI 测试证据

fc99f5975 上的所有检查已完成。无失败。macOS/Windows 单元测试和集成测试被跳过(fork PR 正常——secrets 不可用)。协作者 (gwinthis) 在 Linux 上用真实构建 + tmux 独立验证了所有三个懒加载路径。

真实场景测试

不适用——无用户可见或 TUI 变更。PR 是启动路径优化;协作者的 Linux E2E 报告(GBK 编解码、PTY shell、通过真实 bundle 的 Git worktree)覆盖了行为面。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review across every stage, but this is a 647-production-line core change, so the core-module policy caps the bot at a defer and hands the merge call to a maintainer. This is a policy cap, not doubt.

Stepping back: this is genuinely good work. The motivation is real and measured — not "this could theoretically be slow" but concrete bundle-closure and 30-pair cold-start numbers against a tracked issue (#7264 candidate 5). The implementation matches my independent proposal exactly: three uniform memoized loaders, a sync compat facade for the one public-API constraint (iconv-lite), abort-safe xterm loading, side-effect-free Git construction, and a bundle guard that makes the optimization durable. Nothing in the diff is trying too hard.

The checks I want before trusting a perf PR are all satisfied: the sync/async encoders are proven to agree (table test), the abort-during-load race is handled and tested, the tree-shake plugin is proven load-bearing, and the A/B measurement shows the three packages at zero bytes in the ACP closure with a consistent cold-path latency win (30/30 pairs). A collaborator independently verified all three first-use paths end-to-end on Linux with a real bundle. If I had to maintain this in six months I'd thank the author — the design doc alone makes the intent and failure audit clear.

The only reason this isn't a 5/5 approve is scope-by-policy: a core change of this size gets a human's merge sign-off rather than an auto-approval, and that gate is doing exactly what it's supposed to.

⏸️ Deferring to @wenshao — the review is clean; per the core-module policy the merge call is yours rather than an auto-approval. Nothing outstanding from my side.

中文说明

置信度:3/5 —— 各阶段审查均干净,但这是一处对核心、647 行生产逻辑的改动,按核心模块策略,机器人封顶为"转交维护者",合并决定权交给维护者。这是策略封顶,而非质疑。

退一步看:这是真正高质量的工作。动机真实且有测量——不是"理论上可能慢",而是针对已跟踪 issue(#7264 候选 5)的具体 bundle 闭包与 30 组冷启动数据。实现与我的独立方案完全一致:三个统一的记忆化加载器、为唯一的公开 API 约束(iconv-lite)保留的同步兼容 facade、abort 安全的 xterm 加载、无副作用的 Git 构造、以及使优化持久的 bundle 门禁。diff 中没有过度设计。

信任一个 perf PR 所需的检查全部满足:同步/异步编码器被证明一致(表格测试)、加载期间被 abort 的竞态已处理并有测试、tree-shake 插件被证明真正起作用、A/B 测量显示三个包在 ACP 闭包中为 0 字节且冷路径延迟有稳定收益(30/30 对)。协作者在 Linux 上用真实 bundle 独立验证了所有三个首次使用路径。如果六个月后由我维护,我会感谢作者——光设计文档就把意图与失败审计讲清楚了。

唯一不是 5/5 直接批准的原因是策略上的规模:这种规模的核心改动需要人类的合并签字,而非自动批准,这道门禁正在做它该做的事。

⏸️ 转交 @wenshao —— 审查干净;按核心模块策略,合并决定权归你,而非自动批准。我这边没有未决项。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to maintainers — cc @tanzhenxin @wenshao @yiliang114 @LaZzyMan

Re-run at fc99f59: three follow-up commits landed since the last pass (module-shape validation on all loaders, an abort-during-xterm-load test, and a sync/async encoding parity test) and they answer the earlier feedback cleanly — the review remains green. The change is high quality with a real, measured cold-start benefit, but it's a ~615-production-line structural refactor of core dependency-loading from a fork. Our core-module gate routes a change of this profile to a human maintainer rather than auto-approving. Needs a human call on:

  1. Whether the structural scope is acceptable as a single perf PR (vs. splitting);
  2. Whether the final rebased commit needs a fresh 30-pair benchmark, or the enforced zero-byte bundle guard plus the prototype numbers suffice; and
  3. A Windows CI pass before merge (skipped in this run, not manually exercised).

🔄 转交维护者 —— 已在 fc99f59 重新审查:自上次审查后又落了三个提交(三个 loader 的模块形态校验、xterm 加载期间 abort 的测试、以及同步/异步编码一致性测试),它们干净地回应了先前的反馈——审查仍为绿色。改动质量高且有实测的冷启动收益,但这是来自 fork、约 615 行生产代码的核心依赖加载结构性重构。核心模块门禁将此类改动转交人类维护者而非自动批准。需要人类判断:(1) 该结构范围是否可作为单个 perf PR 接受(还是拆分);(2) 最终 rebase 后的提交是否需重新跑 30 组配对 benchmark,还是强制执行的零字节 bundle 门禁加 prototype 数据即可;(3) 合并前过一次 Windows CI(本次跳过且未手动验证)。

Qwen Code · qwen3.8-max-preview

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

中文说明

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

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/config/config.ts
Comment thread packages/core/src/services/shellExecutionService.ts
Comment thread esbuild.config.js
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Addressed the latest automated review at 2208817.

Feedback Decision Action
Cancellation while the first xterm chunk is loading lacked regression coverage Agree Added a race-path test that holds the loader pending, aborts, then verifies no PTY or child process is spawned.
findGitRoot() allegedly changes linked-worktree behavior Disagree git rev-parse --show-toplevel also returns the current linked worktree root, so the replacement preserves the previous behavior.
The esbuild tree-shake matcher could regress silently Disagree The production metafile guard forbids these packages in the ACP static closure, and its tests cover both static failure and dynamic-only success.

Validation:

  • npm run build
  • npm run typecheck
  • cd packages/core && npx vitest run src/services/shellExecutionService.test.ts src/utils/load-xterm-headless.test.ts (135 passed)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/serve-fast-path-bundle-check.test.js (29 passed)

@wenshao

wenshao commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Review — perf(core): Lazy-load first-use dependencies

Reviewed at 2208817ac. Rather than take the reported numbers on trust I rebuilt and re-measured, including a fresh bundle A/B against the current PR base (2051a4173) — the thing the earlier triage left open. No blockers. Everything below is Low or informational.


Overview

Moves iconv-lite, @xterm/headless, and simple-git off the ACP child's eager static import closure and loads each once at first real use, via three single-flight package-local loaders. The synchronous public encoding helpers survive through a compat module (utils/sync-file-encoding.ts) that an esbuild sideEffects: false resolver plugin can tree-shake off the Core root; internal file-service paths switch to …Async variants. Two narrow CLI runtime entry modules replace deferred namespace imports of the Core root, and check-serve-fast-path-bundle.js gains the three packages as forbidden ACP static-closure entries.


Verification I ran (macOS 26 arm64, Node 22.23.1)

1. Bundle A/B on the actual PR base. Built and bundled both 2051a4173 (base) and 2208817ac (head) with DEV=true node esbuild.config.js, then walked the esbuild metafile from the ACP entry output over static-only edges:

Metric base 2051a4173 head 2208817ac Δ
ACP static-closure outputs 145 143 −2
ACP static-closure bytes 13,214,217 12,120,272 −1,093,945
iconv-lite attributed 552,742 0 −552,742
@xterm/headless attributed 213,120 0 −213,120
simple-git attributed 146,526 0 −146,526

node scripts/check-serve-fast-path-bundle.js passes on that real bundle. sync-file-encoding.ts and iconvHelper.ts appear in zero outputs, so the esbuild plugin genuinely performs the tree-shake rather than merely relocating the code; iconv-lite survives only in chunks reachable by dynamic import. The bundle claim reproduces on the rebased base at the reported magnitude (−1.09 MB), so re-running the bundle gate on the final commit is not needed. The 2C4G latency numbers remain tied to the prototype artifact — that's a separate call.

2. Sync/async encoding equivalence. The compat module duplicates ~35 lines of decode logic, so I ran both implementations side by side over 11 decode cases (empty, ASCII, UTF-8 CJK, UTF-8/UTF-16LE/UTF-16BE/UTF-32LE BOM, GBK, Big5, Shift_JIS, invalid bytes) and 9 encode cases (incl. gbk+BOM, utf-16le+BOM, CRLF metadata, unsupported codec). Byte-identical in every casedecodeBufferWithEncodingInfodecodeBufferWithEncodingInfoAsync, encodeTextFileContentencodeTextFileContentAsync.

3. Mutation check on the new abort test. Deleting the post-import if (abortSignal.aborted) recheck in shellExecutionService.ts fails exactly the new test (1 failed / 132 passed). Discriminating, not incidentally green.

4. Tests / format. 577 focused Core tests pass (load-*, fileUtils, fileSystemService, read-text-range, shellExecutionService, gitWorktreeService, worktreeCleanup, extensionManager, github); 29 bundle-guard tests pass; Prettier clean across all 34 changed files.


Findings

1 · The esbuild plugin's dist branch can never match the file that holds the import. esbuild.config.js:126 gates on packages/core/(src|dist)/index.(ts|js). Only the src branch ever fires, because packages/cli/tsconfig.json:10 maps @qwen-code/qwen-code-core../core/src/index.ts and esbuild honours tsconfig paths — I confirmed packages/core/src/index.ts is the input in the real metafile. If that mapping is ever dropped, resolution falls to package.json exportsdist/index.js, which contains only export * from './src/index.js'; the actual export { … } from './utils/sync-file-encoding.js' lives in dist/src/index.js, which the regex does not match. So the dist half is dead today and would not work if reached. Either widen it to also match dist/src/index.js, or drop it and add a comment that the plugin covers the tsconfig-paths-resolved source path only. The bundle guard fails loudly in that scenario, so this is robustness/maintainability, not a silent regression.

2 · iconvHelper.ts:26 re-export is a re-entry point for the exact regression this PR guards against. export { isUtf8CompatibleEncoding } from './encoding.js'; has zero consumers repo-wide — the only remaining importers of iconvHelper.js are sync-file-encoding.ts and two test files, all for iconvEncode/iconvDecode/iconvEncodingExists. Keeping it means a contributor writing the historically-correct import { isUtf8CompatibleEncoding } from './iconvHelper.js' silently re-establishes a static edge to iconv-lite. Deleting it makes the compiler point them at encoding.js.

3 · Orphaned JSDoc. iconvHelper.ts now ends at line 63 with a doc block (57–63) describing isUtf8CompatibleEncoding, which moved to encoding.ts. Leftover from the extraction.

4 · loadIconvLite is the only loader without a shape guard, and its test covers only one shape. load-simple-git.ts and load-xterm-headless.ts both probe named exports before falling back to default, and each has a test per shape. load-iconv-lite.ts:17 does an unconditional module.default as unknown as IconvLite. I verified this is correct today — Node yields { default } only for iconv-lite, and esbuild's __toESM also sets default — so this is consistency, not a live bug. The asymmetric failure mode is what makes it worth three lines: if default were ever absent, loadIconvLite() resolves to undefined, and decodeBufferWithEncodingInfoAsync's try/catch swallows the resulting TypeError and silently returns UTF-8-replacement mojibake with nothing surfaced. Adding the guard plus the missing named-export test case makes all three loaders uniform.

5 · getWorktreeDiff quietly widened its catch. On base, resolveBaseline() / getCurrentBranch() ran before the try and threw to the caller. They are now inside it (gitWorktreeService.ts:824), so a baseline/branch resolution failure returns the string `Error getting diff: …` as the diff body instead of rejecting. Arguably an improvement, but it's a contract change unrelated to lazy loading and isn't in Risk & Scope. applyWorktreeChanges got the same treatment for its two simpleGit() constructions (much lower impact). If intentional, worth one line in the PR body.

6 · findGitRoot() substitution — agree it's safe, one nuance worth a comment. I agree with your earlier reply: git rev-parse --show-toplevel also returns the linked worktree root, so that case is preserved. Two remaining differences: findGitRoot is purely lexical (path.resolve + existsSync, no symlink resolution), so a repo reached through a symlinked path anchors at the symlinked path rather than the realpath; and it ignores GIT_DIR/GIT_WORK_TREE. Both only change which <root>/.qwen/worktrees the hygiene sweep scans, so the worst case is a no-op sweep — not data loss. Fine as-is, but config.ts:2806 sits under a comment block entirely about having fixed a "sweep was permanently a no-op" bug, so a one-liner noting the lexical-vs-rev-parse difference would help the next reader.

7 · Two unreachable throws. fileSystemService.ts:280 and sync-file-encoding.ts:78 both throw new Error('iconv-lite did not prepare non-UTF-8 text content'). Once iconvLite is supplied, prepareTextFileContent always returns a value — an unsupported codec falls through to the UTF-8 return rather than undefined — so neither can fire. Harmless, but reads like a real failure mode.

8 · The direct unit tests migrated onto the compat path. fileUtils.test.ts:547,558 and fileSystemService.test.ts:212,219 now exercise sync-file-encoding.ts, the copy with zero production callers in this repo. The live async variants keep only indirect coverage via readFileWithEncodingInfo / StandardFileSystemService.writeTextFile, and nothing asserts the two implementations agree. They do agree today (I checked — §2 above), but a small table-driven equivalence test is the cheap way to stop them drifting, since the whole point of the compat module is that it can never be deleted casually.

9 · Maintainer call: the sync compat exports have no in-repo consumers. decodeBufferWithEncodingInfo and encodeTextFileContent are referenced only by packages/core/src/index.ts and tests. Everything that buys them — the esbuild plugin, the widened fileUtils surface (isValidUtf8, decodeBOMBuffer, bomEncodingToName, BOMInfo, UnicodeEncoding), the exported prepareTextFileContent with its undefined sentinel, and the duplicated decode logic — exists purely to keep the published @qwen-code/qwen-code-core surface stable for external consumers. That may well be the right trade, but it is the single largest source of complexity in the diff and deserves an explicit decision rather than being inherited. (Not something to change in this PR.)


Quality, performance, security

  • Style/conventions: consistent with the repo — Apache headers on new files, createDebugLogger tags, ??= single-flight matching existing loader idioms, narrow re-export modules type-checked by use (core-runtime.ts's 26 symbols exactly match the 26 core.* references in run-qwen-serve.ts).
  • Module identity: core-runtime.ts / deferred-core-runtime.ts re-export from the same bundled Core instance, so instanceof and class identity are preserved — confirmed via the metafile (single packages/core/src/index.ts input).
  • Performance: win is real and reproduced above. The added per-call cost is one already-resolved promise await on non-UTF-8 encode/decode, PTY start, and Git operations — negligible against the work that follows. prepareTextFileContent runs twice on the non-UTF-8 write path (once to detect the missing codec, once with it), but needsCrlfLineEndings is pure and the extra work is one string normalisation.
  • Security: no new surface. All three import() calls take string literals; no dynamic specifier, no createRequire, no node_modules runtime dependency. allowUnsafeHooksPath usage is unchanged.
  • Rejection stickiness: the loader promises cache rejections for the process lifetime. Correct for a missing/corrupt bundled chunk (it cannot heal), and documented in the design doc — noting it only so it isn't rediscovered later.
  • Windows: still the main unexercised axis, since PTY + @xterm/headless deferral is exactly the Windows-sensitive path. Worth a green Windows CI run before merge, as already noted.

Verdict: the mechanism is sound, the numbers hold on the current base, and the behavioural contracts I probed (encoding equivalence, PTY abort/fallback, Git structured failures) are preserved. Findings 1–4 are the ones I'd fix before merge; they're all a few lines each.

中文说明

2208817ac 上审查。我没有直接采信 PR 中的数据,而是重新构建并复测,包括针对当前 PR base2051a4173)做了一次全新的 bundle A/B —— 这正是此前 triage 留下的开放问题。无阻塞项,以下均为 Low 或提示性意见。

概述

iconv-lite@xterm/headlesssimple-git 移出 ACP 子进程的启动期静态导入闭包,通过三个 single-flight 包级 loader 在首次真实使用时各加载一次。同步的公开编码辅助函数通过兼容模块 utils/sync-file-encoding.ts 保留,并由 esbuild sideEffects: false resolver 插件从 Core 根导出中 tree-shake 掉;内部文件服务路径改用 …Async 变体。两个窄化的 CLI runtime 入口模块替代了对 Core 根的延迟 namespace import,check-serve-fast-path-bundle.js 新增这三个包为 ACP 静态闭包禁止项。

我实际执行的验证(macOS 26 arm64,Node 22.23.1)

1. 针对真实 PR base 的 bundle A/B。2051a4173(base)与 2208817ac(head)分别执行 DEV=true node esbuild.config.js,然后从 ACP entry output 出发,仅沿静态边遍历 esbuild metafile:

指标 base 2051a4173 head 2208817ac Δ
ACP 静态闭包 outputs 145 143 −2
ACP 静态闭包字节 13,214,217 12,120,272 −1,093,945
iconv-lite 归因 552,742 0 −552,742
@xterm/headless 归因 213,120 0 −213,120
simple-git 归因 146,526 0 −146,526

node scripts/check-serve-fast-path-bundle.js 在该真实 bundle 上通过。sync-file-encoding.tsiconvHelper.ts任何 output 中都不出现,说明 esbuild 插件确实完成了 tree-shake 而非仅仅搬运代码;iconv-lite 只存在于动态 import 可达的 chunk 中。bundle 结论在 rebase 后的 base 上以相同量级复现(−1.09 MB),因此最终提交无需重跑 bundle 门禁。2C4G 的延迟数据仍绑定 prototype artifact —— 那是另一个判断。

2. 同步/异步编码等价性。 兼容模块复制了约 35 行解码逻辑,因此我并排运行了两套实现:11 个解码用例(空、ASCII、UTF-8 中文、UTF-8/UTF-16LE/UTF-16BE/UTF-32LE BOM、GBK、Big5、Shift_JIS、非法字节)与 9 个编码用例(含 gbk+BOM、utf-16le+BOM、CRLF 元数据、不支持的 codec)。全部逐字节一致。

3. 对新增 abort 测试的变异检验。 删除 shellExecutionService.ts 中 import 之后的 if (abortSignal.aborted) 复检后,恰好只有新增的那条测试失败(1 失败 / 132 通过)。说明该测试具备判别力,而非偶然通过。

4. 测试 / 格式。 577 个聚焦 Core 测试通过;29 个 bundle guard 测试通过;34 个改动文件 Prettier 全部干净。

发现

1 · esbuild 插件的 dist 分支永远匹配不到真正含有该 import 的文件。 esbuild.config.js:126packages/core/(src|dist)/index.(ts|js) 作为判定条件。实际只有 src 分支会触发,因为 packages/cli/tsconfig.json:10@qwen-code/qwen-code-core 映射到 ../core/src/index.ts,而 esbuild 会遵循 tsconfig paths —— 我在真实 metafile 中确认输入即为 packages/core/src/index.ts。若该映射被移除,解析会回落到 package.json exportsdist/index.js,而该文件只有 export * from './src/index.js';真正的 export { … } from './utils/sync-file-encoding.js' 位于 dist/src/index.js,正则匹配不到。因此 dist 分支今天是死代码,且即使被走到也不生效。建议要么扩展为同时匹配 dist/src/index.js,要么删除它并注释说明该插件只覆盖 tsconfig-paths 解析出的源码路径。该场景下 bundle guard 会明确失败,所以这属于健壮性/可维护性问题,不是静默回归。

2 · iconvHelper.ts:26 的 re-export 正是本 PR 所防范回归的再入口。 export { isUtf8CompatibleEncoding } from './encoding.js'; 全仓零消费者 —— iconvHelper.js 仅剩 sync-file-encoding.ts 与两个测试文件引用,且都是为了 iconvEncode/iconvDecode/iconvEncodingExists。保留它意味着后续贡献者按历史习惯写 import { isUtf8CompatibleEncoding } from './iconvHelper.js' 会静默重建到 iconv-lite 的静态边。删除后编译器会把他们导向 encoding.js

3 · 悬空 JSDoc。 iconvHelper.ts 现在停在第 63 行,末尾(57–63)是描述已迁往 encoding.tsisUtf8CompatibleEncoding 的文档块,属提取遗留。

4 · loadIconvLite 是唯一没有形状守卫的 loader,其测试也只覆盖一种形状。 load-simple-git.tsload-xterm-headless.ts 都会先探测具名导出再回退 default,且每种形状各有测试。load-iconv-lite.ts:17 则无条件 module.default as unknown as IconvLite。我验证过当前是正确的 —— Node 对 iconv-lite 只给出 { default },esbuild 的 __toESM 同样会设置 default —— 所以这是一致性问题而非现存 bug。值得花三行的原因在于其失败模式不对称:若 default 缺失,loadIconvLite() 解析为 undefined,而 decodeBufferWithEncodingInfoAsynctry/catch 会吞掉由此产生的 TypeError,静默返回 UTF-8 替换字符的乱码且不向上暴露任何信息。补上守卫与缺失的具名导出测试用例,可让三个 loader 保持一致。

5 · getWorktreeDiff 悄悄扩大了 catch 范围。 base 上 resolveBaseline() / getCurrentBranch()try 之前执行并向调用方抛出。现在它们被移入 trygitWorktreeService.ts:824),因此 baseline/分支解析失败会把 `Error getting diff: …` 字符串作为 diff 内容返回而非 reject。这也许是改进,但属于与 lazy load 无关的契约变化,且未列入 Risk & ScopeapplyWorktreeChanges 的两处 simpleGit() 构造也做了同样处理(影响小得多)。若为有意为之,建议在 PR 描述中补一行。

6 · findGitRoot() 替换 —— 认同其安全性,但有一点值得加注释。 同意你此前的回复:git rev-parse --show-toplevel 在 linked worktree 中同样返回该 worktree 根,因此该场景行为保持不变。仍存在两点差异:findGitRoot 是纯词法的(path.resolve + existsSync,不解析符号链接),所以通过符号链接路径进入的仓库会锚定在符号链接路径而非真实路径;且它忽略 GIT_DIR/GIT_WORK_TREE。二者只影响清理任务扫描哪个 <root>/.qwen/worktrees,最坏情况是 sweep 变成 no-op,不会丢数据。维持现状即可,但 config.ts:2806 所处的注释块通篇都在讲修复"sweep 永久 no-op"的 bug,加一行说明词法遍历与 rev-parse 的差异会对后来者更友好。

7 · 两处不可达的 throw。 fileSystemService.ts:280sync-file-encoding.ts:78throw new Error('iconv-lite did not prepare non-UTF-8 text content')。一旦传入 iconvLiteprepareTextFileContent 必然返回值 —— 不支持的 codec 会落到 UTF-8 返回分支而非 undefined —— 因此两处都不可能触发。无害,但读起来像是真实故障模式。

8 · 直接单测被迁移到了兼容路径上。 fileUtils.test.ts:547,558fileSystemService.test.ts:212,219 现在测的是 sync-file-encoding.ts,即本仓库中零生产调用方的那份副本。真正在用的 async 变体仅保留经由 readFileWithEncodingInfo / StandardFileSystemService.writeTextFile 的间接覆盖,且没有任何断言保证两套实现一致。它们今天确实一致(见上文 §2 我的验证),但补一个表驱动的等价性测试是防止二者漂移的低成本手段 —— 毕竟兼容模块的意义就在于它不能被随手删除。

9 · 需维护者决策:同步兼容导出在仓库内没有消费者。 decodeBufferWithEncodingInfoencodeTextFileContent 仅被 packages/core/src/index.ts 和测试引用。为它们付出的全部代价 —— esbuild 插件、被放宽的 fileUtils 导出面(isValidUtf8decodeBOMBufferbomEncodingToNameBOMInfoUnicodeEncoding)、导出的带 undefined 哨兵值的 prepareTextFileContent、以及重复的解码逻辑 —— 完全是为了让已发布的 @qwen-code/qwen-code-core 对外部消费者保持稳定。这可能确实是正确取舍,但它是本 diff 中最大的复杂度来源,应当被显式决策而非默认继承。(不建议在本 PR 中改动。)

质量、性能与安全

  • 风格/约定: 与仓库一致 —— 新文件带 Apache 头、createDebugLogger tag、??= single-flight 沿用既有 loader 惯例、窄化 re-export 模块由使用处做类型校验(core-runtime.ts 的 26 个符号与 run-qwen-serve.ts 中 26 处 core.* 引用完全对应)。
  • 模块同一性: core-runtime.ts / deferred-core-runtime.ts 从同一个被 bundle 的 Core 实例 re-export,instanceof 与类同一性得以保持 —— 已通过 metafile 确认(唯一 packages/core/src/index.ts 输入)。
  • 性能: 收益真实且已复现。新增的单次调用成本是在非 UTF-8 编解码、PTY 启动与 Git 操作上多 await 一个已 resolve 的 promise,相对其后的工作可忽略。非 UTF-8 写入路径会执行两次 prepareTextFileContent(一次探测缺失 codec,一次带上它),但 needsCrlfLineEndings 是纯函数,额外开销仅一次字符串归一化。
  • 安全: 无新增攻击面。三处 import() 均为字符串字面量;无动态 specifier、无 createRequire、无运行期 node_modules 依赖。allowUnsafeHooksPath 用法未变。
  • rejection 粘性: loader promise 会在进程生命周期内缓存 rejection。对于缺失/损坏的 bundled chunk 这是正确的(无法自愈),设计文档也已说明 —— 此处只是记录,避免日后被重新"发现"。
  • Windows: 仍是主要未验证维度,因为 PTY + @xterm/headless 延迟加载恰是 Windows 敏感路径。如已指出,合并前值得跑一次绿色的 Windows CI。

结论: 机制可靠,数据在当前 base 上成立,我探测过的行为契约(编码等价性、PTY abort/fallback、Git 结构化失败返回)均得以保持。发现 1–4 是我建议合并前修掉的,每项都只需几行。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅

中文说明

未发现问题。LGTM!✅

— qwen3.7-max via Qwen Code /review

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Thanks @wenshao — I checked each point against the current head and pushed 769ceac297.

Addressed before merge:

  • Fixed the tree-shake plugin matcher so it covers both packages/core/src/index.ts and the real built importer packages/core/dist/src/index.js.
  • Removed the unused isUtf8CompatibleEncoding compatibility re-export and its orphaned JSDoc.
  • Added an iconv-lite module-shape guard plus tests for default-export, named-export, cached-load, and invalid-module behavior.
  • Added table-driven sync/async encoding equivalence coverage for empty, UTF-8, UTF-8 BOM, GBK, CRLF, and unsupported-encoding paths.

Documentation/disposition:

  • Updated the English and Chinese Risk & Scope sections to disclose that lazy simple-git setup now occurs inside the existing Git diff/apply failure envelopes, and to make the retained synchronous package-root encoding API an explicit compatibility tradeoff.
  • Left the lexical findGitRoot behavior unchanged because it is a synchronous best-effort startup probe and the current fallback semantics are intentional.
  • Left the two defensive throws unchanged because they preserve narrowing/invariant checks without changing observable behavior.

Validation passed: full workspace typecheck, 219 focused Core tests, 29 bundle-guard tests, targeted Prettier/ESLint, the CLI-only production build and bundle, and the real startup bundle closure guard. The review was top-level rather than inline, so there were no new review threads to resolve (0/0).

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

中文说明

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

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/utils/load-simple-git.ts Outdated
Comment thread packages/core/src/utils/load-xterm-headless.ts Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Addressed the latest automated review in fc99f59756.

Feedback Judgment Action
Validate truthy simple-git default exports Agree Added a module-shape predicate that requires a callable factory and non-null enum object; the invalid-default test now uses a truthy empty object.
Validate @xterm/headless exports Agree Added a module-shape predicate that requires a callable Terminal constructor and an invalid-default regression test.

Validation passed: full workspace typecheck, 139 focused Core tests, the CLI-only production build and bundle, and the real startup bundle closure guard.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Latest review disposition (no code change):

Feedback Judgment Disposition
Add a dedicated removeWorktree loader-rejection test Valid non-Critical coverage suggestion Deferred to follow-up work because the existing implementation already returns the structured error and this PR has reached the review-round convergence limit.
Replace or rename the defensive sync encoding invariant throw Non-Critical maintainability preference Deferred to follow-up work; the guard is intentional and currently provides TypeScript narrowing for the optional helper result.

Both inline threads contain the exact rationale. Per the repository's roughly-five-round rule, further changes on this PR are limited to Critical correctness, security, data-loss, or regression fixes.

@wenshao

wenshao commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Re-verification at fc99f5975 — all four blocking-ish findings fixed, fix #1 proven load-bearing

Follow-up to my round-1 review (2208817ac). Two commits landed since — 769ceac29 (addresses that review) and fc99f5975 (module-shape guards). The PR base is unchanged (2051a4173), so this is a clean re-measure of the same A/B. I rebuilt, re-bundled and re-ran everything rather than diffing the diff.

No blockers. Merge-ready from my side.

Previous finding → status at the new head

# Finding at 2208817ac Sev Status at fc99f5975
1 esbuild plugin's dist branch could never match the file holding the import Low Fixed in 769ceac29 — and I verified the new branch is genuinely load-bearing, not cosmetic (below)
2 iconvHelper.ts re-export was a re-entry point for the exact regression this PR guards Low Fixed — re-export removed; isUtf8CompatibleEncoding now only reachable via encoding.js
3 Orphaned JSDoc left by the extraction Nit Fixed — file now ends at the correct iconvEncodingExists doc
4 loadIconvLite was the only loader without a shape guard Low Fixed, and gone further than I asked: fc99f5975 gives all three loaders a shape predicate + invalid-module test
5 getWorktreeDiff quietly widened its catch, not in Risk & Scope Low Documented — Risk & Scope now discloses that lazy Git setup happens inside the existing diff/apply failure envelopes
6 findGitRoot() lexical-vs-rev-parse nuance Info Declined with rationale (intentional best-effort sync probe). I agree — worst case is a no-op sweep
7 Two unreachable throws Nit Declined (kept for TS narrowing). Stands, harmless — I found a third dead branch of the same family, see N1
8 Nothing asserted the sync and async encoders agree Low Fixed — new 9-case table test; I mutation-checked it and it discriminates
9 Sync compat exports have no in-repo consumers Maintainer call Documented as an explicit external-consumer compatibility tradeoff

1 · Bundle A/B re-measured on the new head

Rebuilt both sides and walked the esbuild metafile from the ACP entry output over static-only edges.

Metric base 2051a4173 head fc99f5975 Δ
ACP static-closure outputs 145 143 −2
ACP static-closure bytes 13,214,217 12,121,575 −1,092,642
iconv-lite attributed 552,742 0 −552,742
@xterm/headless attributed 213,120 0 −213,120
simple-git attributed 146,526 0 −146,526

node scripts/check-serve-fast-path-bundle.js passes on the real bundle. The closure is 1,303 bytes larger than at 2208817ac — that is the three new shape predicates themselves, which live in the static closure while the packages they load do not. Expected and correct.

2 · Finding 1's fix is load-bearing, not cosmetic

The change is one regex alternation ((?:src|dist)(?:src|dist[\\/]src)), so it's worth showing it actually does something. I removed the @qwen-code/qwen-code-core mapping from packages/cli/tsconfig.json paths, which forces esbuild to resolve Core through package.json exportsdist/index.jsdist/src/index.js — precisely the scenario the dist branch exists for (confirmed: packages/core/dist/index.js becomes a bundle input in both arms).

Arm sync-file-encoding in bundle iconv-lite in ACP closure guard
A — new regex tree-shaken, absent 0 exit 0 ✓
B — old regex core/dist/src/utils/sync-file-encoding.js pulled in 552,724 exit 1 ✗

Arm B's guard names the offending edge: dist/chunks/acpAgent-XZ6BC7W7.js -> dist/chunks/chunk-34T4CTM7.js. So the one-line change is what keeps the tree-shake working if that tsconfig mapping is ever dropped — and if it somehow still failed, the guard fails loudly rather than silently regressing.

bundle A/B

3 · The new shape guards, checked against the real modules

fc99f5975's guards are only tested with vi.doMock. The risk that introduces is the opposite of the one I raised: a predicate that is too strict would throw at first real use — in production, in a shipped bundle, on a path no unit test covers. So I checked the real shapes three ways.

  • Real modules through the PR's own compiled loaders (no mocks): 14/14, including a real GBK decode + byte-identical round-trip, a real Terminal constructing and replaying output, and real checkIsRepo / revparse / status calls.
  • Same harness through esbuild's __toESM interop: 14/14.
  • The predicates applied to the actual shipped dynamic chunks — the decisive one:
Package Shipped chunk predicate(namespace) Branch taken Verdict
iconv-lite lib-GFEPFPNS.js false .default accepted
@xterm/headless xterm-headless-V6AWZL7D.js false .default accepted
simple-git esm-IIIWUUC5.js true namespace accepted

Two of the three genuinely need the .default fallback, so the branch isn't dead weight. The compiled loaders in dist/chunks reference ./lib-*.js, ./xterm-headless-*.js and ./esm-*.js as real dynamic chunk imports, which is the deferral itself showing up in the shipped artifact.

real module shapes

4 · Mutation matrix — the new tests discriminate

Every new test earns its place: reverting each loader's guard to its pre-fc99f5975 implementation fails that loader's new test, and both sync-decoder mutations fail the new equivalence table.

Mutation Result
M1 revert iconv-lite shape guard 2 failed / 1 passed killed
M2 revert simple-git shape guard 1 failed / 2 passed killed
M3 revert @xterm/headless shape guard 1 failed / 2 passed killed
MD1 sync decoder drops the BOM flag 1 failed / 8 passed killed
MD2 sync decoder skips the iconv decode 1 failed / 8 passed killed
ME1 sync encoder ignores prepared.encoding 9 passed survived

ME1 survived, so I probed whether it was a coverage gap. It isn't — it's an equivalent mutant. Whenever prepared.data is a string, prepared.encoding is always "utf-8"; every non-UTF-8 case returns a Buffer and takes the early return on the line above. The mutation is unobservable by construction.

mutation matrix

Two small new observations (neither blocking)

N1 · sync-file-encoding.ts:81's ?? 'utf-8' fallback is dead. Fallout of the ME1 probe above — the nullish branch can never be taken, for the reason given. Same family as finding 7; mentioning it only so the probe table isn't misread as a gap.

N2 · Where the new guard throws land (informational, and it's an improvement). I traced all three. loadXtermHeadless() is awaited inside the existing PTY try (shellExecutionService.ts:715), so a guard rejection keeps the documented child_process fallback. On the read path, loadIconvLite() sits inside the existing try/catch (fileUtils.ts:216-229), so a rejection is logged via debugLogger.warn and degrades to UTF-8 replacement — still user-invisible unless debug logging is on, but that's pre-existing, and it's now a named error instead of the opaque TypeError the old unconditional module.default cast would have produced. On the write path (fileSystemService.ts:276) the rejection propagates, which is the right call — better to fail than to write wrong bytes.

Regression suites at fc99f5975

586 focused Core tests · 130 focused CLI tests (1 pre-existing skip) · 29 bundle-guard tests · Prettier clean across all 35 changed files · ESLint clean · packages/core tsc --noEmit clean.

(The workspace-wide typecheck fails in my symlinked verification worktree with 61 ink selectable/selectionFlow prop-type errors, all in packages/cli/src/ui, none touched by this PR — an artifact of my setup, not the change. CI's own typecheck is green.)

What I did not verify

  • The 2C4G latency and RSS numbers — those need the 2-vCPU reference host. What I can say is that the bundle half of the claim reproduces at full magnitude on the current base at the current head, so re-running the bundle gate on the final commit is not needed; whether the latency table needs a fresh 30-pair run against fc99f5975 rather than the prototype artifact remains the open maintainer call from triage.
  • Windows — still the one unexercised axis, and PTY + @xterm/headless deferral is exactly the Windows-sensitive path. Worth a green Windows CI run before merge.

Verdict: everything I flagged in round 1 is either fixed or explicitly dispositioned, the fixes hold up under adversarial checks rather than just looking right, and nothing new rises above a nit. Environment: macOS 26 arm64, Node 22.23.1, isolated worktree at fc99f5975, base 2051a4173.

中文说明

fc99f5975 上的复验 —— 四项待修问题全部修复,且已证明修复 #1 是真正起作用的

这是对我第一轮审查2208817ac)的后续。此后新增两个提交:769ceac29(处理该轮审查)与 fc99f5975(模块形状守卫)。PR base 未变(2051a4173),因此这是同一组 A/B 的干净复测。我重新构建、重新 bundle 并重跑了全部验证,而不是只对比 diff 的 diff。

无阻塞项,从我这边看可以合并。

上一轮发现 → 新 head 上的状态

# 2208817ac 上的发现 级别 fc99f5975 上的状态
1 esbuild 插件的 dist 分支永远匹配不到真正含有该 import 的文件 Low 已修复769ceac29)—— 并且我验证了新分支确实起作用,而非装饰性改动(见下)
2 iconvHelper.ts 的 re-export 正是本 PR 所防范回归的再入口 Low 已修复 —— re-export 已删除,isUtf8CompatibleEncoding 现在只能经由 encoding.js 获得
3 提取遗留的悬空 JSDoc Nit 已修复 —— 文件现在正确结束于 iconvEncodingExists 的文档
4 loadIconvLite 是唯一没有形状守卫的 loader Low 已修复,且做得比我建议的更多:fc99f5975 为三个 loader 都加了形状断言与非法模块测试
5 getWorktreeDiff 悄悄扩大了 catch 范围,且未列入 Risk & Scope Low 已补充说明 —— Risk & Scope 现已披露懒加载 Git 的初始化发生在既有 diff/apply 失败包络内
6 findGitRoot() 词法遍历与 rev-parse 的差异 Info 已附理由婉拒(有意保留的同步 best-effort 探测)。我认同 —— 最坏情况只是 sweep 变成 no-op
7 两处不可达的 throw Nit 婉拒(保留用于 TS narrowing)。维持现状,无害 —— 我另发现同类的第三处死分支,见 N1
8 没有任何断言保证同步与异步编码实现一致 Low 已修复 —— 新增 9 组表驱动测试;我做了变异检验,确认其具备判别力
9 同步兼容导出在仓库内没有消费者 维护者决策 已明确记录为面向外部使用者的兼容性取舍

1 · 在新 head 上重测 bundle A/B

重新构建两侧,并从 ACP entry output 出发、仅沿静态边遍历 esbuild metafile。

指标 base 2051a4173 head fc99f5975 Δ
ACP 静态闭包 outputs 145 143 −2
ACP 静态闭包字节 13,214,217 12,121,575 −1,092,642
iconv-lite 归因 552,742 0 −552,742
@xterm/headless 归因 213,120 0 −213,120
simple-git 归因 146,526 0 −146,526

node scripts/check-serve-fast-path-bundle.js 在真实 bundle 上通过。闭包比 2208817ac大 1,303 字节 —— 这正是三个新增的形状断言本身:它们位于静态闭包中,而它们所加载的包不在。符合预期。

2 · 修复 #1 确实起作用,并非装饰性改动

该改动只是一处正则分支((?:src|dist)(?:src|dist[\\/]src)),因此值得证明它真的有效。我从 packages/cli/tsconfig.jsonpaths 中移除了 @qwen-code/qwen-code-core 映射,迫使 esbuild 经由 package.json exportsdist/index.jsdist/src/index.js 解析 Core —— 这正是 dist 分支存在的场景(已确认:两个 arm 中 packages/core/dist/index.js 都成为 bundle 输入)。

Arm bundle 中的 sync-file-encoding ACP 闭包中的 iconv-lite 门禁
A —— 新正则 已被 tree-shake,不存在 0 exit 0 ✓
B —— 旧正则 core/dist/src/utils/sync-file-encoding.js 被引入 552,724 exit 1 ✗

Arm B 的门禁明确指出了违规边:dist/chunks/acpAgent-XZ6BC7W7.js -> dist/chunks/chunk-34T4CTM7.js。因此这一行改动正是在该 tsconfig 映射日后被移除时,保证 tree-shake 仍然生效的关键;而即便仍然失效,门禁也会明确报错,而不是静默回归。

3 · 针对真实模块检验新的形状守卫

fc99f5975 的守卫只用 vi.doMock 做了测试。这引入的风险与我提出的问题正好相反:过严的断言会在首次真实使用时抛错 —— 发生在生产环境、在已 bundle 的产物中、在单测覆盖不到的路径上。因此我用三种方式检验了真实形状。

  • 用 PR 自己编译出的 loader 加载真实模块(无 mock):14/14 通过,包含真实 GBK 解码与逐字节一致的往返、真实 Terminal 构造并回放输出,以及真实的 checkIsRepo / revparse / status 调用。
  • 同一套 harness 经过 esbuild __toESM interop:14/14 通过。
  • 将断言应用于真实发布的动态 chunk —— 这是决定性的一项:
发布的 chunk predicate(namespace) 走的分支 结论
iconv-lite lib-GFEPFPNS.js false .default 接受
@xterm/headless xterm-headless-V6AWZL7D.js false .default 接受
simple-git esm-IIIWUUC5.js true namespace 接受

三者中有两个确实需要 .default 回退分支,说明该分支并非冗余。dist/chunks 中编译后的 loader 以 ./lib-*.js./xterm-headless-*.js./esm-*.js 的形式引用真实动态 chunk —— 这正是延迟加载本身在发布产物中的体现。

4 · 变异矩阵 —— 新增测试具备判别力

每个新测试都名副其实:把各 loader 的守卫回退到 fc99f5975 之前的实现,都会让该 loader 的新测试失败;两处同步解码器的变异也都会让新的等价性表格测试失败。

变异 结果
M1 回退 iconv-lite 形状守卫 2 失败 / 1 通过 已杀死
M2 回退 simple-git 形状守卫 1 失败 / 2 通过 已杀死
M3 回退 @xterm/headless 形状守卫 1 失败 / 2 通过 已杀死
MD1 同步解码器丢弃 BOM 标记 1 失败 / 8 通过 已杀死
MD2 同步解码器跳过 iconv 解码 1 失败 / 8 通过 已杀死
ME1 同步编码器忽略 prepared.encoding 9 通过 存活

ME1 存活,因此我进一步探测它是否属于覆盖缺口。并不是 —— 它是等价变异体。只要 prepared.data 是字符串,prepared.encoding 恒为 "utf-8";所有非 UTF-8 情形都返回 Buffer 并在上一行提前返回。该变异在构造上就不可观测。

两点新的小观察(均不阻塞)

N1 · sync-file-encoding.ts:81?? 'utf-8' 回退是死代码。 这是上述 ME1 探测的副产物 —— 出于同样的原因,该空值合并分支永远走不到。与发现 7 同类;此处提出只是为避免把探测表格误读成覆盖缺口。

N2 · 新守卫抛出的错误落在哪里(提示性,且属于改进)。 三处我都追踪了。loadXtermHeadless() 在既有 PTY try 内被 await(shellExecutionService.ts:715),因此守卫抛错会保留既定的 child_process fallback。读取路径上,loadIconvLite() 位于既有 try/catch 内(fileUtils.ts:216-229),因此抛错会经 debugLogger.warn 记录并降级为 UTF-8 替换字符 —— 未开启 debug 日志时对用户仍不可见,但这是既有行为;且现在是一个具名错误,而非旧的无条件 module.default 转换所产生的、令人费解的 TypeError。写入路径上(fileSystemService.ts:276)抛错会向上传播,这是正确的选择 —— 失败好过写入错误的字节。

fc99f5975 上的回归测试

586 个聚焦 Core 测试 · 130 个聚焦 CLI 测试(1 个既有 skip)· 29 个 bundle guard 测试 · 35 个改动文件 Prettier 全部干净 · ESLint 干净 · packages/coretsc --noEmit 干净。

(全 workspace typecheck 在我的符号链接验证 worktree 中会报 61 个 ink selectable/selectionFlow 属性类型错误,全部位于 packages/cli/src/ui,均非本 PR 触及 —— 这是我环境的产物,不是本次改动的问题。CI 自身的 typecheck 是绿的。)

我未验证的部分

  • 2C4G 的延迟与 RSS 数据 —— 这需要 2 vCPU 参考机器。我能确认的是,该结论中的 bundle 部分在当前 base、当前 head 上以完整量级复现,因此最终提交无需重跑 bundle 门禁;至于延迟表格是否需要针对 fc99f5975 而非 prototype artifact 重新跑一次 30 组配对,仍是 triage 留下的、需维护者判断的开放问题。
  • Windows —— 仍是唯一未验证的维度,而 PTY + @xterm/headless 延迟加载恰是 Windows 敏感路径。合并前值得跑一次绿色的 Windows CI。

结论: 我在第一轮提出的问题要么已修复、要么已明确表态处置;这些修复在对抗性检验下站得住,而不只是「看起来对」;新发现的问题均不超过 nit 级别。环境:macOS 26 arm64、Node 22.23.1、隔离 worktree、head fc99f5975、base 2051a4173

@wenshao

wenshao commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

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

wenshao pushed a commit that referenced this pull request Jul 25, 2026
…rules

Borrow the image-evidence and quantified-verification patterns from
hand-run rounds (#7265, #7471, #7686 r2 and the pr-assets convention):

- publish-verify now hosts agent-produced evidence/*.png on the pr-assets
  branch (verify/pr<N>-<run>-<attempt>/) and appends them below the
  escaped report. Untrusted-payload discipline: strict filename allowlist,
  8-image / 2 MB caps enforced in the find predicates, racing-push retry,
  and every failure degrades to a text-only comment. VERIFY_ASSETS_REMOTE
  is a test seam; the block was dry-run against a local bare remote
  covering hosting, hostile filenames, oversize files, dotfiles, missing
  branch, and no-image runs
- skill: evidence images are named as kebab-case captions binding image to
  claim, before/after pairs over lone after-shots; follow-up rounds lead
  with a previous-finding status table (fixed/stands/superseded/declined,
  with adjudication) and re-measure instead of diffing the old report;
  size/perf claims get measured-metric Δ tables with residual deltas
  accounted for; unreachable branches get the configuration that reaches
  them constructed; defensive guards get their accept path checked against
  real production artifacts, not just mocked rejects
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

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

@gwinthis

Copy link
Copy Markdown
Collaborator

Review + Linux E2E verification report (real build, tmux)

Verdict: the lazy-loading mechanics are correct and verified working end-to-end on Linux — all three first-use paths (iconv-lite, @xterm/headless, simple-git) function correctly from the real bundle, and the bundle-closure guard passes. One robustness suggestion and one pre-existing (not PR-caused) finding below.

Code review reasoning

  1. The loader pattern is uniform and sound. Each of the three loaders memoizes a single import promise (??=), validates the module shape (both named-export and CJS-default forms), and single-flights concurrent loads. Shape validation at the dynamic boundary is the right call: a bad chunk fails with a diagnosable error at first use instead of undefined is not a function deep in a call stack.
  2. Abort semantics are preserved at the new await point. ShellExecutionService.execute re-checks abortSignal.aborted after await loadXtermHeadless() and returns a pre-spawn aborted handle — the new suspension point cannot leak a spawned process. The PTY-load failure path falls into the existing child-process fallback catch.
  3. GitWorktreeService construction is now side-effect-free with a memoized getGit(), and structured-failure methods keep their envelopes. Verified empirically (below): construction loads nothing; the first real operation loads git once.
  4. The guard closes the loop. check:serve-fast-path-bundle rejecting any static path from the ACP runtime to the three packages makes the optimization durable — a future import iconv from 'iconv-lite' anywhere in the ACP closure fails the check instead of silently regressing startup.

Verification evidence (Linux, commit fc99f59, full build + bundle)

  • Focused core tests (loaders, sync-file-encoding, fileUtils, shellExecutionService, fileSystemService): 358/358 passed.
  • npm run check:serve-fast-path-bundle: "Startup bundle closure checks passed."
  • tmux real session against the bundled CLI:
Path exercised Observed Result
Lazy iconv-lite (read) GBK file decoded correctly in Read tool output (你好,世界。这是GBK编码的测试文件。)
Lazy iconv-lite (write) Edit tool appended text; on-disk file is 49 bytes, invalid as UTF-8, decodes cleanly as GBK — encoding preserved
Lazy @xterm/headless Shell command via PTY path rendered output/replay normally
Lazy simple-git new GitWorktreeService(...) constructs without loading git; isGitRepository()/getCurrentBranch() then work correctly via the barrel export

Suggestion (non-blocking)

All three loaders cache a rejected promise forever: p ??= import(...) keeps the rejection, so a transient first-use failure (e.g. EMFILE under fd pressure) permanently poisons the loader for the process lifetime, converting a retryable error into a sticky one. Consider clearing the memo on rejection (promise.catch(() => { memo = undefined; }) style) so a later call can retry.

Pre-existing finding (not caused by this PR)

While exercising the compiled output I hit a TDZ crash when importing services/gitWorktreeService.js directly as an ESM entry: utils/tool-utils.js imports the package barrel (index.js), creating the cycle gitWorktreeService → shell-utils → tool-utils → index.js → config.js → worktreeCleanup → gitWorktreeService. I verified the identical failure on a non-PR build, and the real bundle is unaffected — filing separately may be worthwhile, but it should not block this PR.

Process note

Per the repo's core-infrastructure triage policy, this is an external PR touching packages/core/src/** at ~950 added lines. It is perf-typed (not refactor), so it is not size-blocked, but it should be escalated for maintainer awareness rather than gate-approved. This report is verification input for that decision, not an approval.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and not exercised locally; PR notes Windows as unexercised. Not reviewed: chunk 4, chunk 1, chunk 3, chunk 5, chunk 6, chunk 2 — launched with a prompt that is not the one the CLI built. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

— qwen3.7-max via Qwen Code /review

@doudouOUC
doudouOUC requested a review from ytahdn July 26, 2026 03:01

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Well-structured lazy-loading refactor: iconv-lite, @xterm/headless, and simple-git moved out of the ACP static closure (~1MB reduction). All fallback paths preserved (decoding fallback, child-process fallback, structured Git failure returns). Comprehensive test coverage (461 core + 374 CLI + 29 bundle guard). Solid benchmark data. No blocking findings after OCR + manual audit.

@doudouOUC
doudouOUC added this pull request to the merge queue Jul 26, 2026
Merged via the queue into QwenLM:main with commit 8fa8085 Jul 26, 2026
210 of 212 checks passed
@doudouOUC
doudouOUC deleted the perf/lazy-first-use-dependencies branch July 26, 2026 03:10
pull Bot pushed a commit to Stars1233/qwen-code that referenced this pull request Jul 26, 2026
* feat(triage): add sandboxed /verify deep-verification lane

@qwen-code /verify on a PR now runs a local-verification-style evidence
round in the isolated /tmux sandbox contract (container, token-free agent
env, loopback model proxy, author-write gate) and publishes the report via
a separate PR-code-free job:

- new verify job: merge-ref checkout at depth 2 (base tip + PR head for
  A/B), skills pinned from base so the tree under test can never rewrite
  its own verifier, PR-planted tmp/*-verify-* artifacts dropped, git
  exec-vector sweep for the persistent workspace, agent verdict
  allowlisted before it reaches workflow outputs
- new publish-verify job: upserts one marker comment (running status ->
  final report), HTML-escapes the untrusted report, reports skip/na/
  prepare-fail/infra outcomes explicitly since /verify is always an
  explicit request
- new verify-pr skill: A/B load-bearing proof, vacuity check on new
  tests, mock-free wire-oracle harnesses, targeted gates, fixed report/
  verdict/assertions artifact contract, counts-are-sacred rules
- triage skill Stage 2c now names /verify (not just /tmux) as the trigger
  to recommend when a PR's central claim needs behavioral evidence

The verify check-runs ride the issue_comment event, which the finalize
workflow's event == "pull_request" universe structurally excludes, so
they cannot pollute the CI table or the deferred-approval gate.

* feat(triage): teach /verify round continuity and artifact-matched methods

Fold two more hand-verification patterns into the verify lane:

- round continuity: the resolve step snapshots the previous verify report
  (if any) into the agent context before the status upsert overwrites it,
  and the skill re-checks each prior finding at the new head
  (fixed/stands/superseded), scoping new probes to the delta
- harness quality: prefer configuration seams over module interception,
  encode the upstream's real semantics in the fake peer, add decoy targets
- artifact-matched methods: per-commit load-bearing tables for multi-commit
  PRs; workflow/CI PRs get embedded-script replay against real data, repo
  lint gates, and day-one trigger cost math from real event history; every
  new config knob must trace to an observable effect, and default-path
  dispatch combinations get probed
- findings quality: blockers enumerate blast radius, demonstrate the
  sharpest consequence end-to-end when budget allows, and carry a collapsed
  minimal suggested fix preserving the original commit's intent

* feat(triage): host /verify evidence images and encode quantified-A/B rules

Borrow the image-evidence and quantified-verification patterns from
hand-run rounds (QwenLM#7265, QwenLM#7471, QwenLM#7686 r2 and the pr-assets convention):

- publish-verify now hosts agent-produced evidence/*.png on the pr-assets
  branch (verify/pr<N>-<run>-<attempt>/) and appends them below the
  escaped report. Untrusted-payload discipline: strict filename allowlist,
  8-image / 2 MB caps enforced in the find predicates, racing-push retry,
  and every failure degrades to a text-only comment. VERIFY_ASSETS_REMOTE
  is a test seam; the block was dry-run against a local bare remote
  covering hosting, hostile filenames, oversize files, dotfiles, missing
  branch, and no-image runs
- skill: evidence images are named as kebab-case captions binding image to
  claim, before/after pairs over lone after-shots; follow-up rounds lead
  with a previous-finding status table (fixed/stands/superseded/declined,
  with adjudication) and re-measure instead of diffing the old report;
  size/perf claims get measured-metric Δ tables with residual deltas
  accounted for; unreachable branches get the configuration that reaches
  them constructed; defensive guards get their accept path checked against
  real production artifacts, not just mocked rejects

* fix(triage): address /review suggestions on the verify lane

- skill: local invocation resolves --repo and passes it to every gh call
- skill: call out the dependency confound when the base A/B side reuses
  the PR-installed node_modules and the PR touches package.json/lockfile
- workflow: document the pin step's bootstrap logic — issue_comment jobs
  run the default branch's YAML, so base always carries the verify-pr
  skill by the time this job exists

* fix(triage): harden /verify gate, comment budget, and evidence hosting per review

Address review round 5078770575 items 1-3 plus the cheap follow-ups:

- authorize: /verify now requires write from BOTH the PR author (whose
  code runs) and the commenter (who spends a scarce runner slot + model
  budget) — a drive-by account can no longer burn 45 minutes of ecs-qwen
  on someone else's PR; duplicates check once; /tmux and /triage gates
  unchanged. Replayed 8 principal scenarios against a stubbed gh
- authorize acks /verify with the eyes reaction from the always-hosted
  job, so a queued/saturated sandbox pool no longer means total silence
- publish: emit_block escapes FIRST and caps the escaped size (45 KB for
  the report) — a raw-side cap let dense <>& content inflate past
  GitHub's 65,536-char comment limit, 422 the post, and strand the
  running status with no report at all; iconv -c keeps a UTF-8 sequence
  split by the byte cut (likely, given the mandated 中文 summary) from
  shipping broken; replayed: 50 KB dense report -> 45,873-byte body
- publish: image cap is byte-exact (-size -2097153c; find's -2M rounds
  sizes UP to MiB, silently making the documented 2 MB cap 1 MiB), bytes
  must carry the PNG magic (extension is attacker-choosable), duplicate
  sanitized names dedupe instead of overwriting + double-rendering, and
  dropped images are reported in the comment instead of vanishing
- publish: weak terminal notices (cancelled/infra/skipped/n-a) only
  replace this run's own running status; a previous round's real report
  survives as the marker comment and the notice posts fresh
- publish: report.md/assertions.json lookups pin the artifact-dir shape
  and sort (bare find -name order is filesystem-dependent); the verify
  job's verdict.txt lookup sorts likewise
- verify: global npm install runs from RUNNER_TEMP (the persistent
  workspace still holds the PREVIOUS run's tree, whose .npmrc would
  apply to a root install); both cleanup passes remove leftover tmp/
  worktrees (git worktree prune alone only drops metadata); the run step
  no longer re-chowns 50k node_modules files; pr-assets clone sets its
  committer identity once so the racing-push rebase retry can commit
- skill: worktree guidance now tells the agent to remove its base tree
  itself, with the workflow sweep as backstop only

* fix(triage): close runtime-plant and stale-RUNNER_TEMP channels in /verify

Address review round 2 (comment 5079157987) and the CHANGES_REQUESTED
round on the verify lane:

- run step re-sweeps tmp/*-verify-* AFTER npm ci/build and before the
  agent starts: the pin step's sweep runs before PR lifecycle scripts
  (postinstall etc.), which could re-plant a fake artifact dir whose
  zeroed timestamp deterministically wins the sorted collector. From the
  sweep on, only the agent writes those dirs; a steered agent forging its
  own artifacts remains the documented advisory-report residual
- RUNNER_TEMP verify-results/verify-context are rm'd before mkdir: the
  pool is persistent and runner temp hygiene is runner-managed — a stale
  report or previous-report.md from ANOTHER PR must never ride along
- symlinks are stripped from verify-results before upload:
  actions/upload-artifact dereferences them, so a node-planted link would
  exfiltrate whatever it points at into the artifact
- a trusted commenter invoking /verify on a PR whose author lacks write
  now gets an explanation comment from the hosted authorize job instead
  of total silence (the commenter is checked first; drive-by accounts and
  API errors still get nothing); job timeout 45->60 so a slow install can
  never let the JOB limit kill the agent past its own graceful 25m budget
- stale tmp/base-tree (skill's canonical scratch worktree) is removed by
  name at job start — a plain dir isn't git-registered, so the worktree
  sweep alone misses it and the next worktree add would fail
- scripts/tests/qwen-triage-workflow.test.js gains a verify-lane describe
  block: an 8-arm stub-gh replay of the dual principal gate (drive-by
  deny, author-without-write deny + explain flag, self-comment dedupe,
  404 fail-closed, /tmux and /triage unchanged) plus guards for the
  post-prepare sweep placement, the symlink strip, and the RUNNER_TEMP
  resets — the replay found this commit's sweep edit had silently not
  applied, which is exactly the regression class it exists to catch

* fix(triage): close proxy-hijack, gate-bypass, and false-verdict paths in /verify

Address the Codex /review round (19 findings) and the bot's follow-up.
Each fix was replayed locally; the proxy fix has a decisive A/B.

Gate and routing:
- the shell command match is case-insensitive: GitHub Actions expression
  comparisons ignore case, so `@QWEN-CODE /VERIFY` reached the step and
  fell through to the commenter-only branch — running the PR author's
  code with the author never checked
- the verify ack and denial notice require github.event.issue.pull_request:
  /verify on a plain issue was acknowledged but could never report
- publish-verify joins the verify job's per-PR concurrency group, and a
  failed PATCH falls back to posting fresh instead of going silent

Untrusted-input paths:
- the model proxy binds an EPHEMERAL port, reports it through a
  root-owned file, and its health check must echo a per-run nonce with
  the recorded PID alive. A/B with a squatter on 8787: the old code's
  proxy dies EADDRINUSE yet still reports enabled and points qwen at the
  squatter; the new code comes up unaffected on an ephemeral port
- worktree-scoped git config is deleted before hooksPath is resolved:
  `extensions.worktreeConfig` is allowlisted and .git/config.worktree is
  invisible to `git config --local`, so a prior run could set
  core.hooksPath=/ and make the hook sweep's recursive delete walk / as
  root (verified locally). The sweep now also refuses any hooks path
  outside the repository's git dir
- marker-comment lookups accept only bot-owned comments that START with
  the marker: any user can paste the marker and divert the bot into
  PATCHing a stranger's comment
- the upload staging dir is re-flushed after npm lifecycle scripts

Honest verdicts:
- the docs-only classifier no longer uses a pipeline (grep -q made the
  writer take SIGPIPE, so under pipefail a long file list with an early
  code file classified a code PR as docs-only and skipped verification),
  and executable markdown/YAML (.qwen, .github/workflows, scripts) is
  classified as behavioral before the extension rule
- tee's status is checked alongside qwen's: a full results volume made a
  truncated evidence stream publish as pass
- 137 is split by elapsed budget into watchdog timeout vs crash/OOM
- the agent's verdict is honored only for VERDICT=pass with a report and
  zero failed assertions; otherwise the process outcome headlines and
  the scope paragraph says the run did not complete
- verdict.txt is read through a bounded prefix (SIGPIPE under pipefail)

Skill contract corrections: per-commit tables only when the commits are
reachable at depth 2 (else aggregate + Not covered); internal workspace
symlinks must have their realpaths asserted before a base control is
trusted; repo lint gates and event-history cost math are qualified to
what the token-free container can actually run; --repo is never inferred
from `origin` (a fork holds a different PR under the same number).

Tests: 9 new guards, all mutation-verified (reverting each fix turns one
red), including an executable escaping/size-cap/UTF-8 test for the
publisher's own emit_block and a fix to the earlier command-file test,
which matched the tmux job's identically named step.

* fix(triage): re-establish the /verify trust boundary after PR code runs

Third review round (31 findings). The unifying defect: everything the
lane pinned or swept happened BEFORE npm ci/build executed PR-authored
lifecycle scripts as node, so each control could be undone afterwards.

Trust boundary, rebuilt in order before the agent starts:
- kill every process owned by the build user and fail the step if any
  survives — a detached postinstall child could otherwise wait out each
  one-shot sweep and re-plant afterwards
- re-pin .qwen from the base commit again, now root-owned and read-only:
  the prepare step chowns the workspace to node, so a lifecycle script
  could rewrite the very skill that defines /verify-pr
- give the agent a fresh HOME/QWEN_HOME: qwen loads user-scope file
  commands from $HOME/.qwen, and /home/node belongs to the build user,
  so a planted commands/verify-pr.toml could shadow the pinned skill
- the model proxy now requires a per-run bearer token, closing the
  blind-localhost-scan path to an unauthenticated signer for the real
  model credential (a command the agent itself launches still inherits
  it — documented residual, not closed)

Authorization and lifecycle:
- re-verify the PR author's write permission at execution time and pin
  the authorized head OID; refuse if the checked-out HEAD^2 differs, so
  a push during the runner wait cannot smuggle in unreviewed code
- validate each principal separately: an empty author vanished in word
  splitting and left only the commenter checked
- honor MAINTAINER_ECS_RUNNER_DISABLED with an explicit notice instead
  of queueing forever against a disabled pool
- status comments carry a machine state marker; inferring 'running' from
  prose let a report quoting that sentence be overwritten
- previous-report.md snapshots the newest substantive report, never a
  weak/cancelled notice, so prior findings survive into the next round
- bot-identity lookup failures fail closed instead of widening the
  ownership filter to every user's comments
- publish-verify uses a per-run concurrency group: a per-PR group holds
  only one pending job, so a second /verify could cancel a completed
  run's pending publisher

Correctness:
- install/build failures are classified: signals, ENOSPC, registry and
  network errors are infra-error, not a PR verdict
- watchdog classification measures the child's own elapsed time, not
  shell-global $SECONDS which includes proxy setup
- assertions.json must be three non-negative integers with a positive
  total and total == pass + fail before it counts as evidence
- the proxy keeps its upstream deadline armed until the body ends and
  aborts upstream when the client disconnects
- cleanups remove .qwen/tmp itself: PR code can make it a symlink, and
  globbing below it deleted the target's contents as root (verified)
- emit_block materializes the escaped text and truncates on a character
  boundary via node — iconv -c passes an incomplete trailing sequence
  through on BSD (measured), which the new test caught

Skill: local mode requires the same isolation CI provides and must not
assume HEAD^1/HEAD^2 on a plain head checkout; shallow boundaries make
rev-list counts unreliable for per-commit claims; never run
scripts/lint.js with no arguments (it runs prettier --write and rewrites
the tree under the harnesses); a vacuity check must fail the intended
assertion, not the import. pr-workflow.md now says both sandboxed lanes
need the author to have write, so triage stops recommending a
guaranteed denial on external PRs.

Tests: 9 more guards, all mutation-verified, including executable
replays of the docs-only classifier (SIGPIPE + executable-markdown
cases), the uppercase-command gate, the empty-principal deny, and the
untrusted-image hosting path against a bare pr-assets remote.

* test(triage): pass the classifier fixture through a file, not argv

The new docs-only classifier replay passed on macOS and failed on CI
with `Cannot read properties of undefined (reading 'trim')`: its
60,001-entry fixture is ~889 KB and was passed as a single argv element.
Linux caps one argument at MAX_ARG_STRLEN (128 KB), so the spawn failed
with E2BIG and stdout was undefined; macOS has no per-argument limit and
only a ~1 MB total, so the same call succeeded locally (verified both).

Write the list to a temp file and pass the path. The harness now also
asserts the spawn succeeded, so a future spawn failure reports itself
instead of surfacing as a TypeError on undefined output.

* fix(triage): make the /verify report match what the run actually produced

Three publisher findings, all introduced by my own previous round:

- an artifact download failure (the step is continue-on-error) let the
  full-report path run with no results: the headline read 'completed' and
  the scope paragraph claimed the A/B, the harnesses and the gates had
  run when nothing had been delivered. The download outcome is now an
  input, and its failure gets its own body saying the results could not
  be retrieved
- the prepare-failure branch ignored the verdict the prepare step had
  just computed, so an install killed by a registry outage or OOM
  (classified infra-error) still told the author 'this is treated as a
  PR failure verdict rather than an infrastructure failure' — the exact
  opposite. It now branches on the verdict, and an infra-classified
  prepare failure is a weak body that cannot overwrite a real report
- weak notices were being snapshotted as the follow-up round's
  previous-report.md: they lack the running marker, so 'newest
  non-running comment' selected them. Bodies that carry findings now
  mark themselves (qwen-triage:verify-substantive) and the snapshot
  selects on that marker. A/B on the real jq: report A then cancelled B
  now snapshots A (101), the old filter picked B (102)

Tests: 4 more guards, all mutation-verified — the publisher is rendered
for each outcome with a stubbed gh and the assertions read the body it
would post, and the snapshot test runs the workflow's own jq program
verbatim against a paginate-shaped fixture.

* fix(triage): stop PR build output from masquerading as an infra failure

Two review findings plus a test-helper hazard:

- classify_failure grepped the prepare log for bare words like ENOSPC
  and ETIMEDOUT, but that log is written by PR-controlled code: a
  genuine build failure that merely prints 'expected ETIMEDOUT to equal
  ok' would be published as an infrastructure incident, telling the
  author to re-run something that fails identically. The patterns are
  now anchored to lines only npm's reporter or the kernel emits
  ('npm ERR! code E…', 'npm ERR! network …', kernel OOM, bare 'Killed');
  a signal exit still needs no log evidence. Replayed 10 cells: four
  PR-authored logs quoting infra words stay 'fail', five real
  diagnostics and one signal exit are 'infra-error'
- the two execution-time controls added last round — re-verifying the
  author's permission after the runner wait, and refusing a head that
  moved since authorization — had no tests. Both are now executed:
  the re-auth snippet against a stubbed permission API (write proceeds
  and pins head_oid; read skips with a publishable reason), and the pin
  step against a real git repo with a real merge commit (matching head
  proceeds, moved head exits non-zero)
- add a stepIn(job, step) test helper. Several step names exist in both
  the tmux and verify jobs, and the unscoped step() returns the first
  match, so a verify-lane assertion silently tests the tmux copy — that
  has now bitten this suite three times, including in this commit.

* docs(triage): teach verify-pr test-only PRs, differential oracles, gate liveness

Fold techniques from the round-2 verification on QwenLM#7620 (an ANSI parser
PR) that the skill had no equivalent for:

- test-only PRs get their own method: a mutation A/B across TEST FILES
  (same mutants of the unmodified production file, only the test file
  swapped), reporting killed/total on both sides, requiring that no
  mutant regressed from killed to survived, checking that the killing
  assertion is the one the commit claims to have strengthened, and
  adjudicating every survivor as coverage gap or defect with independent
  evidence rather than by inspection
- when the code emulates a known implementation, that implementation is
  the oracle: feed identical input to both and report disagreement
  counts per side, lift reference tables verbatim out of the shipped
  dependency, and build the corpus from bytes captured off a real
  producer alongside synthesized sweeps
- prove a gate is live before citing it: plant a violation the linter
  must catch, confirm it is reported, remove it — a linter that matched
  no files exits 0 exactly like one that passed
- attribute pre-existing failures by byte-identical failing file AND
  test names on both sides, with deltas, not just totals
- when the base is far behind, verify the merge: trial-merge into
  current main, confirm it is conflict-free, and re-run the affected
  suite on the merged tree
- round continuity gains its one legitimate shortcut: a production file
  proven byte-identical (sha256 quoted at both heads) carries prior
  evidence forward by construction

* style(triage): reflow verify-pr skill to prettier's markdown wrapping

The previous commit's added paragraphs were hand-wrapped and prettier
--check flagged the file; the repo runs prettier over all of it.

* test(triage): cover the disabled-runner-pool notice

The kill-switch path had no test: a refactor could drop the notice and
leave a /verify request acknowledged with 👀 but permanently unanswered,
since the verify job refuses to start and publish-verify skips with it.

Fold the step into the existing PR-guard loop (now scoped through
stepIn, so it cannot match a same-named step in another job) and assert
the parts that make the answer useful — the kill-switch and permission
conditions, both languages, the alternative it points at, and the verify
job's own exclusion of the disabled pool. All three mutations turn it
red: removing the step, dropping its PR guard, or letting the verify job
queue against the disabled pool.

* fix(triage): repair a step-killing PIPESTATUS read and six forgeable controls

Sixth review round, 12 findings. Several are regressions from my own two
previous rounds; the first would have broken every single run.

- `AGENT_STATUS=${PIPESTATUS[0]}` is itself a command and resets
  PIPESTATUS, so the next line's ${PIPESTATUS[1]} was unset and `set -u`
  aborted the step immediately after the agent finished — before artifact
  collection, the verdict, or anything else. Verified by replaying the
  exact structure: 'PIPESTATUS[1]: unbound variable'. Both elements are
  now snapshotted in one command
- concurrency predicates were broader than the job conditions they guard,
  and GitHub evaluates concurrency BEFORE the job `if`: a /verify comment
  entered the triage job's shared per-PR group (where it could displace a
  pending /triage and then skip), and a /verify queued while the runner
  kill switch was on did the same to a real verification. Both predicates
  now match their job's runnable set exactly
- an outward-resolving .git/hooks entry was only warned about and left in
  place, so the next root-owned git command would run it. It is now
  unlinked without traversing its target, a root-owned hooks directory is
  restored, and core.hooksPath is unset
- the second .qwen pin re-derived HEAD^1 from git metadata after the
  workspace, including .git, had been handed to the build user. The base
  OID is now recorded while .git is still root-owned and the re-pin
  archives that content-addressed OID
- classify_failure took both of its inputs from PR-controlled sources: a
  lifecycle script can exit with a signal status and can print any line
  the log patterns matched, turning its own deterministic breakage into
  'infrastructure, please re-run' — which hid the failure and preserved a
  stale report. No infra verdict is derivable there, so the prepare step
  reports `fail` and lets the embedded log speak for itself
- cleanups descended through PR-writable parents: `.qwen` itself can be a
  symlink, and the worktree sweep trusted git metadata with only a lexical
  prefix check. Symlinks are unlinked without traversal and worktree paths
  must canonicalize inside the workspace. Replayed all three escapes
- skipped and docs-only outcomes upload no artifact, so the new
  download-failure branch pre-empted them and made their real reason
  unreachable; they are answered first now
- a run that crashed before writing report.md still claimed the
  substantive marker, letting a headline overwrite the previous round's
  evidence. The marker now requires a report

Skill: the byte-identical shortcut needs the whole input closure, not one
file hash; the credential-free local path cannot call `gh` at all (fetch
the metadata outside and mount it read-only); and the A/B base is
`baseRefOid` in local mode, not `HEAD^1`.

Tests: 7 new guards plus 4 updated to the new shapes, all
mutation-verified (50/50).

* fix(triage): answer dropped /verify requests and prove the proxy rejects

Maintainer review (yiliang114), 7 items:

- a third /verify while two runs are in flight is dropped by the
  concurrency group with no job and therefore no comment. The hosted
  authorize job now counts this workflow's other in-flight runs and says
  so; an API hiccup leaves the request alone rather than denying it
- the proxy's bearer check had no executable test. It now starts the real
  proxy against a real upstream and issues real requests: no header and
  a wrong token are 401, this run's token is 200, and a route other than
  /chat/completions is 403 — with the health endpoint echoing the nonce
- the 502 path forwarded the raw upstream error, which can name resolved
  hosts and TLS detail to PR code. It logs server-side and returns a
  generic failure
- publish-verify inherited the 360-minute default; it downloads one
  artifact and posts one comment, so it is bounded at 10
- removing the log classifier last round left the comment block it
  replaced, which still said failures are classified from the exit status
  and the log. Deleted
- that removal also left every install failure reported as the PR's
  fault, including a registry outage. There is exactly one signal here PR
  code cannot write — asking the registry ourselves, as root, with the
  container's resolver — so an install failure is downgraded to
  infra-error only when that probe fails. It proves reachability now
  rather than at failure time, so it can only ever downgrade, never
  confirm; a build failure has no equivalent and stays the tree's problem
- the skill's local-invocation warning ran into the preceding sentence,
  which GFM renders as one paragraph

Tests: 5 new guards, all mutation-verified (55/55).

* fix(triage): resolve hooks hermetically and mirror symlink guards at job end

Maintainer review round (doudouOUC), 6 findings. Two were Critical and
both reproduced:

- the hooks sweep resolved its path with the ambient git config in play.
  With a global core.hooksPath set — which the reviewer has and I do not,
  which is why my earlier replay showed a false pass — `git rev-parse
  --git-path hooks` returns that global path, the in-git-dir guard reads
  'outside', and a planted `.git/hooks` symlink survives untouched. A/B:
  old code leaves the symlink under a global hooksPath, new code removes
  it in both environments and never touches the link target. Resolution
  now runs with GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM pointed at
  /dev/null
- the END-of-job cleanup still used the bare `rm -rf .qwen/tmp` that the
  start-of-job cleaner was hardened against two rounds ago. The agent
  executes PR code between the two, so the end is no safer than the
  start: it now unlinks symlinks without descending and canonicalizes
  worktree paths inside the workspace before deleting

Plus four suggestions, all valid:

- the saturation notice counted this workflow's in-flight runs across
  every PR while the concurrency group is per-PR, so a run on another PR
  would trigger a warning about a queue that does not exist. It now
  matches on the PR title (the only per-PR handle an issue_comment run
  record carries) and stays silent when that cannot be resolved
- the skill recommended `require.resolve` for the workspace-realpath
  check; these packages are ESM-only with import-only exports, so it
  throws ERR_PACKAGE_PATH_NOT_EXPORTED and reads like a missing module.
  Verified, and replaced with `readlink -f node_modules/@qwen-code/...`
- the symlink-escape test inherited the developer's git config, which is
  what hid the first finding. It now runs with global/system config
  neutralized AND repeats the case with a global core.hooksPath planted
- the publisher's build-phase arm was never rendered by any test (every
  case used 'install'), so a typo in that command name would have
  shipped. Now covered, along with an unrecognized phase

Mutation-verified 4/4. The hooks guard needed a discriminating assertion:
git's own `*.sample` files must survive the sweep, because the
outward-path fallback removes the whole directory and would otherwise
satisfy a bare 'planted hook is gone' check.

* fix(triage): count only /verify runs for saturation, and test the PATCH arm

Bot review round, 2 suggestions, both valid:

- the saturation notice matched runs by PR title, which narrowed to this
  PR but not to /verify. /triage and /tmux live in their own concurrency
  groups, so two of those in flight would warn about a verify queue that
  is actually empty. It now also requires the run to have a job named
  'verify' — the run record carries no command, but its job list does.
  Replayed: two non-verify runs stay silent, two verify runs warn
- every publish fixture returned an empty comments listing, so the PATCH
  arm was never executed: a broken PATCH would have stranded the running
  status comment and posted a duplicate below it, with the suite green.
  The publisher now runs against a stubbed listing and the test asserts
  which verb went to which comment id — bot-owned live status is PATCHed
  in place, an absent comment posts fresh, and a marker comment owned by
  someone else is left alone and posted around

Mutation-verified 3/3: counting every command, never PATCHing, and
accepting foreign-owned markers each turn one test red.

Two stub bugs found while writing these, both mine and both silent:
${*#pattern} applies per positional parameter rather than to the joined
string (yielding a wrong run id), and the paginate fixture needs one
array per page, not an array of pages.

* fix(triage): fix the real silent drop and drop the step built on a wrong premise

Review round 4. The blocker was mine twice over: the saturation notice I
added last round had GitHub's concurrency semantics backwards, and the
silent drop it claimed to cover was somewhere else entirely.

- GitHub cancels the OLDER pending run in a group and admits the new one
  (confirmed against the workflow-syntax reference). My step told the
  person who had just typed /verify that their request might be dropped,
  when theirs is the one that runs — and said nothing to the person whose
  queued run actually died. This PR already had it right in
  publish-verify's own comment, so the file contradicted itself and the
  user-facing copy followed the wrong half. The step is removed rather
  than reworded: with the fix below there is nothing left for it to warn
  about, and it cost 2+N API calls on every /verify.
- the actual drop: a verify job cancelled while still PENDING never
  reaches a runner, so its outputs block — where the
  "|| github.event.issue.number" fallback lived — is never evaluated.
  publish-verify then read an empty PR_NUMBER, hit its own guard and
  exited 0, making the cancelled branch unreachable in exactly the
  scenario that produces cancellations. The fallback now lives where the
  value is read. Reproduced both arms by executing the real step: with a
  number the cancelled notice posts, with an empty one it only warns.
- same one-line class in publish-tmux, fixed alongside.

Two copy defects from the classifier removal, both mis-attribution
pointed the other way:

- the infra-error body still named a signal/OOM kill and a full disk,
  none of which the current prepare step can produce — infra-error now
  requires npm ci to fail AND the registry probe to fail. It names that
  condition only, and offers a re-run instead of asserting it is the fix.
- the code comment above it still described the deleted classifier.

Also fixes the indentation break an earlier scripted edit left in the
publish body builder, and replaces the saturation test with one that
executes the cancelled path. Mutation-verified 2/2; the copy needed its
own guard, since reverting the wording alone left every test green.

* docs(triage): teach verify-pr survivor accounting and observability regressions

Fold techniques from the re-verification on QwenLM#7709 that the skill had no
equivalent for:

- the mutation matrix must report the mutations that changed NOTHING, not
  only the ones that failed. Each survivor gets classified as an ordinary
  coverage gap or as dead code — a guard whose deletion leaves every test
  green is one of those two, and the difference is what the author needs.
  Survivors mirroring a pre-existing gap are labelled as such, and the set
  is framed as completeness reporting rather than merge conditions
- the sharper case that report demonstrates: a test that passes for the
  WRONG REASON. If deleting the new guard leaves its own new test green,
  that test is pinned by an earlier early-return, not by the change, and
  asserts nothing about it. Name what actually pins it
- and do not generalize from one dead guard to its siblings: the same
  report shows a clause that is unreachable on one path while being the
  only protection on another. Check each, report the contrast
- observability regressions: when a change suppresses output, follow the
  value before calling the suppression correct. A bare catch on the path
  plus a field with no readers anywhere in the repo means the cause is now
  unobservable even in devtools — a real loss that no behavioural
  assertion can see
- report structure gains a Corrections section: when an earlier round or
  bot comment described the code inaccurately, state the correct fact with
  evidence and label it as a correction to the description, not a request
  to change code. A wrong description left standing costs the next reader
  more than the original finding did

---------

Co-authored-by: wenshao <wenshao@example.com>
wenshao added a commit to wenshao/qwen-code that referenced this pull request Jul 26, 2026
…n omits the Han flag (QwenLM#7739)

* fix(review): recover the bilingual register from the live PR when the plan omits the Han flag

The posted review body renders bilingually (English, with the full Chinese
version collapsed under it) only when the plan `compose-review` reads carries
`prDescriptionHasHan: true`. That flag is written in exactly one place —
`fetch-pr` — and read in exactly one place, from a plan path the orchestrator
supplies. Two gaps follow: a `plan-diff` plan never records the flag, and a run
that improvises the pipeline can hand `compose-review` a plan that is not
`fetch-pr`'s report at all. Either way the switch fails safe to English, and a
Chinese-authored PR gets an English-only review — observed on QwenLM#7686, where the
four bot reviews off a proper plan were bilingual and the one off an improvised
plan was not.

When the flag is absent but the plan still names the PR (`ownerRepo` +
`prNumber`), recover the signal from the live description with a single
`gh pr view`, and test that recovered text for Han. This runs only on the
absent-flag path: a recorded `true`/`false` is authoritative and spends no
network, so every healthy `fetch-pr` review is unchanged. The signal stays the
CLI's own — the real PR body, which the caller cannot forge — so the recovery
tightens the "caller cannot toggle the register" property rather than loosening
it, and any failure of the fetch falls back to English so the language can never
take the review down.

* refactor(review): reuse roster's isPositivePrNumber in the bilingual recovery

`bilingualFromPlan`'s local `positivePrNumber` duplicated the exact PR-number
validation already in `roster.ts` (positive integer, all-digit string, reject
null/0/empty). Two copies of the same rule invite a silent divergence: a future
change to how a PR number is validated, applied to only one, would make the
bilingual recovery and `requiredAgents` disagree on the same plan's PR identity.
Export the roster helper and reuse it, coercing to the string form the
`gh pr view` call needs at the one call site.

* fix(review): route compose-review's gh call via the PR host on GitHub Enterprise

The bilingual body-language recovery added a `gh pr view` call inside
compose-review, but its CLI handler never called `setGhHost` — unlike
fetch-pr/submit/pr-context/comment-status/presubmit. On a GitHub Enterprise PR
whose plan lacks `prDescriptionHasHan` but carries the PR identity, the recovery
fetch would target github.com, fail, and compose an English-only body that
disagrees with the bilingual body `submit` (which does route by host) posts.

Give compose-review a `--host` option and call `setGhHost(host)` in the handler,
mirroring the sibling subcommands; add it to the skill's Enterprise host list and
the Step 6 invocation. Covered by a test that drives the handler with --host and
asserts the routing took.

* fix(review): strip the prBodyFetcher test seam at the compose-review boundary

`prBodyFetcher` is a unit-test seam, but unlike `env` it was not stripped from
the model-written state JSON. A state JSON carrying `"prBodyFetcher": "suppress"`
survives `JSON.parse`, reaches `bilingualFromPlan`, is called, throws, and drops
the Chinese fold through the fail-safe — letting the caller suppress a fold that
the plan's own signal would have rendered. Strip it in the handler the same way
`env` is stripped, and correct the field doc, which wrongly claimed a model
could not supply one.

* fix(review): strip prBodyFetcher at the submit boundary; pin fetchPrBodyViaGh and handler stripping with tests (QwenLM#7739)

* fix(review): pin the submit-boundary prBodyFetcher strip; soften SKILL.md wording (QwenLM#7739)

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
wenshao added a commit to wenshao/qwen-code that referenced this pull request Jul 27, 2026
…LM#7753)

* feat(triage): add sandboxed /verify deep-verification lane

@qwen-code /verify on a PR now runs a local-verification-style evidence
round in the isolated /tmux sandbox contract (container, token-free agent
env, loopback model proxy, author-write gate) and publishes the report via
a separate PR-code-free job:

- new verify job: merge-ref checkout at depth 2 (base tip + PR head for
  A/B), skills pinned from base so the tree under test can never rewrite
  its own verifier, PR-planted tmp/*-verify-* artifacts dropped, git
  exec-vector sweep for the persistent workspace, agent verdict
  allowlisted before it reaches workflow outputs
- new publish-verify job: upserts one marker comment (running status ->
  final report), HTML-escapes the untrusted report, reports skip/na/
  prepare-fail/infra outcomes explicitly since /verify is always an
  explicit request
- new verify-pr skill: A/B load-bearing proof, vacuity check on new
  tests, mock-free wire-oracle harnesses, targeted gates, fixed report/
  verdict/assertions artifact contract, counts-are-sacred rules
- triage skill Stage 2c now names /verify (not just /tmux) as the trigger
  to recommend when a PR's central claim needs behavioral evidence

The verify check-runs ride the issue_comment event, which the finalize
workflow's event == "pull_request" universe structurally excludes, so
they cannot pollute the CI table or the deferred-approval gate.

* feat(triage): teach /verify round continuity and artifact-matched methods

Fold two more hand-verification patterns into the verify lane:

- round continuity: the resolve step snapshots the previous verify report
  (if any) into the agent context before the status upsert overwrites it,
  and the skill re-checks each prior finding at the new head
  (fixed/stands/superseded), scoping new probes to the delta
- harness quality: prefer configuration seams over module interception,
  encode the upstream's real semantics in the fake peer, add decoy targets
- artifact-matched methods: per-commit load-bearing tables for multi-commit
  PRs; workflow/CI PRs get embedded-script replay against real data, repo
  lint gates, and day-one trigger cost math from real event history; every
  new config knob must trace to an observable effect, and default-path
  dispatch combinations get probed
- findings quality: blockers enumerate blast radius, demonstrate the
  sharpest consequence end-to-end when budget allows, and carry a collapsed
  minimal suggested fix preserving the original commit's intent

* feat(triage): host /verify evidence images and encode quantified-A/B rules

Borrow the image-evidence and quantified-verification patterns from
hand-run rounds (QwenLM#7265, QwenLM#7471, QwenLM#7686 r2 and the pr-assets convention):

- publish-verify now hosts agent-produced evidence/*.png on the pr-assets
  branch (verify/pr<N>-<run>-<attempt>/) and appends them below the
  escaped report. Untrusted-payload discipline: strict filename allowlist,
  8-image / 2 MB caps enforced in the find predicates, racing-push retry,
  and every failure degrades to a text-only comment. VERIFY_ASSETS_REMOTE
  is a test seam; the block was dry-run against a local bare remote
  covering hosting, hostile filenames, oversize files, dotfiles, missing
  branch, and no-image runs
- skill: evidence images are named as kebab-case captions binding image to
  claim, before/after pairs over lone after-shots; follow-up rounds lead
  with a previous-finding status table (fixed/stands/superseded/declined,
  with adjudication) and re-measure instead of diffing the old report;
  size/perf claims get measured-metric Δ tables with residual deltas
  accounted for; unreachable branches get the configuration that reaches
  them constructed; defensive guards get their accept path checked against
  real production artifacts, not just mocked rejects

* fix(triage): address /review suggestions on the verify lane

- skill: local invocation resolves --repo and passes it to every gh call
- skill: call out the dependency confound when the base A/B side reuses
  the PR-installed node_modules and the PR touches package.json/lockfile
- workflow: document the pin step's bootstrap logic — issue_comment jobs
  run the default branch's YAML, so base always carries the verify-pr
  skill by the time this job exists

* fix(triage): harden /verify gate, comment budget, and evidence hosting per review

Address review round 5078770575 items 1-3 plus the cheap follow-ups:

- authorize: /verify now requires write from BOTH the PR author (whose
  code runs) and the commenter (who spends a scarce runner slot + model
  budget) — a drive-by account can no longer burn 45 minutes of ecs-qwen
  on someone else's PR; duplicates check once; /tmux and /triage gates
  unchanged. Replayed 8 principal scenarios against a stubbed gh
- authorize acks /verify with the eyes reaction from the always-hosted
  job, so a queued/saturated sandbox pool no longer means total silence
- publish: emit_block escapes FIRST and caps the escaped size (45 KB for
  the report) — a raw-side cap let dense <>& content inflate past
  GitHub's 65,536-char comment limit, 422 the post, and strand the
  running status with no report at all; iconv -c keeps a UTF-8 sequence
  split by the byte cut (likely, given the mandated 中文 summary) from
  shipping broken; replayed: 50 KB dense report -> 45,873-byte body
- publish: image cap is byte-exact (-size -2097153c; find's -2M rounds
  sizes UP to MiB, silently making the documented 2 MB cap 1 MiB), bytes
  must carry the PNG magic (extension is attacker-choosable), duplicate
  sanitized names dedupe instead of overwriting + double-rendering, and
  dropped images are reported in the comment instead of vanishing
- publish: weak terminal notices (cancelled/infra/skipped/n-a) only
  replace this run's own running status; a previous round's real report
  survives as the marker comment and the notice posts fresh
- publish: report.md/assertions.json lookups pin the artifact-dir shape
  and sort (bare find -name order is filesystem-dependent); the verify
  job's verdict.txt lookup sorts likewise
- verify: global npm install runs from RUNNER_TEMP (the persistent
  workspace still holds the PREVIOUS run's tree, whose .npmrc would
  apply to a root install); both cleanup passes remove leftover tmp/
  worktrees (git worktree prune alone only drops metadata); the run step
  no longer re-chowns 50k node_modules files; pr-assets clone sets its
  committer identity once so the racing-push rebase retry can commit
- skill: worktree guidance now tells the agent to remove its base tree
  itself, with the workflow sweep as backstop only

* fix(triage): close runtime-plant and stale-RUNNER_TEMP channels in /verify

Address review round 2 (comment 5079157987) and the CHANGES_REQUESTED
round on the verify lane:

- run step re-sweeps tmp/*-verify-* AFTER npm ci/build and before the
  agent starts: the pin step's sweep runs before PR lifecycle scripts
  (postinstall etc.), which could re-plant a fake artifact dir whose
  zeroed timestamp deterministically wins the sorted collector. From the
  sweep on, only the agent writes those dirs; a steered agent forging its
  own artifacts remains the documented advisory-report residual
- RUNNER_TEMP verify-results/verify-context are rm'd before mkdir: the
  pool is persistent and runner temp hygiene is runner-managed — a stale
  report or previous-report.md from ANOTHER PR must never ride along
- symlinks are stripped from verify-results before upload:
  actions/upload-artifact dereferences them, so a node-planted link would
  exfiltrate whatever it points at into the artifact
- a trusted commenter invoking /verify on a PR whose author lacks write
  now gets an explanation comment from the hosted authorize job instead
  of total silence (the commenter is checked first; drive-by accounts and
  API errors still get nothing); job timeout 45->60 so a slow install can
  never let the JOB limit kill the agent past its own graceful 25m budget
- stale tmp/base-tree (skill's canonical scratch worktree) is removed by
  name at job start — a plain dir isn't git-registered, so the worktree
  sweep alone misses it and the next worktree add would fail
- scripts/tests/qwen-triage-workflow.test.js gains a verify-lane describe
  block: an 8-arm stub-gh replay of the dual principal gate (drive-by
  deny, author-without-write deny + explain flag, self-comment dedupe,
  404 fail-closed, /tmux and /triage unchanged) plus guards for the
  post-prepare sweep placement, the symlink strip, and the RUNNER_TEMP
  resets — the replay found this commit's sweep edit had silently not
  applied, which is exactly the regression class it exists to catch

* fix(triage): close proxy-hijack, gate-bypass, and false-verdict paths in /verify

Address the Codex /review round (19 findings) and the bot's follow-up.
Each fix was replayed locally; the proxy fix has a decisive A/B.

Gate and routing:
- the shell command match is case-insensitive: GitHub Actions expression
  comparisons ignore case, so `@QWEN-CODE /VERIFY` reached the step and
  fell through to the commenter-only branch — running the PR author's
  code with the author never checked
- the verify ack and denial notice require github.event.issue.pull_request:
  /verify on a plain issue was acknowledged but could never report
- publish-verify joins the verify job's per-PR concurrency group, and a
  failed PATCH falls back to posting fresh instead of going silent

Untrusted-input paths:
- the model proxy binds an EPHEMERAL port, reports it through a
  root-owned file, and its health check must echo a per-run nonce with
  the recorded PID alive. A/B with a squatter on 8787: the old code's
  proxy dies EADDRINUSE yet still reports enabled and points qwen at the
  squatter; the new code comes up unaffected on an ephemeral port
- worktree-scoped git config is deleted before hooksPath is resolved:
  `extensions.worktreeConfig` is allowlisted and .git/config.worktree is
  invisible to `git config --local`, so a prior run could set
  core.hooksPath=/ and make the hook sweep's recursive delete walk / as
  root (verified locally). The sweep now also refuses any hooks path
  outside the repository's git dir
- marker-comment lookups accept only bot-owned comments that START with
  the marker: any user can paste the marker and divert the bot into
  PATCHing a stranger's comment
- the upload staging dir is re-flushed after npm lifecycle scripts

Honest verdicts:
- the docs-only classifier no longer uses a pipeline (grep -q made the
  writer take SIGPIPE, so under pipefail a long file list with an early
  code file classified a code PR as docs-only and skipped verification),
  and executable markdown/YAML (.qwen, .github/workflows, scripts) is
  classified as behavioral before the extension rule
- tee's status is checked alongside qwen's: a full results volume made a
  truncated evidence stream publish as pass
- 137 is split by elapsed budget into watchdog timeout vs crash/OOM
- the agent's verdict is honored only for VERDICT=pass with a report and
  zero failed assertions; otherwise the process outcome headlines and
  the scope paragraph says the run did not complete
- verdict.txt is read through a bounded prefix (SIGPIPE under pipefail)

Skill contract corrections: per-commit tables only when the commits are
reachable at depth 2 (else aggregate + Not covered); internal workspace
symlinks must have their realpaths asserted before a base control is
trusted; repo lint gates and event-history cost math are qualified to
what the token-free container can actually run; --repo is never inferred
from `origin` (a fork holds a different PR under the same number).

Tests: 9 new guards, all mutation-verified (reverting each fix turns one
red), including an executable escaping/size-cap/UTF-8 test for the
publisher's own emit_block and a fix to the earlier command-file test,
which matched the tmux job's identically named step.

* fix(triage): re-establish the /verify trust boundary after PR code runs

Third review round (31 findings). The unifying defect: everything the
lane pinned or swept happened BEFORE npm ci/build executed PR-authored
lifecycle scripts as node, so each control could be undone afterwards.

Trust boundary, rebuilt in order before the agent starts:
- kill every process owned by the build user and fail the step if any
  survives — a detached postinstall child could otherwise wait out each
  one-shot sweep and re-plant afterwards
- re-pin .qwen from the base commit again, now root-owned and read-only:
  the prepare step chowns the workspace to node, so a lifecycle script
  could rewrite the very skill that defines /verify-pr
- give the agent a fresh HOME/QWEN_HOME: qwen loads user-scope file
  commands from $HOME/.qwen, and /home/node belongs to the build user,
  so a planted commands/verify-pr.toml could shadow the pinned skill
- the model proxy now requires a per-run bearer token, closing the
  blind-localhost-scan path to an unauthenticated signer for the real
  model credential (a command the agent itself launches still inherits
  it — documented residual, not closed)

Authorization and lifecycle:
- re-verify the PR author's write permission at execution time and pin
  the authorized head OID; refuse if the checked-out HEAD^2 differs, so
  a push during the runner wait cannot smuggle in unreviewed code
- validate each principal separately: an empty author vanished in word
  splitting and left only the commenter checked
- honor MAINTAINER_ECS_RUNNER_DISABLED with an explicit notice instead
  of queueing forever against a disabled pool
- status comments carry a machine state marker; inferring 'running' from
  prose let a report quoting that sentence be overwritten
- previous-report.md snapshots the newest substantive report, never a
  weak/cancelled notice, so prior findings survive into the next round
- bot-identity lookup failures fail closed instead of widening the
  ownership filter to every user's comments
- publish-verify uses a per-run concurrency group: a per-PR group holds
  only one pending job, so a second /verify could cancel a completed
  run's pending publisher

Correctness:
- install/build failures are classified: signals, ENOSPC, registry and
  network errors are infra-error, not a PR verdict
- watchdog classification measures the child's own elapsed time, not
  shell-global $SECONDS which includes proxy setup
- assertions.json must be three non-negative integers with a positive
  total and total == pass + fail before it counts as evidence
- the proxy keeps its upstream deadline armed until the body ends and
  aborts upstream when the client disconnects
- cleanups remove .qwen/tmp itself: PR code can make it a symlink, and
  globbing below it deleted the target's contents as root (verified)
- emit_block materializes the escaped text and truncates on a character
  boundary via node — iconv -c passes an incomplete trailing sequence
  through on BSD (measured), which the new test caught

Skill: local mode requires the same isolation CI provides and must not
assume HEAD^1/HEAD^2 on a plain head checkout; shallow boundaries make
rev-list counts unreliable for per-commit claims; never run
scripts/lint.js with no arguments (it runs prettier --write and rewrites
the tree under the harnesses); a vacuity check must fail the intended
assertion, not the import. pr-workflow.md now says both sandboxed lanes
need the author to have write, so triage stops recommending a
guaranteed denial on external PRs.

Tests: 9 more guards, all mutation-verified, including executable
replays of the docs-only classifier (SIGPIPE + executable-markdown
cases), the uppercase-command gate, the empty-principal deny, and the
untrusted-image hosting path against a bare pr-assets remote.

* test(triage): pass the classifier fixture through a file, not argv

The new docs-only classifier replay passed on macOS and failed on CI
with `Cannot read properties of undefined (reading 'trim')`: its
60,001-entry fixture is ~889 KB and was passed as a single argv element.
Linux caps one argument at MAX_ARG_STRLEN (128 KB), so the spawn failed
with E2BIG and stdout was undefined; macOS has no per-argument limit and
only a ~1 MB total, so the same call succeeded locally (verified both).

Write the list to a temp file and pass the path. The harness now also
asserts the spawn succeeded, so a future spawn failure reports itself
instead of surfacing as a TypeError on undefined output.

* fix(triage): make the /verify report match what the run actually produced

Three publisher findings, all introduced by my own previous round:

- an artifact download failure (the step is continue-on-error) let the
  full-report path run with no results: the headline read 'completed' and
  the scope paragraph claimed the A/B, the harnesses and the gates had
  run when nothing had been delivered. The download outcome is now an
  input, and its failure gets its own body saying the results could not
  be retrieved
- the prepare-failure branch ignored the verdict the prepare step had
  just computed, so an install killed by a registry outage or OOM
  (classified infra-error) still told the author 'this is treated as a
  PR failure verdict rather than an infrastructure failure' — the exact
  opposite. It now branches on the verdict, and an infra-classified
  prepare failure is a weak body that cannot overwrite a real report
- weak notices were being snapshotted as the follow-up round's
  previous-report.md: they lack the running marker, so 'newest
  non-running comment' selected them. Bodies that carry findings now
  mark themselves (qwen-triage:verify-substantive) and the snapshot
  selects on that marker. A/B on the real jq: report A then cancelled B
  now snapshots A (101), the old filter picked B (102)

Tests: 4 more guards, all mutation-verified — the publisher is rendered
for each outcome with a stubbed gh and the assertions read the body it
would post, and the snapshot test runs the workflow's own jq program
verbatim against a paginate-shaped fixture.

* fix(triage): stop PR build output from masquerading as an infra failure

Two review findings plus a test-helper hazard:

- classify_failure grepped the prepare log for bare words like ENOSPC
  and ETIMEDOUT, but that log is written by PR-controlled code: a
  genuine build failure that merely prints 'expected ETIMEDOUT to equal
  ok' would be published as an infrastructure incident, telling the
  author to re-run something that fails identically. The patterns are
  now anchored to lines only npm's reporter or the kernel emits
  ('npm ERR! code E…', 'npm ERR! network …', kernel OOM, bare 'Killed');
  a signal exit still needs no log evidence. Replayed 10 cells: four
  PR-authored logs quoting infra words stay 'fail', five real
  diagnostics and one signal exit are 'infra-error'
- the two execution-time controls added last round — re-verifying the
  author's permission after the runner wait, and refusing a head that
  moved since authorization — had no tests. Both are now executed:
  the re-auth snippet against a stubbed permission API (write proceeds
  and pins head_oid; read skips with a publishable reason), and the pin
  step against a real git repo with a real merge commit (matching head
  proceeds, moved head exits non-zero)
- add a stepIn(job, step) test helper. Several step names exist in both
  the tmux and verify jobs, and the unscoped step() returns the first
  match, so a verify-lane assertion silently tests the tmux copy — that
  has now bitten this suite three times, including in this commit.

* docs(triage): teach verify-pr test-only PRs, differential oracles, gate liveness

Fold techniques from the round-2 verification on QwenLM#7620 (an ANSI parser
PR) that the skill had no equivalent for:

- test-only PRs get their own method: a mutation A/B across TEST FILES
  (same mutants of the unmodified production file, only the test file
  swapped), reporting killed/total on both sides, requiring that no
  mutant regressed from killed to survived, checking that the killing
  assertion is the one the commit claims to have strengthened, and
  adjudicating every survivor as coverage gap or defect with independent
  evidence rather than by inspection
- when the code emulates a known implementation, that implementation is
  the oracle: feed identical input to both and report disagreement
  counts per side, lift reference tables verbatim out of the shipped
  dependency, and build the corpus from bytes captured off a real
  producer alongside synthesized sweeps
- prove a gate is live before citing it: plant a violation the linter
  must catch, confirm it is reported, remove it — a linter that matched
  no files exits 0 exactly like one that passed
- attribute pre-existing failures by byte-identical failing file AND
  test names on both sides, with deltas, not just totals
- when the base is far behind, verify the merge: trial-merge into
  current main, confirm it is conflict-free, and re-run the affected
  suite on the merged tree
- round continuity gains its one legitimate shortcut: a production file
  proven byte-identical (sha256 quoted at both heads) carries prior
  evidence forward by construction

* style(triage): reflow verify-pr skill to prettier's markdown wrapping

The previous commit's added paragraphs were hand-wrapped and prettier
--check flagged the file; the repo runs prettier over all of it.

* test(triage): cover the disabled-runner-pool notice

The kill-switch path had no test: a refactor could drop the notice and
leave a /verify request acknowledged with 👀 but permanently unanswered,
since the verify job refuses to start and publish-verify skips with it.

Fold the step into the existing PR-guard loop (now scoped through
stepIn, so it cannot match a same-named step in another job) and assert
the parts that make the answer useful — the kill-switch and permission
conditions, both languages, the alternative it points at, and the verify
job's own exclusion of the disabled pool. All three mutations turn it
red: removing the step, dropping its PR guard, or letting the verify job
queue against the disabled pool.

* fix(triage): repair a step-killing PIPESTATUS read and six forgeable controls

Sixth review round, 12 findings. Several are regressions from my own two
previous rounds; the first would have broken every single run.

- `AGENT_STATUS=${PIPESTATUS[0]}` is itself a command and resets
  PIPESTATUS, so the next line's ${PIPESTATUS[1]} was unset and `set -u`
  aborted the step immediately after the agent finished — before artifact
  collection, the verdict, or anything else. Verified by replaying the
  exact structure: 'PIPESTATUS[1]: unbound variable'. Both elements are
  now snapshotted in one command
- concurrency predicates were broader than the job conditions they guard,
  and GitHub evaluates concurrency BEFORE the job `if`: a /verify comment
  entered the triage job's shared per-PR group (where it could displace a
  pending /triage and then skip), and a /verify queued while the runner
  kill switch was on did the same to a real verification. Both predicates
  now match their job's runnable set exactly
- an outward-resolving .git/hooks entry was only warned about and left in
  place, so the next root-owned git command would run it. It is now
  unlinked without traversing its target, a root-owned hooks directory is
  restored, and core.hooksPath is unset
- the second .qwen pin re-derived HEAD^1 from git metadata after the
  workspace, including .git, had been handed to the build user. The base
  OID is now recorded while .git is still root-owned and the re-pin
  archives that content-addressed OID
- classify_failure took both of its inputs from PR-controlled sources: a
  lifecycle script can exit with a signal status and can print any line
  the log patterns matched, turning its own deterministic breakage into
  'infrastructure, please re-run' — which hid the failure and preserved a
  stale report. No infra verdict is derivable there, so the prepare step
  reports `fail` and lets the embedded log speak for itself
- cleanups descended through PR-writable parents: `.qwen` itself can be a
  symlink, and the worktree sweep trusted git metadata with only a lexical
  prefix check. Symlinks are unlinked without traversal and worktree paths
  must canonicalize inside the workspace. Replayed all three escapes
- skipped and docs-only outcomes upload no artifact, so the new
  download-failure branch pre-empted them and made their real reason
  unreachable; they are answered first now
- a run that crashed before writing report.md still claimed the
  substantive marker, letting a headline overwrite the previous round's
  evidence. The marker now requires a report

Skill: the byte-identical shortcut needs the whole input closure, not one
file hash; the credential-free local path cannot call `gh` at all (fetch
the metadata outside and mount it read-only); and the A/B base is
`baseRefOid` in local mode, not `HEAD^1`.

Tests: 7 new guards plus 4 updated to the new shapes, all
mutation-verified (50/50).

* fix(triage): answer dropped /verify requests and prove the proxy rejects

Maintainer review (yiliang114), 7 items:

- a third /verify while two runs are in flight is dropped by the
  concurrency group with no job and therefore no comment. The hosted
  authorize job now counts this workflow's other in-flight runs and says
  so; an API hiccup leaves the request alone rather than denying it
- the proxy's bearer check had no executable test. It now starts the real
  proxy against a real upstream and issues real requests: no header and
  a wrong token are 401, this run's token is 200, and a route other than
  /chat/completions is 403 — with the health endpoint echoing the nonce
- the 502 path forwarded the raw upstream error, which can name resolved
  hosts and TLS detail to PR code. It logs server-side and returns a
  generic failure
- publish-verify inherited the 360-minute default; it downloads one
  artifact and posts one comment, so it is bounded at 10
- removing the log classifier last round left the comment block it
  replaced, which still said failures are classified from the exit status
  and the log. Deleted
- that removal also left every install failure reported as the PR's
  fault, including a registry outage. There is exactly one signal here PR
  code cannot write — asking the registry ourselves, as root, with the
  container's resolver — so an install failure is downgraded to
  infra-error only when that probe fails. It proves reachability now
  rather than at failure time, so it can only ever downgrade, never
  confirm; a build failure has no equivalent and stays the tree's problem
- the skill's local-invocation warning ran into the preceding sentence,
  which GFM renders as one paragraph

Tests: 5 new guards, all mutation-verified (55/55).

* fix(triage): resolve hooks hermetically and mirror symlink guards at job end

Maintainer review round (doudouOUC), 6 findings. Two were Critical and
both reproduced:

- the hooks sweep resolved its path with the ambient git config in play.
  With a global core.hooksPath set — which the reviewer has and I do not,
  which is why my earlier replay showed a false pass — `git rev-parse
  --git-path hooks` returns that global path, the in-git-dir guard reads
  'outside', and a planted `.git/hooks` symlink survives untouched. A/B:
  old code leaves the symlink under a global hooksPath, new code removes
  it in both environments and never touches the link target. Resolution
  now runs with GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM pointed at
  /dev/null
- the END-of-job cleanup still used the bare `rm -rf .qwen/tmp` that the
  start-of-job cleaner was hardened against two rounds ago. The agent
  executes PR code between the two, so the end is no safer than the
  start: it now unlinks symlinks without descending and canonicalizes
  worktree paths inside the workspace before deleting

Plus four suggestions, all valid:

- the saturation notice counted this workflow's in-flight runs across
  every PR while the concurrency group is per-PR, so a run on another PR
  would trigger a warning about a queue that does not exist. It now
  matches on the PR title (the only per-PR handle an issue_comment run
  record carries) and stays silent when that cannot be resolved
- the skill recommended `require.resolve` for the workspace-realpath
  check; these packages are ESM-only with import-only exports, so it
  throws ERR_PACKAGE_PATH_NOT_EXPORTED and reads like a missing module.
  Verified, and replaced with `readlink -f node_modules/@qwen-code/...`
- the symlink-escape test inherited the developer's git config, which is
  what hid the first finding. It now runs with global/system config
  neutralized AND repeats the case with a global core.hooksPath planted
- the publisher's build-phase arm was never rendered by any test (every
  case used 'install'), so a typo in that command name would have
  shipped. Now covered, along with an unrecognized phase

Mutation-verified 4/4. The hooks guard needed a discriminating assertion:
git's own `*.sample` files must survive the sweep, because the
outward-path fallback removes the whole directory and would otherwise
satisfy a bare 'planted hook is gone' check.

* fix(triage): count only /verify runs for saturation, and test the PATCH arm

Bot review round, 2 suggestions, both valid:

- the saturation notice matched runs by PR title, which narrowed to this
  PR but not to /verify. /triage and /tmux live in their own concurrency
  groups, so two of those in flight would warn about a verify queue that
  is actually empty. It now also requires the run to have a job named
  'verify' — the run record carries no command, but its job list does.
  Replayed: two non-verify runs stay silent, two verify runs warn
- every publish fixture returned an empty comments listing, so the PATCH
  arm was never executed: a broken PATCH would have stranded the running
  status comment and posted a duplicate below it, with the suite green.
  The publisher now runs against a stubbed listing and the test asserts
  which verb went to which comment id — bot-owned live status is PATCHed
  in place, an absent comment posts fresh, and a marker comment owned by
  someone else is left alone and posted around

Mutation-verified 3/3: counting every command, never PATCHing, and
accepting foreign-owned markers each turn one test red.

Two stub bugs found while writing these, both mine and both silent:
${*#pattern} applies per positional parameter rather than to the joined
string (yielding a wrong run id), and the paginate fixture needs one
array per page, not an array of pages.

* fix(triage): fix the real silent drop and drop the step built on a wrong premise

Review round 4. The blocker was mine twice over: the saturation notice I
added last round had GitHub's concurrency semantics backwards, and the
silent drop it claimed to cover was somewhere else entirely.

- GitHub cancels the OLDER pending run in a group and admits the new one
  (confirmed against the workflow-syntax reference). My step told the
  person who had just typed /verify that their request might be dropped,
  when theirs is the one that runs — and said nothing to the person whose
  queued run actually died. This PR already had it right in
  publish-verify's own comment, so the file contradicted itself and the
  user-facing copy followed the wrong half. The step is removed rather
  than reworded: with the fix below there is nothing left for it to warn
  about, and it cost 2+N API calls on every /verify.
- the actual drop: a verify job cancelled while still PENDING never
  reaches a runner, so its outputs block — where the
  "|| github.event.issue.number" fallback lived — is never evaluated.
  publish-verify then read an empty PR_NUMBER, hit its own guard and
  exited 0, making the cancelled branch unreachable in exactly the
  scenario that produces cancellations. The fallback now lives where the
  value is read. Reproduced both arms by executing the real step: with a
  number the cancelled notice posts, with an empty one it only warns.
- same one-line class in publish-tmux, fixed alongside.

Two copy defects from the classifier removal, both mis-attribution
pointed the other way:

- the infra-error body still named a signal/OOM kill and a full disk,
  none of which the current prepare step can produce — infra-error now
  requires npm ci to fail AND the registry probe to fail. It names that
  condition only, and offers a re-run instead of asserting it is the fix.
- the code comment above it still described the deleted classifier.

Also fixes the indentation break an earlier scripted edit left in the
publish body builder, and replaces the saturation test with one that
executes the cancelled path. Mutation-verified 2/2; the copy needed its
own guard, since reverting the wording alone left every test green.

* docs(triage): teach verify-pr survivor accounting and observability regressions

Fold techniques from the re-verification on QwenLM#7709 that the skill had no
equivalent for:

- the mutation matrix must report the mutations that changed NOTHING, not
  only the ones that failed. Each survivor gets classified as an ordinary
  coverage gap or as dead code — a guard whose deletion leaves every test
  green is one of those two, and the difference is what the author needs.
  Survivors mirroring a pre-existing gap are labelled as such, and the set
  is framed as completeness reporting rather than merge conditions
- the sharper case that report demonstrates: a test that passes for the
  WRONG REASON. If deleting the new guard leaves its own new test green,
  that test is pinned by an earlier early-return, not by the change, and
  asserts nothing about it. Name what actually pins it
- and do not generalize from one dead guard to its siblings: the same
  report shows a clause that is unreachable on one path while being the
  only protection on another. Check each, report the contrast
- observability regressions: when a change suppresses output, follow the
  value before calling the suppression correct. A bare catch on the path
  plus a field with no readers anywhere in the repo means the cause is now
  unobservable even in devtools — a real loss that no behavioural
  assertion can see
- report structure gains a Corrections section: when an earlier round or
  bot comment described the code inaccurately, state the correct fact with
  evidence and label it as a correction to the description, not a request
  to change code. A wrong description left standing costs the next reader
  more than the original finding did

* fix(triage): carry the /verify lane's hardening across to /tmux

The /tmux job executes the same untrusted PR code, as the same user, on the
same persistent self-hosted pool as the /verify lane that QwenLM#7710 hardened.
Five of those controls had no equivalent here. Each was found on the verify
side by reproducing an attack or a failure, not by reading the code, so the
same evidence applies unchanged.

- the model proxy bound a FIXED port (8787). PR lifecycle scripts run before
  it, so a detached child can squat that port: the real proxy then dies with
  EADDRINUSE while the health probe succeeds against the squatter, and the
  agent takes its chat completions. Now an ephemeral port published through
  a root-owned file, a per-run nonce the health endpoint must echo, and a
  liveness check on the PID we started. Replayed with 8787 occupied: the
  proxy comes up on an OS-chosen port and answers with the nonce.
- nothing swept planted artifact directories. npm ci/build run the PR's
  lifecycle scripts, which can create tmp/<name>-tmux-<ts>/ holding a
  report.md and a transcript; the collector globs *-tmux-* and the publisher
  takes the first match, so a planted directory could supply the comment's
  contents. Swept after the last PR-controlled process and before the agent.
- the global npm install ran with the workspace as cwd, where the PREVIOUS
  run's checked-out tree still sits. npm reads a cwd .npmrc, and a
  --registry flag does not override script-shell or hooks, so that config
  reached a root-privileged install. It now runs from RUNNER_TEMP.
- the end-of-job cleanup globbed below .qwen/tmp. PR code ran in this
  workspace, so either .qwen or .qwen/tmp can be a symlink out of the tree —
  verified on the verify lane, where the glob deleted the link target's
  contents as root. Symlinks are unlinked without descending.
- emit_block capped the raw log then escaped it. Escaping inflates every
  & < > by 4-5 bytes, so dense content can push the assembled body past
  GitHub's 65,536-character comment limit, 422 the post, and leave no
  comment at all. It now escapes first, caps the escaped bytes, and
  truncates on a character boundary via node — BSD iconv -c passes an
  incomplete trailing UTF-8 sequence through unchanged.

Tests: a tmux-lane-parity suite, all six mutations verified (restoring the
fixed port, dropping the sweep, moving the install back, dropping the
symlink guard, reverting to a raw-side cap, and dropping the
character-boundary truncation each turn one test red; a no-op control
correctly changes nothing). One pre-existing assertion updated: it pinned
emit_block's old inline-capture shape, and the guarantee it protects —
a render failure is caught — is asserted in the new form.

Also adds the regression guard for the publish-tmux PR_NUMBER fallback that
landed in QwenLM#7710 without one: a job cancelled while pending never evaluates
its outputs, so without the fallback the result comment silently does not
post.

* fix(triage): address review — symlink guard, artifact strip, bearer auth (QwenLM#7753)

* fix(triage): address R2 review — proxy parity, bearer wire tests, process kill (QwenLM#7753)

* fix(triage): address R3 review — publisher parity, dedup ownership, cap budget tests (QwenLM#7753)

* fix(triage): address R4 review — drop redundant tmux-lane .mjs guards (QwenLM#7753)

* fix(triage): address R5 review — tmp symlink sweep guard, proxy timer clear (QwenLM#7753)

* fix(triage): address R6 review — hoist proxy timer out of try, dead-upstream 502 tests (QwenLM#7753)

* fix(triage): address R7 review — make proxy watchdog idle, end stalled response (QwenLM#7753)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.1.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants