Skip to content

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

Open
doudouOUC wants to merge 5 commits into
QwenLM:mainfrom
doudouOUC:perf/lazy-first-use-dependencies
Open

perf(core): Lazy-load first-use dependencies#7686
doudouOUC wants to merge 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 above 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

Copy link
Copy Markdown
Collaborator

Thanks for the PR — this is a well-put-together change with a thorough design doc.

Template ✓ — all sections present, including the full Chinese translation.

Problem: real and measured. This isn't theoretical — it's tied to #7264, and the numbers are concrete: the ACP static closure drops from 13.4 MB to 12.3 MB with iconv-lite, @xterm/headless, and simple-git all at zero static bytes, and the 2-vCPU run shows cold process-to-first-session P50 improving ~144 ms (1877 → 1733 ms) with a small RSS win. I appreciate the honesty that the 30-pair benchmark applies to the prototype artifact and that the final rebased commit was rebuilt + guarded but not re-benchmarked.

Direction: aligned. Cold-start latency and RSS are core to the serve/ACP experience, and #7264 explicitly tracks these three packages as candidate 5. CHANGELOG has no direct reference, but the area is clearly relevant.

Size & scope — flagging for a maintainer. This is ~559 production-logic lines (≈489 in packages/core, ≈70 in packages/cli), plus ~169 test lines and a 160-line design doc; the largest single file is gitWorktreeService.ts (~169 lines). It's under the 1000-line advisory, but it's a substantial core change. More importantly, although titled perf, the diff is largely a structural refactor of how core services load their dependencies — three new loader modules, a sync-compat encoding module plus an esbuild tree-shake plugin, narrow CLI runtime entries, a sync→async split in the file service, and ~20 call-site conversions in GitWorktreeService. Per our core-module gate, a structural change of this size from a fork needs a maintainer's sign-off rather than an automatic approval, so I'm escalating it (this is a process flag, not a quality concern).

Approach: sound, and matches how I'd tackle it. Per-package single-flight loaders, keeping the public sync encoding API via a tree-shakeable compat module, deferring @xterm/headless until the PTY path is chosen (with an abort re-check), and keeping GitWorktreeService construction side-effect-free are all the right calls. Swapping the config sweep's getRepoTopLevel() probe for the lightweight findGitRoot() is a nice touch that drops simple-git from that path entirely. I don't see a materially simpler route — the iconv sync/async split is forced by the public sync API, and landing only two of the three packages would leave the largest (iconv-lite) on the eager path.

Moving on to code review. 🔍

中文说明

感谢贡献——这是一个组织良好、附带完整设计文档的 PR。

模板 ✓ —— 各节齐全,包含完整中文翻译。

问题:真实且有实测数据。 这不是理论性问题——它关联 #7264,且数据具体:ACP 静态闭包从 13.4 MB 降到 12.3 MB,iconv-lite@xterm/headlesssimple-git 三个包的静态归因字节均为 0;2-vCPU 测试显示冷启动 process-to-first-session P50 改善约 144 ms(1877 → 1733 ms),RSS 也有小幅下降。作者坦诚说明 30 组配对 benchmark 对应的是 prototype artifact,最终 rebase 后的提交仅重新构建并执行门禁、未重新跑 benchmark——这点很值得肯定。

方向:对齐。 冷启动延迟和 RSS 是 serve/ACP 体验的核心,#7264 也明确将这三个包列为 candidate 5。CHANGELOG 无直接引用,但该领域明显相关。

规模与范围——提请注意维护者。 约 559 行生产逻辑(packages/core 约 489 行,packages/cli 约 70 行),另有约 169 行测试和 160 行设计文档;单文件最大为 gitWorktreeService.ts(约 169 行)。未达 1000 行大 PR 建议线,但已是相当可观的核心改动。更重要的是:尽管标题为 perf,diff 实际上大部分是对核心服务依赖加载方式的结构性重构——三个新 loader 模块、一个同步兼容编码模块加 esbuild tree-shake 插件、窄化的 CLI runtime 入口、文件服务的 sync→async 拆分,以及 GitWorktreeService 中约 20 处调用点改造。按核心模块门禁,这种规模的结构性改动来自 fork 时需要维护者签字,而非自动批准,因此我将转交维护者(这是流程标记,并非质量顾虑)。

方案:合理,且与我的思路一致。 按包单飞(single-flight)loader、通过可 tree-shake 的兼容模块保留公共同步编码 API、将 @xterm/headless 延迟到选中 PTY 路径时再加载(并重新检查 abort)、保持 GitWorktreeService 构造无副作用——这些都是正确的选择。将 config 清理中的 getRepoTopLevel() 探测换成轻量级 findGitRoot() 是个亮点,彻底把 simple-git 移出该路径。我没有看到明显更简的路径——iconv 的 sync/async 拆分是被公共同步 API 倒逼的,而只落地三个包中的两个会把最大的 iconv-lite 留在启动路径上。

进入代码审查。🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

I read the diff against an independent proposal (per-package single-flight loaders + a sync compat module for the public encoding API + defer xterm/simple-git to first use + a metafile bundle guard). The PR matches that approach and I found no correctness blockers. Behavior preservation checks out:

  • Encoding split is faithful. prepareTextFileContent now takes an optional iconvLite and returns undefined when a non-UTF-8 encoding is requested without it; prepareTextFileContentAsync tries the sync path first, then loads the codec and retries. I traced the edge cases — UTF-8/ASCII/BOM/empty never load iconv, a detected non-UTF-8 read loads it once, and an unsupported non-UTF-8 encoding still falls through to UTF-8 exactly as before. The sync decodeBufferWithEncodingInfo / encodeTextFileContent moved to sync-file-encoding.ts are the same logic as the old fileUtils/fileSystemService versions.
  • PTY path is safe. Terminal is loaded right before executeWithPty, the abort signal is re-checked after the async import (returns the pre-spawn aborted handle), and a chunk-load failure stays inside the existing PTY try/catch so the child_process fallback is preserved.
  • Git stays side-effect-free until first use. GitWorktreeService swaps the eager this.git for a single-flight getGit() promise; construction no longer touches simple-git. Standalone simpleGit(path) call sites load through loadSimpleGit(). The removeWorktree path now surfaces a loader failure as a structured { success: false, error } instead of throwing — consistent with the other failure-returning methods.
  • Loaders correctly single-flight and normalize both named-export and default-only CJS chunk shapes; load-simple-git even rejects an unexpected module shape, and the tests cover all three cases.
  • Bundle guard is metafile-graph based (not bundle text), adds the three packages to FORBIDDEN_ACP_PACKAGES, and the new tests assert static paths are rejected while dynamic-only paths pass.

Non-blocking observations (not asking for changes):

  • prepareTextFileContentAsync's final if (!prepared) throw is effectively unreachable — the second call always passes a resolved iconvLite, so it can't return undefined. Harmless defensive code.
  • The decode logic now lives in two places (async in fileUtils.ts, sync in sync-file-encoding.ts). That's forced by the sync public API and the design doc calls it out, but the two copies need to stay in sync going forward — a cross-referencing comment would be belt-and-suspenders if a maintainer wants it.
  • getWorktreeDiff widens its try to also cover resolveBaseline/getCurrentBranch; those errors are now returned as "Error getting diff: …" strings instead of propagating. This makes the method consistent with its existing string-return contract, so I view it as a minor improvement rather than a regression.

Test evidence (PR's own CI, read via the API — I did not run any PR code)

At the reviewed commit ac96c57, CI is green with no failing checks:

  • Test (ubuntu-latest, Node 22.x)success (the author's earlier Ubuntu failure was fixed in this commit; the simple-git mock now exposes CheckRepoActions).
  • Serve A/B (ubuntu-latest, Node 22.x)success; the bot's A/B run reported no response changes against the PR base across 4 scenarios, which is good evidence the serve API surface is behaviorally unchanged.
  • Real daemon E2E / Java 11 and web-shell E2E Smoke (ubuntu-latest, Node 22.x)success.
  • Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x)skipped in this run; the author also notes Windows was not manually exercised, so a maintainer may want a Windows pass before merge.

Not verified: the latency/RSS numbers — these apply to the prototype artifact (SHA-256 f0ac7edc…), and the final rebased commit was not re-benchmarked (the author states this plainly). The bundle-closure zero-byte claim is enforced on the final commit by the production bundle guard.

Real-scenario (tmux) testing: N/A — no user-visible/TUI changes, and this is an unattended CI run.

中文说明

代码审查

我先独立给出方案(按包单飞 loader + 为公共编码 API 保留同步兼容模块 + 将 xterm/simple-git 延迟到首次使用 + 基于 metafile 的 bundle 门禁),再对照 diff。PR 与该方案一致,未发现正确性阻断项。行为保持已核对:

  • 编码拆分是忠实的。 prepareTextFileContent 现在接受可选的 iconvLite,在请求非 UTF-8 编码但未提供 iconvLite 时返回 undefinedprepareTextFileContentAsync 先走同步路径,再加载 codec 重试。我核对了边界情况——UTF-8/ASCII/BOM/空文件都不加载 iconv,检测到非 UTF-8 读取时加载一次,而不支持的非 UTF-8 编码仍像以前一样回退到 UTF-8。移到 sync-file-encoding.ts 的同步版 decodeBufferWithEncodingInfo / encodeTextFileContent 与原 fileUtils/fileSystemService 版本逻辑一致。
  • PTY 路径安全。 TerminalexecuteWithPty 之前加载,异步 import 后重新检查 abort 信号(返回 pre-spawn aborted handle),chunk 加载失败仍落在既有 PTY try/catch 内,从而保留 child_process 回退。
  • Git 在首次使用前保持无副作用。 GitWorktreeService 将急切的 this.git 换成单飞的 getGit() promise;构造不再触碰 simple-git。各处独立的 simpleGit(path) 调用通过 loadSimpleGit() 加载。removeWorktree 路径现在把 loader 失败以结构化 { success: false, error } 返回而非抛异常——与其他返回失败的方法一致。
  • Loader 正确单飞并归一化 named-export 与 default-only 两种 CJS chunk 形态;load-simple-git 甚至会拒绝非预期模块形态,测试覆盖了三种情况。
  • Bundle 门禁 基于 metafile 导入图(而非 bundle 文本),将三个包加入 FORBIDDEN_ACP_PACKAGES,新测试断言静态路径被拒绝、仅动态路径通过。

非阻断观察(不要求修改):

  • prepareTextFileContentAsync 末尾的 if (!prepared) throw 实际不可达——第二次调用总是传入已解析的 iconvLite,不会返回 undefined。无害的防御代码。
  • 解码逻辑现在有两处(fileUtils.ts 异步版、sync-file-encoding.ts 同步版)。这是被公共同步 API 倒逼的,设计文档也已说明,但两份副本今后需保持一致——若维护者想加保险,可加一条交叉引用注释。
  • getWorktreeDiff 扩大了 try 范围以覆盖 resolveBaseline/getCurrentBranch;这些错误现在以 "Error getting diff: …" 字符串返回而非向上抛出。这使该方法与其既有的字符串返回契约一致,因此我视为小幅改进而非回归。

测试证据(PR 自身的 CI,通过 API 读取——我未运行任何 PR 代码)

在受审提交 ac96c57,CI 为绿色,无失败检查

  • Test (ubuntu-latest, Node 22.x) —— success(作者先前的 Ubuntu 失败已在此提交修复;simple-git mock 现在暴露 CheckRepoActions)。
  • Serve A/B (ubuntu-latest, Node 22.x) —— success;机器人的 A/B 运行报告相对 PR base 在 4 个场景下无响应变化,这是 serve API 表面行为未变的有力证据。
  • Real daemon E2E / Java 11web-shell E2E Smoke (ubuntu-latest, Node 22.x) —— success
  • Test (macos-latest, Node 22.x)Test (windows-latest, Node 22.x) —— 本次运行中 skipped;作者也注明未手动验证 Windows,因此维护者可能希望合并前过一次 Windows。

未验证:延迟/RSS 数据——这些对应 prototype artifact(SHA-256 f0ac7edc…),最终 rebase 后的提交未重新跑 benchmark(作者已明确说明)。bundle 闭包零字节声明确实由生产 bundle 门禁在最终提交上强制执行。

真实场景(tmux)测试:N/A —— 无用户可见/TUI 变化,且本次为无人值守 CI 运行。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review and a genuinely high-quality PR, but it's a large structural refactor of core dependency-loading from a fork (titled perf), so our core-module gate needs a maintainer's sign-off rather than an auto-approval.

Stepping back: this is the right fix for a real, measured problem. My independent proposal landed on the same shape — per-package single-flight loaders, a tree-shakeable sync compat module for the public encoding API, xterm/simple-git deferred to first use, and a metafile bundle guard to stop regressions — and the PR executes it cleanly. The encoding edge cases are preserved, the PTY abort/fallback contract holds, Git construction is side-effect-free, and CI is green with a no-change Serve A/B. The design doc is unusually good, and the author is candid that the latency numbers belong to the prototype artifact rather than the final rebased commit.

Why I'm not approving: the diff is ~559 production lines that restructure how core services obtain their dependencies — three new loader modules, a sync/async split in the file service, an esbuild tree-shake plugin, narrow CLI runtime entries, and ~20 call-site conversions in GitWorktreeService. Even though the goal is performance and the title says perf, the mechanism is a broad structural change to core, and it comes from a fork. Our gate asks for a human maintainer's eye on exactly this profile, so I'm escalating rather than approving. If I had to maintain this in six months I'd thank the author — the loaders are small, the guard prevents backsliding, and the compat surface is explicit — but that's a maintainer's call to make.

Two things I'd want before merge that the static review can't settle: a Windows CI pass (skipped here and not manually exercised), and a maintainer's judgment on whether the final rebased commit warrants a fresh 30-pair benchmark or whether the enforced zero-byte bundle guard plus the prototype numbers are sufficient.

中文说明

置信度:3/5 —— 审查干净、PR 质量确实高,但这是一次来自 fork、标题为 perf 的大规模核心依赖加载结构性重构,因此核心模块门禁需要维护者签字,而非自动批准。

退一步看:这是对一个真实且有实测数据的问题的正确修复。我独立得出的方案与其形态一致——按包单飞 loader、为公共编码 API 保留可 tree-shake 的同步兼容模块、将 xterm/simple-git 延迟到首次使用、以及基于 metafile 的 bundle 门禁防止回归——而 PR 干净地实现了它。编码边界情况得以保留,PTY 的 abort/回退契约成立,Git 构造无副作用,CI 为绿色且 Serve A/B 无变化。设计文档异常出色,作者也坦诚延迟数据属于 prototype artifact 而非最终 rebase 后的提交。

我不批准的原因:diff 约 559 行生产代码,重构了核心服务获取依赖的方式——三个新 loader 模块、文件服务的 sync/async 拆分、一个 esbuild tree-shake 插件、窄化的 CLI runtime 入口,以及 GitWorktreeService 中约 20 处调用点改造。尽管目标是性能、标题写着 perf,但机制是对核心的大范围结构性改动,且来自 fork。我们的门禁恰恰要求对这种画像由人类维护者把关,因此我转交而非批准。如果半年后由我维护,我会感谢作者——loader 很小、门禁防止倒退、兼容面明确——但这应由维护者来决定。

合并前我希望补上、而静态审查无法定论的两点:过一次 Windows CI(本次被跳过且未手动验证),以及由维护者判断最终 rebase 后的提交是否需要重新跑一次 30 组配对 benchmark,还是强制执行的零字节 bundle 门禁加 prototype 数据就已足够。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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

The review is clean (see above) and the change is high quality with a real, measured cold-start benefit, but it's a ~559-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).

🔄 转交维护者 —— 审查干净(见上),改动质量高且有实测的冷启动收益,但这是来自 fork、约 559 行生产代码的核心依赖加载结构性重构。核心模块门禁将此类改动转交人类维护者而非自动批准。需要人类判断:(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.

@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/services/gitWorktreeService.ts
Comment thread packages/core/src/utils/sync-file-encoding.ts
@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.

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.

3 participants