Skip to content

perf(startup): lazy-load Google GenAI SDK on first use - #7512

Merged
doudouOUC merged 3 commits into
QwenLM:mainfrom
doudouOUC:perf/lazy-google-genai
Jul 23, 2026
Merged

perf(startup): lazy-load Google GenAI SDK on first use#7512
doudouOUC merged 3 commits into
QwenLM:mainfrom
doudouOUC:perf/lazy-google-genai

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR removes @google/genai from the ACP bootstrap static closure. Core orchestration now uses a package-local, SDK-parity implementation for the small synchronous surface it needs while retaining the official SDK types and provider response classes.

Provider construction and the logging decorator are loaded through a single-flight lazy content generator on the first asynchronous model operation. Configuration validation, runtime fetch preparation, and Qwen OAuth credential acquisition keep their existing eager timing. MCP tool adaptation loads the official SDK on discovery, and the bundle boundary guard now rejects any future static SDK path from the ACP entry.

Why it's needed

Candidate 3 in #7264 identified the Google GenAI SDK as a remaining ACP cold-start cost after the telemetry and undici lazy-loading work had landed. A dynamic import alone was insufficient because ACP session creation eagerly constructed the selected provider, so the SDK would merely have moved from initialize into POST /session.

The bundled ACP static closure decreased from 14,279,497 bytes to 13,280,177 bytes, a 999,320-byte reduction. The 755,788 bytes attributed directly to @google/genai dropped to zero in the static closure while remaining available in dynamic provider and MCP chunks.

On the 2-vCPU reference host, 30 alternating paired cold starts with telemetry enabled to an outfile improved channel.initialize P50/P95 from 984.9/1010.6 ms to 954.8/972.5 ms, process-to-first-session from 1924.6/1951.1 ms to 1858.7/1901.0 ms, and peak RSS from 414.6/427.1 MiB to 406.5/420.5 MiB. A separate 30-pair telemetry-off run with an immediate real prompt improved process-to-first-token P50 from 2900.7 ms to 2843.1 ms; first-token P95 was dominated by unrelated multi-second model-network outliers, so this PR does not claim a first-token tail improvement.

Reviewer Test Plan

How to verify

  • Build the release bundle and run the serve fast-path boundary check. The ACP static closure should contain no @google/genai input, while dynamic provider and MCP chunks should still contain the SDK.
  • Start the bundled daemon without MCP servers and create an ACP session. Initialization and session creation should succeed without evaluating the SDK; the first content-generator operation should load the selected provider once, including when two first operations race.
  • With Qwen OAuth selected, valid cached credentials should still be acquired before session registration, and missing or expired cached credentials should reject initial authentication with the existing /auth guidance rather than starting device flow after the session has been accepted.
  • Exercise OpenAI-compatible, Qwen OAuth, Anthropic, Gemini, and Vertex configurations. Provider behavior, summarized-thinking selection, auth refresh, per-model routing, subagent overrides, and logging should remain unchanged after first use.
  • Configure an MCP server and confirm discovery, schemas, annotations, duplicate-name handling, pagination, and direct tool invocation still use the SDK adapter successfully.

Evidence (Before & After)

N/A (no user-visible or TUI change). The cold-start and memory measurements are reported above and in the committed design document.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux

Environment (optional)

macOS 26.4.1 with Node.js 22.22.3: build, full workspace typecheck, affected-file ESLint, 805 targeted core tests, 299 ACP integration tests, 27 bundle-guard tests, release bundle, and startup boundary check. Alibaba Cloud Linux with 2 vCPUs, approximately 3.5 GiB RAM, no swap, and Node.js 22.23.1: 30-pair cold and preheated benchmarks, concurrent-session/telemetry-disabled/legacy functional checks, and a 30-pair immediate live OpenAI-compatible prompt run.

Risk & Scope

  • Main risk or tradeoff: Provider module or constructor failures now surface on the first asynchronous model operation rather than during content-generator creation. Configuration errors and initial Qwen OAuth credential failures remain eager. An MCP configuration may load the SDK during background discovery before the first model prompt.
  • Not validated / out of scope: Live Gemini, Vertex, and Anthropic calls were not available on the benchmark host; their construction and compatibility paths are covered by unit tests. First-token P95 is not claimed as an improvement because live model-network variance dominated the tail. Other lazy-loading candidates in Cold-start follow-ups: remaining lazy-loading candidates from the ACP eager-closure audit #7264 remain out of scope.
  • Breaking changes / migration notes: None. The public content-generator interface and documented provider configuration remain unchanged.

Linked Issues

Part of #7264. Implements candidate 3 without closing the tracking issue.

中文说明

本 PR 做了什么

本 PR 将 @google/genai 从 ACP 启动静态闭包中移除。Core 编排层现在通过包内、与 SDK 行为一致的实现提供所需的少量同步能力,同时继续使用官方 SDK 类型和 provider 响应类。

Provider 构造和 logging 装饰器通过单飞懒加载 content generator,在首次异步模型操作时加载。配置校验、运行时 fetch 准备和 Qwen OAuth 凭证获取仍保持原有的急切执行时机。MCP 工具适配在 discovery 时加载官方 SDK,bundle 边界守卫也会阻止未来从 ACP 入口重新静态引入 SDK。

为什么需要

#7264 的 candidate 3 指出,在 telemetry 和 undici 懒加载工作合入后,Google GenAI SDK 仍是 ACP 冷启动成本之一。仅改成动态 import 并不足够,因为 ACP session 创建仍会急切构造选定 provider,SDK 只会从 initialize 阶段移动到 POST /session 阶段。

打包后的 ACP 静态闭包从 14,279,497 bytes 降至 13,280,177 bytes,减少 999,320 bytes。静态闭包中直接归属于 @google/genai 的 755,788 bytes 降为零,同时 SDK 仍保留在动态 provider 和 MCP chunks 中。

在 2 vCPU 参考机器上,开启 telemetry outfile 的 30 对交替冷启动中,channel.initialize P50/P95 从 984.9/1010.6 ms 改善至 954.8/972.5 ms,进程到首 session 从 1924.6/1951.1 ms 改善至 1858.7/1901.0 ms,峰值 RSS 从 414.6/427.1 MiB 降至 406.5/420.5 MiB。另一次关闭 telemetry、启动后立即发送真实 prompt 的 30 对测试中,进程到首 token P50 从 2900.7 ms 改善至 2843.1 ms;首 token P95 受到互不相关的多秒模型网络离群点主导,因此本 PR 不宣称首 token 长尾改善。

Reviewer 测试计划

如何验证

  • 构建 release bundle 并运行 serve fast-path 边界检查。ACP 静态闭包不应包含任何 @google/genai input,而动态 provider 和 MCP chunks 仍应包含 SDK。
  • 在不配置 MCP server 的情况下启动打包 daemon 并创建 ACP session。初始化和 session 创建应在未执行 SDK 的情况下成功;首次 content-generator 操作应只加载一次选定 provider,包括两个首次操作并发竞争时。
  • 选择 Qwen OAuth 时,有效缓存凭证仍应在 session 注册前获取;缺失或过期的缓存凭证应通过现有 /auth 提示拒绝初始认证,而不是在 session 已接受后才启动 device flow。
  • 验证 OpenAI-compatible、Qwen OAuth、Anthropic、Gemini 和 Vertex 配置。首次使用后的 provider 行为、summarized-thinking 选择、auth refresh、按模型路由、subagent override 和 logging 应保持不变。
  • 配置 MCP server,确认 discovery、schema、annotation、重名处理、分页和直接工具调用仍能正确使用 SDK adapter。

证据(修改前后)

N/A(无用户可见或 TUI 变更)。冷启动与内存测量已在上文及提交的设计文档中报告。

测试平台

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux

环境(可选)

macOS 26.4.1、Node.js 22.22.3:build、完整 workspace typecheck、受影响文件 ESLint、805 项目标 core 测试、299 项 ACP 集成测试、27 项 bundle guard 测试、release bundle 和启动边界检查。Alibaba Cloud Linux、2 vCPU、约 3.5 GiB RAM、无 swap、Node.js 22.23.1:30 对冷启动与预热基准、并发 session/telemetry-disabled/legacy 功能检查,以及 30 对启动后立即发送真实 OpenAI-compatible prompt 的测试。

风险与范围

  • 主要风险或权衡:Provider 模块或构造器错误现在会在首次异步模型操作时暴露,而不是在 content-generator 创建时暴露。配置错误和初始 Qwen OAuth 凭证失败仍保持急切执行。配置 MCP 时,SDK 可能在首次模型 prompt 前由后台 discovery 加载。
  • 未验证 / 超出范围:Benchmark 主机上没有可用的 Gemini、Vertex 和 Anthropic 真实调用凭证;其构造和兼容路径由单元测试覆盖。由于真实模型网络波动主导长尾,本 PR 不宣称首 token P95 改善。Cold-start follow-ups: remaining lazy-loading candidates from the ACP eager-closure audit #7264 中其他懒加载候选不在本 PR 范围内。
  • 破坏性变更 / 迁移说明:无。公共 content-generator 接口和文档化 provider 配置保持不变。

关联 Issue

属于 #7264 的一部分。实现 candidate 3,但不关闭该跟踪 issue。

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: clearly observed and well-measured. The ACP static closure carries ~756 KB of @google/genai that is parsed on every cold start despite not being needed until the first model operation. The 30-pair benchmark data (P50 -30ms on channel.initialize, -66ms process-to-first-session, -8 MiB RSS) demonstrates a real, reproducible cold-start cost. This is candidate 3 from the #7264 tracking issue, so the problem is already triaged and prioritised.

Direction: aligned. Startup latency is a core product concern, and the #7264 tracking issue explicitly scopes this work. CHANGELOG has no direct reference to lazy-loading the GenAI SDK, but startup performance is a recurring theme in the ecosystem.

Size: 264 production logic lines (additions + deletions), 313 test lines, 96 docs lines. Core paths are touched (packages/core/src/**), but production lines are well under the 500-line advisory threshold.

Approach: the scope feels right. The design doc considers and rejects five alternatives, and the implementation follows through on the chosen path cleanly — a small genai-compat.ts for the synchronous surface, a single-flight LazyContentGenerator for deferred provider construction, eager Qwen OAuth credential checks preserved, and a bundle guard to prevent regression. Every file in the diff serves the stated goal; no drive-by changes. The two follow-up commits address the earlier review feedback (bundle-guard wording narrowed to @google/genai, a real ERR_MODULE_NOT_FOUND test via a fresh module graph, and the Google/Apache-2.0 attribution added beside the adapted conversion helpers). Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测且有充分测量。ACP 静态闭包中携带约 756 KB 的 @google/genai,每次冷启动都会解析,但直到首次模型操作才需要。30 对基准数据(channel.initialize P50 -30ms、进程到首 session -66ms、RSS -8 MiB)证明了真实的冷启动成本。这是 #7264 跟踪 issue 的 candidate 3,问题已经过分类和优先级排序。

方向:对齐。启动延迟是核心产品关注点,#7264 跟踪 issue 明确将此工作纳入范围。

规模:264 行生产逻辑(增+删),313 行测试,96 行文档。触及核心路径(packages/core/src/**),但生产行数远低于 500 行建议阈值。

方案:范围合理。设计文档考虑并否决了五种替代方案,实现干净地遵循了选定路径——小型 genai-compat.ts 提供同步表面、单飞 LazyContentGenerator 延迟 provider 构造、保持 Qwen OAuth 凭证急切检查、bundle 守卫防止回归。diff 中每个文件都服务于既定目标,无夹带改动。两个后续提交已处理早先的审查意见(bundle 守卫措辞收窄到 @google/genai、通过全新模块图实现真实的 ERR_MODULE_NOT_FOUND 测试、在改编的转换 helper 旁补充 Google/Apache-2.0 署名)。进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@wenshao

wenshao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Code Review — perf(startup): lazy-load Google GenAI SDK on first use

Reviewed the full diff (16 files, +546/-88) plus the referenced SDK source and every downstream consumer of the rerouted imports. This is a well-scoped, well-documented change and I verified the risky parts against @google/genai directly. No blocking issues found; notes below are mostly maintainability/edge-case.

What it does

Removes @google/genai from the ACP bootstrap static closure (≈999 KB / 755 KB attributable to the SDK). It does this three ways: (1) a package-local genai-compat.ts supplies the tiny synchronous surface core orchestration needs (FinishReason, FunctionCallingConfigMode, createUserContent, createModelContent); (2) createContentGenerator now returns a single-flight LazyContentGenerator that defers provider construction + LoggingContentGenerator wrapping to the first async op; (3) mcpToTool moves to a dynamic import in discoverTools. A bundle guard entry makes any future static re-import fail CI.

Correctness — verified ✅

  • genai-compat content conversion is a faithful copy. isPart uses the exact key set as the SDK's _isPart (fileData/text/functionCall/functionResponse/inlineData/videoMetadata/codeExecutionResult/executableCode); string→{text} matches createPartFromText; the three error messages and the branch ordering match _toParts. genai-compat.test.ts cross-checks output and validation against the real SDK, which is the right way to lock this.
  • The compat approach is sound because these are string enums. FinishReason.STOP === 'STOP', FunctionCallingConfigMode.ANY === 'ANY', etc. Provider converters still import the real SDK enums, and cross-boundary comparisons (e.g. baseLlmClient.ts:281 sets mode: FunctionCallingConfigMode.ANY, consumed downstream) stay equal by string value. Confirmed each rerouted file only uses members present in the compat (STOP/MAX_TOKENS/ANY).
  • useSummarizedThinking() hardcoding is faithful. The lazy value authType === USE_GEMINI || USE_VERTEX_AI matches every provider: Gemini true, Vertex→Gemini true, OpenAI/Qwen(extends OpenAI)/Anthropic false.
  • No caller unwraps the concrete generator. Repo-wide, getWrapped()/instanceof LoggingContentGenerator have no production callers, so returning LazyContentGenerator (which wraps LoggingContentGenerator internally, preserving logging/telemetry) is safe. The removed instanceof assertions in the test are appropriate.
  • Single-flight is correct. this.generatorPromise ??= this.loader() is atomic w.r.t. the event loop (no await between read and assign); the concurrent-calls test proves createCount === 1.
  • Eager Qwen credential check preserved. OAuth acquisition stays before session registration; the captured qwenClient is closed over and only the QwenContentGenerator construction defers. Tested for both success and cached-credential-failure-rejects paths.
  • ACP closure stays clean. acpAgent.ts, session/Session.ts, history-replayer.ts all import @google/genai type-only (erased); the new guard + boundary tests backstop regressions.

Notes / suggestions (non-blocking)

  1. Partial-enum compat is TS-guarded but under-commented (low). FinishReason/FunctionCallingConfigMode in genai-compat.ts intentionally expose only the members core code uses. Accessing an absent member (e.g. FinishReason.SAFETY) is a compile error thanks to as const, so this is safe — but a one-line comment ("only the subset used by non-provider core code; provider converters still use the SDK enum") would prevent a future contributor from assuming it's the full enum, and the genai-compat.test.ts parity list must be extended in lockstep whenever a member is added.

  2. Failed loader promise is sticky (low, partly documented). A first-use load failure caches the rejected promise, so every subsequent call on that generator fails identically until refreshAuth() rebuilds it. Correct for the ERR_MODULE_NOT_FOUND "needs restart" case; for other provider-construction errors it means a session that looked healthy at creation now fails on first prompt and stays failed. Provider constructors are effectively deterministic so this is acceptable, and it's covered by the Risk & Scope "failure surfaces on first async op" note — just calling it out explicitly.

  3. Config captured by reference (low, documented). The loader closes over generatorConfig/config, so a same-provider model change between createContentGenerator and first use is observed by the deferred constructor (vs. the old snapshot-at-construction). Documented in the design doc; edge case, fine.

  4. await import('@google/genai') runs per discoverTools() call. Module cache makes repeats ~free, so no real cost — noting only that MCP-configured setups still evaluate the full SDK during background discovery (as the PR states).

Test coverage

Strong: deferred construction, concurrent single-flight, Qwen eager-check + failure, module-not-found→restart wrapping, non-module re-throw, and SDK-parity for the compat helpers. Minor gaps worth a follow-up: no assertion that useSummarizedThinking() is true for USE_VERTEX_AI specifically, and no direct test for the sticky-rejection recovery or the generateContentStream/embedContent passthroughs.

Verdict

Correct, faithful to the SDK where it matters, and the perf/closure win is backed by a bundle guard so it won't silently regress. Approve with the minor notes above (a comment on the partial-enum module being the only one I'd actually action before merge).

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The lazy-loading design is solid — single-flight loader, eager Qwen OAuth credential check preserved, bundle guard prevents future regression. Design doc is thorough and the benchmark methodology is honest about what it does and doesn't claim.

One blocking item and a few non-blocking observations.

Blocking: copyright headers on new files

genai-compat.ts and genai-compat.test.ts both say Copyright 2025 Google LLC. These are new Qwen-authored files — the repo convention for those is Copyright 2026 Qwen Team (see session-start-profiler.ts, grepReadTracking.ts, notebook-edit.test.ts, etc.).

Non-blocking:

  1. Error message text in toParts doesn't exactly match the SDK. The compat module throws 'partOrString must be a Part object, string, or array' where the SDK throws 'partOrString must be a Part object'; same pattern for the array-element message (or string suffix). Tests pass because toThrow does substring matching, but the test name says "matches SDK validation" — the messages aren't identical. No behavioral impact.

  2. LazyContentGenerator caches a rejected loader promise permanently (this.generatorPromise ??= this.loader()). A transient dynamic-import failure sticks until refreshAuth() rebuilds the generator. The design doc documents this as the intended retry boundary — just noting that clearing the promise on rejection would allow next-call retry if that ever becomes desirable.

  3. isPart checks 8 properties matching the current SDK Part type. If the SDK adds a new part field later, this won't recognize it. Low risk since the parity test compares against the actual SDK at test time.

Comment thread packages/core/src/core/genai-compat.ts Outdated
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: given the goal "remove @google/genai from the ACP static closure", I would (1) convert value imports of the small synchronous surface (FinishReason, FunctionCallingConfigMode, createUserContent, createModelContent) to a local compat module, (2) convert remaining SDK imports to import type where only types are used, (3) wrap provider construction in a lazy single-flight loader so the SDK loads on first async operation, (4) keep Qwen OAuth credential acquisition eager, (5) make MCP's mcpToTool a dynamic import inside discoverTools(), and (6) add a bundle guard.

Comparison with the diff: the PR's approach matches this proposal almost exactly, and the implementation is clean:

  • genai-compat.ts is minimal — only the four synchronous values core orchestration needs, with type-only SDK imports. The parity test verifies values and validation messages match the real SDK. The follow-up commit added the Google/Apache-2.0 attribution beside the adapted _isPart/_toParts helpers, which was the right call.
  • LazyContentGenerator is a textbook single-flight wrapper. generatorPromise ??= this.loader() guarantees one load across concurrent first calls. I verified the hardcoded useSummarizedThinking() mapping against every provider: geminiContentGenerator returns true, openaiContentGenerator/anthropicContentGenerator return false, and QwenContentGenerator extends OpenAIContentGenerator (so false) — the authType-based boolean (true for Gemini/Vertex, false otherwise) matches all five providers exactly, so deferring it ahead of provider load is not a behavior change.
  • The LoggingContentGenerator import is parallelised with the provider load via Promise.all — avoids serialising two dynamic imports.
  • Qwen OAuth credential acquisition stays eager in createContentGenerator(); only the QwenContentGenerator construction is deferred, preserving the existing auth-failure UX (missing/expired cached creds still reject session creation).
  • The mcpToTool dynamic import inside discoverTools() is the right call — replacing it locally would duplicate experimental SDK behavior.
  • The sticky-rejection behavior of the cached loader promise is intentional and documented: a failed first load stays failed until refreshAuth() rebuilds the generator, which is the correct retry boundary for immutable chunk/config failures.
  • I confirmed the design doc's downstream-consumer audit: getWrapped() is defined but never called in production, and no code does instanceof LoggingContentGenerator, so nothing unwraps the lazy wrapper.

No critical blockers or AGENTS.md violations found.

sequenceDiagram
    participant P1 as createContentGenerator
    participant P2 as LazyContentGenerator
    participant P3 as Provider Loader
    participant P4 as LoggingContentGenerator
    P1->>P1: validate config, preload fetch, Qwen OAuth (eager)
    P1->>P2: return lazy wrapper
    Note over P2: first async call (generateContent, countTokens, etc.)
    P2->>P3: single-flight loader (once)
    P3->>P3: dynamic import provider module
    P3->>P4: wrap in LoggingContentGenerator
    P4-->>P2: resolved ContentGenerator
    P2-->>P2: delegate all subsequent calls
Loading
Files changed (16 total)
File What changed
docs/design/2026-07-22-lazy-google-genai-loading.md Design doc covering problem, design, alternatives, and benchmark results
packages/core/src/core/genai-compat.ts New module providing FinishReason, FunctionCallingConfigMode, createUserContent, createModelContent without SDK runtime
packages/core/src/core/genai-compat.test.ts Parity tests verifying compat values and validation match the real SDK
packages/core/src/core/contentGenerator.ts Core change: LazyContentGenerator class, deferred provider construction, parallel LoggingContentGenerator import
packages/core/src/core/contentGenerator.test.ts Updated tests for deferred construction, single-flight, Qwen credential timing, real ERR_MODULE_NOT_FOUND via fresh module graph
packages/core/src/core/turn.ts Import FinishReason from genai-compat instead of SDK
packages/core/src/core/client.ts Import createUserContent from genai-compat instead of SDK
packages/core/src/core/geminiChat.ts Import createUserContent and FinishReason from genai-compat
packages/core/src/core/baseLlmClient.ts Import FunctionCallingConfigMode from genai-compat
packages/core/src/agents/runtime/agent-core.ts Import FinishReason from genai-compat
packages/core/src/services/chatRecordingService.ts Import createModelContent and createUserContent from genai-compat
packages/core/src/confirmation-bus/types.ts Convert to import type for full erasure
packages/core/src/core/geminiRequest.ts Convert to import type for full erasure
packages/core/src/tools/mcp-client.ts Dynamic import of mcpToTool inside discoverTools
scripts/check-serve-fast-path-bundle.js Add google/genai to ACP forbidden packages
scripts/tests/serve-fast-path-bundle-check.test.js Tests for static detection and dynamic allowance

Real-Scenario Testing

No user-visible TUI change — verified build, bundle boundary, and a live headless prompt on the bundled PR build (current head b67c64770).

Unit tests (worktree, PR head):

 ✓ src/core/genai-compat.test.ts (7 tests) 4ms
 ✓ src/core/contentGenerator.test.ts (12 tests) 555ms
 Test Files  2 passed (2)
      Tests  19 passed (19)

 ✓ scripts/tests/serve-fast-path-bundle-check.test.js (27 tests) 95ms
 Test Files  1 passed (1)
      Tests  27 passed (27)

Build + bundle + boundary check + live prompt (tmux capture-pane):

$ npm run build && DEV=true npm run bundle
BUILD_BUNDLE_EXIT=0
✅ All bundle assets copied to dist/

$ node scripts/check-serve-fast-path-bundle.js
Startup bundle closure checks passed.
BOUNDARY_EXIT=0

$ node dist/cli.js --version
0.20.1
VERSION_EXIT=0

$ timeout 120 node dist/cli.js -p 'reply with the single word: hello' --output-format text
hello
PROMPT_EXIT=0

The boundary guard passing on the freshly built bundle confirms @google/genai is absent from the ACP static closure (and the 27 guard tests confirm it still detects a static re-import while allowing the SDK behind dynamic imports). The live headless prompt shows the lazy-loaded provider constructs correctly on first use and the model responds. TypeScript compilation passed as part of npm run build.

中文说明

代码审查

独立方案: 给定目标"从 ACP 静态闭包中移除 @google/genai",我会 (1) 将小型同步表面(FinishReasonFunctionCallingConfigModecreateUserContentcreateModelContent)的值导入转换为本地兼容模块,(2) 将仅使用类型的 SDK 导入转换为 import type,(3) 将 provider 构造包装在懒加载单飞加载器中,使 SDK 在首次异步操作时加载,(4) 保持 Qwen OAuth 凭证获取为急切执行,(5) 将 MCP 的 mcpToTool 改为 discoverTools() 内的动态导入,(6) 添加 bundle 守卫。

与 diff 的比较: PR 方案与此提案几乎完全一致,实现干净。已逐一核对 useSummarizedThinking() 的硬编码映射与全部五个 provider 一致(Gemini/Vertex 为 true,OpenAI/Anthropic/Qwen 为 false,其中 Qwen 继承自 OpenAI),因此提前返回该布尔值不构成行为变化。同时确认 getWrapped() 仅有定义、生产代码无任何调用,也没有 instanceof LoggingContentGenerator,因此没有消费者会解开懒包装。未发现关键阻塞问题或 AGENTS.md 违规。

实际场景测试

无用户可见 TUI 变更——在当前 head b67c64770 的打包 PR 构建上验证了构建、bundle 边界与真实无头 prompt。

  • 单元测试:46 项全部通过(genai-compat 7 项、contentGenerator 12 项、bundle guard 27 项)
  • 构建 + bundle + 边界检查(tmux capture-pane):BUILD_BUNDLE_EXIT=0Startup bundle closure checks passed.BOUNDARY_EXIT=0
  • 真实无头 prompt:helloPROMPT_EXIT=0——懒加载 provider 在首次使用时正确构造,模型正常响应
  • TypeScript 编译随 npm run build 通过

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage on re-review; the design is well-reasoned, the implementation is minimal, and the results are verified on a real build.

This is what a good performance PR looks like. The problem is real and measured (756 KB of SDK parsed on every cold start), the design doc considers five alternatives and explains why each was rejected, and the implementation follows through with the minimum code needed — an 83-line compat module, a single-flight lazy wrapper, and import rewiring. No scope creep, no drive-by refactors, no unnecessary abstractions.

On this re-run I re-verified against the current head (b67c64770), including the two follow-up commits that addressed the earlier review feedback (bundle-guard wording narrowed to @google/genai, a real ERR_MODULE_NOT_FOUND test via a fresh module graph, and the Google/Apache-2.0 attribution). The things I checked most carefully:

  • useSummarizedThinking mapping: now returned ahead of provider load, so I confirmed it against every provider implementation — Gemini/Vertex return true, OpenAI/Anthropic return false, and Qwen extends OpenAI (false). The authType-based boolean matches all five exactly; not a behavior change.
  • Qwen OAuth timing: credential acquisition stays eager, only provider construction is deferred. An expired credential still rejects session creation with the existing /auth guidance. Verified in the test suite.
  • Single-flight correctness: concurrent first calls share one loader promise; the test fires two countTokens in parallel and asserts the provider is constructed exactly once.
  • No unwrapping: getWrapped() is defined but never called in production and nothing does instanceof LoggingContentGenerator, so the lazy wrapper is opaque to all consumers.
  • Bundle guard + live build: 46 targeted tests pass, the freshly built bundle passes the closure check (@google/genai out of the ACP static closure, still shipped in dynamic chunks), and a live headless prompt responds — the lazy provider constructs on first use.

This also aligns with the maintainer's independent A/B verification on the same head, which reproduced the −999,320-byte closure reduction byte-for-byte and confirmed SDK parity (61/61 checks) and runtime deferral.

If I had to maintain this in six months, I'd thank the author — the bundle guard prevents regression, the design doc explains the why, and the code is straightforward.

中文说明

置信度:5/5 —— 复审后每个阶段都很干净;设计合理,实现最小化,结果已在真实构建上验证。

这是一个优秀的性能 PR 的典范。问题真实且有测量(每次冷启动解析 756 KB SDK),设计文档考虑了五种替代方案并解释了否决原因,实现以最少代码完成——83 行兼容模块、单飞懒加载包装器和导入重连。无范围蔓延、无顺手重构、无不必要的抽象。

本次复审针对当前 head(b67c64770),包含处理早先审查意见的两个后续提交。重点核对:useSummarizedThinking 映射已与全部五个 provider 逐一对照,完全一致,不构成行为变化;Qwen OAuth 凭证获取保持急切执行;并发首次调用共享一个 loader promise;getWrapped() 仅有定义、生产代码无调用,懒包装对所有消费者透明;46 项目标测试通过,新构建 bundle 通过闭包检查,真实无头 prompt 正常响应。

这也与维护者在同一 head 上的独立 A/B 验证一致——其逐字节复现了 −999,320 字节的闭包缩减,并确认 SDK 等价性(61/61)与运行时延迟加载。

Qwen Code · qwen3.8-max-preview

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

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

Copy link
Copy Markdown
Collaborator Author

Addressed the first review round in 8ca1571f63.

Feedback Decision Action
New-file copyright headers Fixed Updated both new files to Copyright 2026 Qwen Team.
Partial runtime enum surface is under-commented Fixed Added a concise comment stating that the runtime values are intentionally limited to the subset used outside provider adapters.
Compat validation messages differ from the SDK Not changed Verified against the locked @google/genai Node source: it emits the same full partOrString must be a Part object, string, or array and element in PartUnion must be a Part object or string messages.
Sticky rejected loader promise Not changed Intentional for deterministic provider construction/import failures; refreshAuth() remains the documented retry boundary.
Additional Vertex/passthrough/recovery tests Deferred Existing provider parity, single-flight, failure-timing, and broad downstream tests cover the changed behavior; the suggested additions are non-blocking follow-up coverage.

Verification on the pushed commit: genai-compat.test.ts 7/7, affected ESLint, Prettier, and a clean worktree.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Code Review — perf(startup): lazy-load Google GenAI SDK on first use

Reviewed the full diff against 2f725c863, cross-checked genai-compat.ts against the actual @google/genai@2.6.0 source pulled from npm, and traced every downstream consumer of the rerouted imports. Two thorough reviews are already on this PR, so the notes below are limited to what they did not cover, plus one correction.

Verdict: no blocking correctness defect. The content-conversion parity is exact, the single-flight ??= is atomic w.r.t. the event loop, the eager Qwen OAuth check is preserved, and the guard's inputMatchesPackage prefix match won't false-positive on google-auth-library. Only item 5 below is something I'd action before merge; item 1 is a two-line change I'd fold in while here.

1. BaseLlmClient's per-model fallback path is silently bypassed (Suggestion, worth fixing)

packages/core/src/core/baseLlmClient.ts:741-748 depends on createContentGenerator() throwing:

} catch (err: unknown) {
  this.perModelGeneratorCache.delete(cacheKey);   // evict on construction failure
  throw normalizeGeneratorError(err);
}
...
return generatorPromise.catch(fallbackAfterGeneratorError);  // non-failClosed → fall back to main generator

After this change it no longer throws for two error classes: dynamic-import failure (ERR_MODULE_NOT_FOUND, i.e. a background update swapped the chunks) and provider-constructor failure (e.g. GoogleGenAI throwing Project/location and API key are mutually exclusive when GOOGLE_CLOUD_PROJECT is set alongside a Vertex API key).

Two consequences: (a) fallbackAfterGeneratorError becomes dead code for those errors — a routed side request (summarize, memory, title generation) that used to degrade to the main generator with a warning now hard-fails; (b) the failed lazy generator stays in perModelGeneratorCache, because the eviction only wraps createContentGenerator, and LazyContentGenerator.generatorPromise ??= never clears on rejection — so every subsequent routed call for that model rethrows the same stale rejection until refreshAuth().

validateModelConfig still runs eagerly and catches the common "env var not set" case, so severity is moderate rather than high. But clearing generatorPromise on rejection inside getGenerator() is a two-line fix that removes both the sticky failure and the cache poisoning at once. Both prior reviews flagged the sticky rejection as optional; combined with perModelGeneratorCache I think it's worth doing now.

2. The bundle-guard comment overclaims — the Google auth graph is still in the static closure (Suggestion)

scripts/check-serve-fast-path-bundle.js:216-218 says "Keep its SDK and Google auth graph out of the ACP bootstrap closure". At this same commit:

packages/core/src/tools/mcp-client.ts:31  import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
packages/core/src/tools/mcp-client.ts:32  import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';

Both modules statically import google-auth-library (packages/core/src/mcp/google-auth-provider.ts:14, sa-impersonation-provider.ts:13), and mcp-client.ts is itself inside the ACP static closure — otherwise this PR wouldn't have needed to make mcpToTool dynamic. So google-auth-library and its dependency graph remain in the bootstrap closure, and the guard won't catch it since only @google/genai is listed.

Suggest narrowing the comment to what the guard actually enforces. Separately, this looks like a cheap follow-up: ServiceAccountImpersonationProvider and GoogleCredentialProvider are only instantiated inside function bodies (mcp-client.ts:1994 and :2023) — exactly the mcpToTool shape — so an await import() plus a new FORBIDDEN_ACP_PACKAGES entry would trim the closure further. Not for this PR, but worth recording on #7264.

3. The ERR_MODULE_NOT_FOUND path lost its real coverage (Suggestion)

The mock semantics changed in packages/core/src/core/contentGenerator.test.ts: the module factory used to throw (simulating a genuine dynamic-import failure), and now the throw happens inside createOpenAIContentGenerator, i.e. a constructor failure. The import itself now always succeeds, so should handle ERR_MODULE_NOT_FOUND exercises "a constructor threw an error carrying an ERR_MODULE_NOT_FOUND code" — something that doesn't occur in production. The assertions still pass only because wrapProviderLoadError treats both identically.

That matters here because the "background update removed the chunk" case is the sole reason the restart message exists, and it's precisely the path this PR moves from session creation to first prompt — so the most-changed behavior is now the least-covered one. A vi.doMock + vi.resetModules() case that makes the module factory itself throw would restore it (that describe block already calls vi.resetModules()).

4. Correction to an earlier note — please don't act on it

An earlier review reports that the compat error messages diverge from the SDK ("the SDK throws partOrString must be a Part object"). I checked the pinned @google/genai@2.6.0 (packages/core/package.json:40) source directly:

throw new Error('partOrString must be a Part object, string, or array');
throw new Error('element in PartUnion must be a Part object or string');

These are byte-identical to genai-compat.ts. Changing them would introduce the divergence the note is trying to prevent.

5. License header — attribution rather than a straight swap (the one item I'd action before merge)

The content-conversion logic in genai-compat.ts is adapted from the Apache-2.0 @google/genai, not original code, so replacing the header with Copyright 2026 Qwen Team isn't obviously more correct. Keeping the Google copyright and adding an upstream-attribution comment reads better and also answers the "partial enum is under-commented" note from the other review:

// Adapted from @google/genai 2.6.0 (`_isPart` / `_toParts`, Apache-2.0) so core
// orchestration can use these helpers without evaluating the SDK. `FinishReason` and
// `FunctionCallingConfigMode` expose only the members non-provider core code uses;
// provider converters still import the real SDK enums. Parity is locked by
// genai-compat.test.ts — re-check on every SDK upgrade.

6. Minor observations

  • FinishReason members lose their literal types — FinishReason.STOP is now typed as the whole enum rather than the STOP literal, so a switch (reason) { case FinishReason.STOP: } would no longer narrow. All rerouted sites do plain === comparisons (turn.ts:560, geminiChat.ts:2760/2823/3953, agent-core.ts:908), so there's no impact today.
  • refreshAuth() now reports success — and fires the auth_success notification hook (config.ts:3354-3368) — for configurations whose provider construction is guaranteed to fail (the Vertex project + API key conflict above). The Risk section covers deferred failures generically but not this effect on the auth-success signal.
  • The import { type X }import type { X } changes in confirmation-bus/types.ts and geminiRequest.ts are pure style: esbuild elides both forms in TS, so they don't contribute to the closure reduction. Zero risk, just not load-bearing.
  • docs/design/2026-07-22-lazy-google-genai-loading.md:104 cites absolute paths on the benchmark host (/root/qwen-7264-c3-20260722/results/...) that no reviewer can reach and that will rot. Either inline the summary numbers or drop the reference.

Also worth waiting on CI: Test (ubuntu-latest, Node 22.x) is still pending and the macOS/Windows jobs are showing as skipping.

中文说明

在提交 2f725c863 上完整审阅了 diff,并把 genai-compat.ts 与从 npm 下载的 @google/genai@2.6.0 实际源码逐条比对,同时追踪了所有改道导入的下游消费点。本 PR 已有两份详细 review,下面只写它们未覆盖的内容,外加一处更正。

结论:无阻塞性正确性缺陷。 内容转换与 SDK 完全等价,单飞 ??= 在事件循环内是原子的,Qwen OAuth 急切校验得以保留,guard 的 inputMatchesPackage 前缀匹配不会误伤 google-auth-library。下面只有第 5 条是我认为合并前需要处理的;第 1 条是两行改动,建议顺手做掉。

1. BaseLlmClient 的 per-model 降级路径被静默绕过(建议处理)baseLlmClient.ts:741-748 依赖 createContentGenerator() 抛错。改动后它对两类错误不再抛出——动态 import 失败(ERR_MODULE_NOT_FOUND,即后台升级替换了 chunk)和 provider 构造器抛错(例如同时设置 GOOGLE_CLOUD_PROJECT 与 Vertex apiKey 时 GoogleGenAI 抛 mutually exclusive)。后果有两层:fallbackAfterGeneratorError 对这两类错误成为死代码,原本会带 warning 回落主 generator 的 side request 现在直接硬失败;失败的 lazy generator 还会留在 perModelGeneratorCache 里(驱逐逻辑只包住 createContentGenerator),叠加 generatorPromise ??= 从不在 rejection 后复位,该 model 的每次后续路由请求都会重抛同一个陈旧 rejection,直到 refreshAuth()validateModelConfig 仍急切校验,最常见的"环境变量没配"仍被拦住,所以严重度中等。但在 getGenerator() 中 rejection 时清掉 generatorPromise 是两行修复,能同时消除 sticky 失败和缓存污染。

2. bundle guard 注释过度宣称,Google auth graph 仍在静态闭包内(建议)check-serve-fast-path-bundle.js:216-218 的注释写着要把 SDK "and Google auth graph" 挡在 ACP bootstrap 闭包外,但同一提交上 mcp-client.ts:31-32 静态导入了 GoogleCredentialProvider / ServiceAccountImpersonationProvider,二者又静态导入 google-auth-library,而 mcp-client.ts 本身就在 ACP 静态闭包内(否则本 PR 无需把 mcpToTool 改成动态)。建议把注释收敛到 guard 实际保证的范围。另外这是个顺手的后续优化:那两个 provider 只在 mcp-client.ts:1994:2023 的函数体内实例化,与 mcpToTool 形态完全一致,改成 await import() 后把 google-auth-library 加进 FORBIDDEN_ACP_PACKAGES 即可继续瘦身。不必在本 PR 做,建议记进 #7264

3. ERR_MODULE_NOT_FOUND 的真实路径失去覆盖(建议)contentGenerator.test.ts 的 mock 语义变了——原先是 module factory 抛错(模拟真正的动态 import 失败),现在改成在 createOpenAIContentGenerator 内抛错,即构造器失败。import 本身永远成功,因此该用例实际测的是"构造器抛出一个带 ERR_MODULE_NOT_FOUND code 的错误",现实中不会发生;断言仍通过只是因为 wrapProviderLoadError 对两条路径处理相同。而"后台升级导致 chunk 消失"正是这个 restart 提示存在的唯一理由,也恰恰是本 PR 从 session 创建推迟到首次 prompt 的那条路径——改动最大的路径反而覆盖最弱。用 vi.doMock + vi.resetModules() 让模块工厂真正抛错即可恢复(该 describe 已有 vi.resetModules())。

4. 对既有 review 的一处更正——请勿据此修改:有 review 指出 compat 的错误消息与 SDK 不一致。核对仓库锁定的 @google/genai@2.6.0packages/core/package.json:40)实际源码,其抛出的正是 partOrString must be a Part object, string, or arrayelement in PartUnion must be a Part object or string,与 genai-compat.ts 逐字相同。按该意见修改反而会引入它想避免的不一致。

5. License header——补归属而非直接替换(合并前唯一需处理项)genai-compat.ts 的内容转换逻辑改写自 Apache-2.0 的 @google/genai,并非原创,因此直接换成 Copyright 2026 Qwen Team 未必更正确。保留 Google 版权声明并加一段上游归属注释更合适,同时也回应了另一份 review 提到的"partial enum 缺注释"。

6. 其他小点FinishReason 成员丢失了字面量类型(switch 不再收窄,当前所有改道点都是 === 比较,无影响);refreshAuth() 现在对必然构造失败的配置也报告成功并触发 auth_success hook(config.ts:3354-3368);confirmation-bus/types.tsgeminiRequest.tsimport { type X }import type { X } 是纯风格改动,esbuild 对两种写法都会消除,不构成闭包缩减的一部分;设计文档 :104 引用的基准机绝对路径(/root/qwen-7264-c3-20260722/...)reviewer 无法访问且会失效,建议内联数据或删除。

另外 Test (ubuntu-latest, Node 22.x) 仍在 pending,macOS/Windows 显示为 skipping,建议等 CI 出结果。

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/core/contentGenerator.ts
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Review follow-up for b67c647705:

Review item Decision Action
Lazy loader rejection caching / per-model fallback Not taking The sticky rejection is the intentional retry boundary for immutable chunk/config failures. Clearing only the lazy promise would not restore BaseLlmClient fallback semantics and would repeatedly rerun non-recoverable construction; auth refresh or restart rebuilds the generator.
Bundle guard wording Fixed Narrowed the comment to the boundary the guard actually enforces: @google/genai, not the separate Google auth graph.
Real ERR_MODULE_NOT_FOUND coverage Fixed The test now uses a fresh module graph plus vi.doMock() whose module factory throws, so it exercises an actual dynamic-import failure.
Upstream attribution Fixed Kept the maintainer-requested Qwen file header and added the Google/Apache-2.0 attribution beside the adapted conversion helpers.
Google auth lazy loading Deferred Useful follow-up for #7264, but separate from candidate 3 and not needed for this PR boundary.
Inaccessible benchmark-host paths Fixed Removed the private absolute paths; the full benchmark results remain inline in the design document.
Enum literal typing and auth-success timing observations Deferred No current downstream narrowing defect; deferred provider construction is the documented behavior and changing auth signaling would widen scope.

Validation: 19 targeted core tests passed, 27 bundle-boundary tests passed, and affected ESLint/Prettier checks passed. The bundle test had one local 5-second subprocess timeout while run concurrently, then passed 27/27 when rerun sequentially.

@wenshao

wenshao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Local build & real-test verification — perf(startup): lazy-load Google GenAI SDK on first use

Verified as a maintainer against the current head b67c64770. The load-bearing claims — @google/genai leaves the ACP cold-start static closure, the ~999 KB reduction, deferred provider construction, and unchanged behavior — all reproduce exactly on a real build.

Setup (clean A/B, no shortcuts). Two isolated worktrees, each with a full npm ci (no node_modules symlink — mandatory here since the change lives in packages/core, which the bundle resolves through the workspace link):

  • HEAD = PR head b67c64770
  • BASE = merge-base 271497417 (HEAD is exactly BASE + the PR's 3 commits, so this isolates the PR)

Both bundled with the exact reviewer command (clean-package-build-artifacts && build --cli-only && DEV=true bundle). Anti-false-bundle check confirmed each metafile contains its own code: HEAD carries genai-compat.ts / LazyContentGenerator, BASE carries neither.


1. @google/genai removed from the ACP static closure — matches the claim to the byte

Running the PR's own findAcpImportBoundaryOffenders as the oracle against both metafiles:

metric (bytes) BASE HEAD Δ
ACP static closure total 14,289,021 13,289,701 −999,320
@google/genai in closure 755,788 0 −755,788
  • Guard fails-closed on BASE (ok=false, offender Google GenAI SDK, static path acpAgent-2WDIV2Z3.js → chunk-TYQMGACW.js (1,196,331 bytes)) and passes on HEAD (Startup bundle closure checks passed., exit 0).
  • The SDK is still shipped in HEAD — now in a dynamic chunk (chunk-22C7BQ6D.js, inStaticClosure=false), so provider/MCP first-use still works.
  • My −999,320 and 755,788 → 0 are identical to the PR's stated deltas. Absolute totals sit ~9.5 KB above the PR's figures only because my BASE is the merge-base (4 commits behind the PR's stated control) — the delta is identical.

ACP static closure A/B


2. Behavior unchanged & loading genuinely deferred — on real built code

  • genai-compat@google/genai 2.6.0 — 61/61 equivalence checks. A side-by-side harness against the real installed SDK: enum values (FinishReason, FunctionCallingConfigMode), 19 inputs × createUserContent/createModelContent deep-equal, role correctness, and 9 error inputs where both throw with byte-identical messages. (Broader than the 3-case shipped test.)
  • 844 unit/regression tests pass across every touched module — geminiChat (239), client (281), mcp-client (103), chatRecordingService (48), baseLlmClient (54), turn (35), agent-core (26), geminiRequest (12), plus genai-compat (7), contentGenerator (12) and the bundle guard (27). The contentGenerator suite pins the deferral: Gemini generator not built until first op, single-flight across concurrent first calls, Qwen OAuth creds checked before deferral, missing creds reject at session-create, and module-not-found → restart message + cause.
  • Runtime deferral, proven on the shipped dist via an ESM load hook: importing each of the 6 changed core modules evaluates @google/genai 0× on HEAD vs 1× on BASE; genai-compat.js itself pulls the SDK . This is the exact mechanism behind the closure reduction, observed at runtime.

Behavioral verification


Scope / not covered (honesty)

  • I did not reproduce the cold-start latency / peak-RSS numbers — those need the 2-vCPU reference host and are hardware-sensitive. My verification targets the structural + behavioral claims, which are the load-bearing ones for correctness and for the stated bundle win.
  • Live Gemini / Vertex / Anthropic network calls weren't exercised (no creds) — same limitation the PR states; those paths are covered by unit tests. The documented first-use trade-offs (aborted-first-request module eval, MCP background discovery loading the SDK) are intentional and called out in the design doc.
  • Environment: macOS (Darwin 24.6), Node 22.23.1.

Verdict

From a correctness and bundle-boundary standpoint this is solid and merge-ready: the @google/genai removal, the guard's fail-closed behavior, SDK-parity of the compat shim, deferral, and no-regression across the touched surface all check out on a real build. 👍

中文说明

本地构建 + 真实测试验证 — perf(startup): lazy-load Google GenAI SDK on first use

作为维护者针对当前 head b67c64770 进行了验证。核心(load-bearing)结论 —— @google/genai 移出 ACP 冷启动静态闭包、约 999 KB 的减少、provider 延迟构造、行为不变 —— 在真实构建中全部精确复现

环境搭建(干净 A/B,无捷径)。 两个隔离 worktree,各自完整 npm ci symlink node_modules —— 因为改动位于 packages/core,bundle 通过 workspace 链接解析它,symlink 会让 esbuild 误取主仓的 core,导致假 A/B):

  • HEAD = PR head b67c64770
  • BASE = merge-base 271497417(HEAD 恰好 = BASE + PR 的 3 个提交,因此精确隔离本 PR)

两者均用 reviewer 原始命令构建(clean-package-build-artifacts && build --cli-only && DEV=true bundle)。防「假 bundle」检查确认各 metafile 含自身代码:HEAD 含 genai-compat.ts / LazyContentGenerator,BASE 都没有。

1. @google/genai 移出 ACP 静态闭包 —— 与声明逐字节吻合

PR 自带的 findAcpImportBoundaryOffenders 作为 oracle,对两个 metafile 运行:

指标(bytes) BASE HEAD Δ
ACP 静态闭包总量 14,289,021 13,289,701 −999,320
闭包中的 @google/genai 755,788 0 −755,788
  • 守卫在 BASE 上失败(fail-closed)ok=false,offender Google GenAI SDK,静态路径 acpAgent-2WDIV2Z3.js → chunk-TYQMGACW.js (1,196,331 bytes)),在 HEAD 上通过Startup bundle closure checks passed.,exit 0)。
  • SDK 在 HEAD 中仍然打包,只是进入了动态 chunk(chunk-22C7BQ6D.jsinStaticClosure=false),provider/MCP 首次使用仍可用。
  • 我测得的 −999,320755,788 → 0 与 PR 声明的 delta 完全一致。绝对总量比 PR 高约 9.5 KB,仅因我的 BASE 是 merge-base(比 PR 声明的 control 落后 4 个提交)—— delta 相同

2. 行为不变 & 加载确实延迟 —— 基于真实构建产物

  • genai-compat@google/genai 2.6.0 —— 61/61 等价性检查。真实安装的 SDK 逐一对照:枚举值(FinishReasonFunctionCallingConfigMode)、19 组输入 × createUserContent/createModelContent 深度相等、role 正确性、以及 9 组错误输入两者均抛出且报错信息逐字节一致(比 PR 自带的 3 组用例更广)。
  • 844 项单测/回归测试全部通过,覆盖全部被改模块 —— geminiChat(239)、client(281)、mcp-client(103)、chatRecordingService(48)、baseLlmClient(54)、turn(35)、agent-core(26)、geminiRequest(12),外加 genai-compat(7)、contentGenerator(12) 与 bundle 守卫(27)。contentGenerator 用例锁定延迟语义:首次操作前不构造 Gemini generator、并发首调用单飞、Qwen OAuth 凭证在延迟之前校验、缺失凭证在 session 创建时拒绝、module-not-found → 重启提示 + cause。
  • 运行时延迟在打包 dist 上验证(ESM load hook): 导入 6 个被改 core 模块,各自对 @google/genai 的求值次数 HEAD 为 0,BASE 为 1genai-compat.js 自身求值 SDK 0 次。这正是闭包缩减的机制,运行时可观测。

范围 / 未覆盖(如实说明)

  • 复现冷启动延迟 / 峰值 RSS 数字 —— 需 2-vCPU 参考机且对硬件敏感;本次验证聚焦结构性 + 行为性结论,这些才是正确性与 bundle 收益的关键。
  • 未做真实 Gemini / Vertex / Anthropic 网络调用(无凭证)—— 与 PR 声明一致,相关路径由单测覆盖。设计文档中列出的首次使用权衡(已中止的首请求仍会完成模块求值、MCP 后台 discovery 会加载 SDK)为有意为之。
  • 环境:macOS(Darwin 24.6),Node 22.23.1。

结论

从正确性与 bundle 边界角度看,本 PR 扎实、可合并@google/genai 移除、守卫 fail-closed 行为、compat shim 的 SDK 等价性、延迟加载、以及被改面无回归,均在真实构建中验证通过。👍

@doudouOUC
doudouOUC requested a review from yiliang114 July 23, 2026 01:31
@wenshao

wenshao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@doudouOUC
doudouOUC added this pull request to the merge queue Jul 23, 2026
Merged via the queue into QwenLM:main with commit 7c73768 Jul 23, 2026
98 checks passed
@doudouOUC
doudouOUC deleted the perf/lazy-google-genai branch July 23, 2026 02:08
chiga0 pushed a commit that referenced this pull request Jul 23, 2026
* perf(startup): lazy-load Google GenAI SDK on first use

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

* codex: address PR review feedback (#7512)

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

* codex: address PR review feedback (#7512)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
yiliang114 added a commit to he-yufeng/qwen-code that referenced this pull request Jul 23, 2026
)

* fix(cli): correct queued message display style and ordering

Mid-turn steer messages (user input queued while the model is
responding) had two display bugs:

1. They rendered with notification styling (● icon) instead of
   user-input styling (> prefix) because accept() added them to
   UI history as MessageType.NOTIFICATION.

2. They appeared below the model's reply because accept() was
   only called in the finally block after the entire response
   stream completed, appending the user message after all model
   response items.

Fix: use MessageType.USER with sentToModel: true for steer
messages, and settle the steer input on the first stream event
(after the user-content push lands but before model-response
events are committed to UI history). Pass steer inputs through
to recursive sendMessageStream calls so all takeSteerInput paths
benefit from early settlement. Add a WeakSet guard to
settleSteerInput for idempotency across recursive invocations.

* test(core): add ordering test for early steer settlement

Verify that accept() is called after the first stream event is
pulled but before subsequent events reach the consumer, pinning
the settle-before-content timing that ensures queued user
messages render above the model's reply.

* fix(cli): use sentToModel: false for steer messages, address review

- Use sentToModel: false instead of true: steer messages are injected
  into an existing tool-result turn, not standalone user turns.
  sentToModel: true would make isRealUserTurn() count them as real
  turns, inflating the rewind turn index.
- Remove unnecessary as HistoryItemWithoutId cast.
- Add post-cleanup assertion in ordering test to verify the WeakSet
  guard prevents double-settlement.

* fix(cli): align resumed mid-turn steer display with live session (#7381)

Resume path now renders mid_turn_user_message as MessageType.USER with
sentToModel: false, matching the live-session styling. Add a comment
documenting the intentional sentToModel: false choice.

* fix(cli): exclude steer messages from user-turn filters (#7381)

Steer messages (sentToModel: false) were counted as real user turns by
five downstream consumers that filter on type === 'user' without checking
sentToModel, breaking cancel auto-restore, telemetry turn count, prompt
recall, away-recap thresholds, and resume collapse boundaries.

Add sentToModel !== false guards at each site.

* test(cli): add coverage for sentToModel !== false guards (#7381)

* test(cli): add coverage for sentToModel !== false guard in input-history filter (#7381)

* test(cli): add coverage for sentToModel !== false guard in YOLO turn-count telemetry (#7381)

* fix(cli): restore corrupted docs and classify steer items as synthetic (#7381)

* fix(docs): restore corrupted autogenerated input names in GitHub Action docs (#7381)

* fix(cli): deduplicate findLastUserItemIndex and add steerInput forwarding test (#7381)

* fix(cli): keep code-block copy numbering continuous across steer items (#7381)

* test(core): add Hook continuation steerInput forwarding test

Verify that steerInput is forwarded through the Stop-hook
continuation path and settled early on the first content event
of the continuation turn, matching the existing Steer
continuation coverage.

* fix(cli): sync selection test fixtures with ink FrameCell/ReadonlyFrame types (#7381)

* fix(core): align cron day wildcard semantics (#7464)

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>

* feat(core): keep completed background agents resident (#7426)

* feat(core): keep background agents resident

* fix(core): harden background continuation boundaries

* docs(core): move per-spawn cleanup comment to subagentDispose

The comment describing the per-spawn cleanup (which stays undefined on
the fork-resume path) had drifted above the launchModel declaration,
where it no longer applied and could mislead readers. Relocate it to the
subagentDispose assignment in the non-fork branch it actually documents.

* fix(core): close finishing window and release resident on error in background GOAL path

- Non-worktree GOAL completion drained the message queue but never called
  registry.beginFinishing(), unlike the worktree path. A send_message racing
  the terminal transition could be accepted (status still running,
  finishingAgents empty) and then orphaned by complete(). Call beginFinishing()
  after the empty drain to reject the racing message instead.
- The completion catch block never reset keepResident, so a throw from
  patchAgentMeta/registry.complete left the runtime resident but finalized as
  failed — a zombie that cleanupRuntime never reclaimed. Reset keepResident in
  the catch so the finally block disposes it.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* ci(autofix): continue environment-specific fixes (#7444)

* ci(autofix): continue environment-specific fixes

* docs(autofix): align verification wording

* docs(autofix): require bundle before integration tests

* docs(autofix): scope surrogate verification rules

* docs(autofix): require focused tests before integration checks

* docs(autofix): clarify review verification guidance

* fix(acp-bridge): close prompt-terminal follow-ups from the PR #7400 self-review (#7453)

* fix(acp-bridge): close prompt-terminal follow-ups from PR #7400 self-review

Keep a removed RUNNING prompt visible to the teardown flush via a removed flag so its terminal still publishes when the session closes before the agent cooperates; gate broadcastTurnError's session turn-state mutation to running prompts; propagate the typed PromptDeadlineExceededError from the pre-dispatch abort check; document the deadline FIFO-release overlap trade-off, the trailing prompt_cancelled after flush, and the result.then/finally ordering invariant; route the dedup log to the debug channel; drop the prompt-deadline re-export that pulled the bridge into a leaf module.

Fixes #7451

* test(acp-bridge): cover promote-then-remove-then-settle duplicate completed guard (#7453)

---------

Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>

* fix(core): strip Qwen-internal daemon secrets from agent-spawned child env (#7256)

* fix(core): strip Qwen-internal daemon secrets from agent-spawned child env

Shell subprocesses (and the monitor tool and stdio MCP servers) inherited
the full daemon process.env, including QWEN_SERVER_TOKEN (the serve-daemon
bearer credential), so an agent-run command like printenv QWEN_SERVER_TOKEN
could read an internal secret. Add a shared sanitizeChildEnv() that removes
Qwen-internal daemon/server tokens (QWEN_SERVER_TOKEN, QWEN_DAEMON_TOKEN)
before spawning, and apply it at the shell child_process + PTY paths,
monitor.ts, and the mcp-client stdio transport.

The denylist is deliberately narrow: it does NOT strip third-party
credentials (GH_TOKEN, AWS_*, NPM_TOKEN, ...) that real shell workflows
legitimately inherit -- only Qwen-internal secrets. Exported from the
package root so the desktop denylists can consolidate onto it later.

Fixes #6601.

* test(core): cover daemon-secret stripping on monitor and mcp-client spawn sites

* test(core): replace process.env instead of mutating in shell sanitization tests

The file restores process.env by reference in afterEach, so in-place key
mutations leaked into later tests. Use the replacement pattern already used
by setupConflictingPathEnv.

* docs(core): align JSDoc @param names with actual function signatures (#7492)

Fix 6 instances where JSDoc @param tags had drifted from their
corresponding function signatures — parameters were renamed, removed,
or undocumented over time but the doc blocks were not updated.

Closes #7446

* feat(serve): support forced MCP reconnects (#7488)

* feat(serve): support forced MCP reconnects

* test(serve): cover forced MCP reconnect options

---------

Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>

* fix(cli): insert newline on Shift+Enter and stop streaming thinking-block flicker (#7397)

* fix(cli): re-push Kitty keyboard flags onto the alternate screen in VP mode

In VP mode the app renders on the alternate screen (`alternateScreen: true`),
but the Kitty keyboard progressive-enhancement flags were pushed only once at
startup on the main screen. The Kitty spec tracks these flags per screen
buffer, so the alternate screen's stack stays empty and the terminal never
reports modifiers: Shift+Enter arrives as a bare Enter (submit) or, when the
terminal emits an ESC-prefixed variant, as an orphaned Escape that trips the
empty-buffer double-Esc rewind prompt — so Shift+Enter can never insert a
newline in VP mode even on Kitty-capable terminals (e.g. cmux).

Re-push the flags onto the alternate screen right after Ink enters it (Ink
writes the enter-alt-screen sequence synchronously inside render(), so the
push is correctly ordered). Ink discards the alternate screen and its flag
stack on unmount, leaving the startup main-screen push balanced by the
existing disableKittyProtocol() on cleanup.

Generated with AI

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

* fix(cli): stabilize streaming thinking block height to stop flicker

The pending "Thinking…" block renders the tail of the reasoning stream in a
content-sized box. As the model emits paragraph separators, a blank line
enters and leaves the tail window (and `trimEnd` drops trailing blanks), so the
visible line count oscillates and the block flickers 2→3→5 rows during
streaming.

Track the tallest height the block has reached for the current thought and
never render fewer rows than that (capped at the streaming window size),
padding at the top so the newest line stays pinned to the bottom. The tracker
resets when streaming ends or when the buffer shrinks (a new thought replaced
it), so height is monotonic within a thought without leaking across thoughts.

Generated with AI

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

* fix(cli): decode xterm modifyOtherKeys Shift/Ctrl/Alt+Enter so it inserts a newline

Terminals such as Ghostty report Shift+Enter as the xterm modifyOtherKeys
sequence `ESC [ 27 ; <mods> ; <key> ~` (e.g. `ESC [ 27 ; 2 ; 13 ~`) when the
Kitty keyboard protocol is not negotiated — which is the default, since Kitty
detection does not always succeed. Two bugs kept this from inserting a newline:

1. The CSI-u parser read the leading `27` marker as the key code (matching the
   Escape key code 27) instead of the real key code in the third parameter, so
   with Kitty enabled Shift+Enter was mistaken for Escape and tripped the
   double-Esc rewind prompt.
2. The reassembly path that stitches readline's shredded CSI fragments back
   together was gated behind `kittyProtocolEnabled`, so with Kitty disabled the
   `ESC [ 27 ; 2 ;` head plus the stray `13~` tail leaked into the composer as
   literal text and no newline was inserted.

Decode the third parameter as the real key code for the `27;…~` form, and route
those sequences through the reassembly buffer even when Kitty is off (only the
`ESC [ 27` marker opts in, so keys readline already parses cleanly are
untouched). Shift/Ctrl/Alt+Enter now insert a newline in both VP and non-VP
mode regardless of Kitty negotiation.

Generated with AI

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

* fix(cli): anchor VP viewport to the top until a conversation turn exists

On a fresh VP-mode session the virtualized list holds the banner plus startup
notices (tips / MOTD / info), so it is longer than one item. Keying the initial
scroll anchor off list length alone selected scroll-to-end, which pinned the
banner to the bottom of the full-height viewport and left the top half of the
screen blank.

Anchor to the top until there is an actual conversation turn (a user/user_shell
history item or a pending response), then resume scroll-to-end so the latest
output stays in view. Startup notices no longer count as content that forces
bottom alignment.

Generated with AI

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

* fix(cli): stabilize streaming thinking window against availableTerminalHeight drift

The grow-only streaming thinking window still flickered because its line cap was
derived from availableTerminalHeight. While a thought streams the terminal keeps
constrainHeight on, so availableTerminalHeight (and the derived maxLines) drifts
up and down as sibling pending content grows, and the grow-only clamp
`min(maxLines, …)` shrank the block whenever it dipped.

Use a constant window height (MAX_STREAMING_THINKING_VISUAL_LINES) for the
pending window instead. The window is only a few lines, so a fixed cap cannot
meaningfully overflow (VP scrolls anyway), and the height stays stable while
still growing monotonically within a thought.

Generated with AI

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

* Revert "fix(cli): anchor VP viewport to the top until a conversation turn exists"

This reverts commit fbe86a9e159b75ea1f5b689cc327599c9dc91090.

* fix(cli): guard modifyOtherKeys detection against keypresses without a sequence

The modifyOtherKeys prefix check ran on every keypress, but some synthetic
keypresses (and the useKeypress test harness) emit a key with no `sequence`,
so `key.sequence.startsWith(...)` threw an unhandled rejection. Use optional
chaining so a missing sequence is simply not a modifyOtherKeys start.

Generated with AI

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

* test(cli): mock pushKittyProtocolFlags in gemini.test.tsx kitty mock

The kittyProtocolDetector mock omitted the newly added pushKittyProtocolFlags
export. Add it so the mock stays in sync with the real module and a VP-mode
startup path exercised through this suite cannot hit an undefined call.

Generated with AI

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

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): open singleton subagent details (#7495)

Co-authored-by: ytahdn <ytahdn@gmail.com>

* fix(web-shell): avoid redundant git status requests (#7496)

Co-authored-by: ytahdn <ytahdn@gmail.com>

* fix(agent): ignore empty working_dir placeholders (#7343)

* fix(agent): ignore empty working_dir placeholders

* test(agent): align empty working_dir expectations

* feat(prompts): allow overriding core identity via QWEN_SYSTEM_IDENTITY_MD (#7478)

* feat(prompts): update prompts.ts for QWEN_SYSTEM_IDENTITY_MD

* feat(prompts): update prompts.test.ts for QWEN_SYSTEM_IDENTITY_MD

* fix(prompts): address CR on QWEN_SYSTEM_IDENTITY_MD

Keep getDefaultCoreIdentitySentence private, fail loud on path
resolution errors, use trimEnd, and resolve identity only on the
default-prompt branch.

* test(prompts): align identity override tests with CR feedback

Sample default identity from live prompt, cover trimEnd trailing
whitespace, and assert homedir resolution failures throw.

---------

Co-authored-by: 易良 <1204183885@qq.com>

* fix(cli): yield to single-slot background agents (#7258)

Co-authored-by: hogeheer <267467744+hogeheer499-commits@users.noreply.github.com>

* docs(autofix): require evidenced pre-commit verification, not a bare "verified" (#7486)

* docs(autofix): require evidenced pre-commit verification, not a bare "verified"

The skill already said to run build/typecheck/lint/Vitest before
committing, but softly — and #7408 committed a fix with a TS error the
gate then rejected while its summary claimed "verified all 3 commits".
A self-assessment the gate contradicts wastes a whole round.

Strengthens the address-review contract from "run the checks" to:
- actually run them, do not assert them from reading the diff;
- if typecheck or a touched-package test fails, do NOT commit — treat
  the feedback as unresolved (failure.md);
- end address-summary.md with a `## Verification` section listing each
  command run and its result; a bare "verified" is not acceptable.

The framing is structural, not etiquette: the deterministic gate re-runs
the same commands and discards the round on any failure, so skipping them
only moves the rejection later. Pinned by a test so it cannot soften back.

This is the checkable half of "audit before committing" — the
undirected/reverse-audit-until-clean practice does not transfer to an
unsupervised agent (no verifiable stopping condition, and it worsens the
timeouts seen on large PRs), but "run the gate's own checks first and
show the evidence" does.

* fix(autofix): clarify Verification section precedes collapsed Chinese translation (#7486)

---------

Co-authored-by: wenshao <wenshao@example.com>
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>

* feat(autofix): stop a PR that fails to push for N rounds in a row (#7482)

* feat(autofix): stop a PR that fails to push for N rounds in a row

Under takeover the round cap is 100, which is right for a PR that needs
many PRODUCTIVE rounds. It is wrong for one that fails every round: #6723
ran 7 consecutive failed rounds (3 agent timeouts at 50 min, 4 gate
rejections whose fix broke tests) over 8 hours, heading for round 100,
because it is a 5700-line, 47-file, 5-day-old PR racing a fast-moving
main — every round re-resolves a conflict it cannot finish or that fails
the gate. Retrying at the same per-round budget will not converge; a
human has to rebase or split it.

Adds CONSECUTIVE_FAILURE_CAP (5), distinct from the total round cap. The
handoff step already runs only when a round did NOT push, so it counts
the unbroken run of prior failure markers — stopping at the first push
("Addressed the latest review feedback") or legitimate no-op ("no
changes needed"), either of which proves progress and resets the streak.
At the cap it forces the terminal round even under takeover, with a
handoff that names the real fix (rebase/split, then /retry). Cause-
agnostic: a timeout and a gate rejection both count.

* fix(autofix): address review feedback on consecutive-failure circuit breaker (#7482)

- Fix misleading comment: the walk is oldest-first (API order) with
  reset-on-success, not newest-first with early stop
- Prefer the already-fetched ic.json over a redundant gh api call,
  falling back to the API only when the file is missing
- Filter eval markers by re-arm window (win=) so pre-re-arm failures
  do not immediately re-terminate a re-armed PR
- Add test coverage for the MARK_ROUND == MAX_ROUNDS guard and for
  window-scoped streak counting

* fix(autofix): exempt transient model errors from consecutive-failure breaker (#7482)

---------

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

* feat(core): restore background agent roster (#7459)

* feat(core): restore background agent roster

* fix(web-shell): add list_agents to TOOL_DISPLAY_NAMES

The new list_agents core wire tool was added to core's ToolNames but not
to the web-shell TOOL_DISPLAY_NAMES map, causing toolFormatting.drift.test.ts
to fail (expected ['list_agents'] to deeply equal []). Add the missing
'ListAgents' display-name entry so the browser panel shows a friendly name
instead of the raw wire name and the drift guard passes.

* fix(cli): reload old-session background agents on failed resume rollback

When /resume fails after core has swapped but before the UI swap, the catch
block rolls core back to the old session via startNewSession(oldSessionId).
However the forward path already called resetBackgroundStateForSessionSwitch,
which cleared the old session's in-memory background agents. The rollback did
not reload them, so list_agents returned empty for the old session (whose
sidecars are still on disk) until the next process start or successful resume.

Reload the old session's paused background agents after rolling core back, so
the restored roster matches on-disk state. Placed after startNewSession so the
loadPausedBackgroundAgents current-session guard is satisfied; best-effort via
.catch so it never blocks the rollback path.

* fix(web-shell): add zh translation for list_agents tool name

The toolFormatting test 'has a zh translation for every tool in the
display-name map' failed with expected ['list_agents'] to deeply equal []
because list_agents was added to TOOL_DISPLAY_NAMES without a matching
toolName.list_agents zh-CN entry. Add the translation to restore parity.

* fix(cli): resolve CI failures for background-agent roster restore

- Add toolDisplayName.ListAgents translations (en, zh, zh-TW, ca) so the
  new list_agents tool has a zh entry; fixes i18n/index.test.ts.
- Add loadPausedBackgroundAgents and consumePendingRecoveredAgentsNotice
  to the acpAgent worktree test config mock, which loadSession now calls
  via #restoreBackgroundAgentsOnResume; fixes acpAgent.worktree.test.ts.

* refactor(core): extract incompatible-isolation blocked reason to a const

Move the incompatible-isolation blocked-reason string out of an inline
literal into a module-level INCOMPATIBLE_ISOLATION_BLOCKED_REASON const,
matching its four sibling reasons so the text is discoverable by
constant-name grep and edited alongside the others.

* fix(core): preserve retained activity state on failed agent revive

Address review feedback on the background-agent roster restore:

- On a failed completed-agent revive, restore UI state with a non-empty
  guard instead of `??`. Because `restorePausedEntry` resets the paused
  entry's `recentActivities` to `[]`, the previous `failedEntry?.field ??
  completedEntry.field` kept that empty array and dropped the pre-revive
  snapshot (the UI Progress section rendered empty). Applied consistently
  to pendingMessages, recentActivities, and pendingApprovals.

Add regression coverage for previously untested paths:

- failed revive preserves pre-revive recentActivities
- terminal-agent cap admits only the newest MAX_RETAINED_TERMINAL_AGENTS
  completed sidecars on restore
- /resume rollback reloads the old session's background agents
- headless resume prepends the recovered-agents notice to the prompt

* test(cli): cover interrupted-turn continuation not consuming recovered-agents notice

Add ACP and headless regression tests asserting an interrupted-turn
continuation does not consume the one-shot recovered-agents notice
(the !isContinue / !continueInterrupted guards), so it is delivered on
the user's next ordinary prompt. Mirrors the existing slash-command
coverage.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(cli): support custom skill directories via settings (#7395)

* feat(cli): support custom skill directories via settings (#7394)

Add skills.directories setting that accepts an array of additional
directory paths to scan for skills (SKILL.md files). Paths support
~ expansion. Directories are scanned recursively at user level,
after the default ~/.qwen/skills/ directory.

Example settings.json:
{
  "skills": {
    "directories": ["~/.agent/skills", "~/.claude/skills"]
  }
}

Changes:
- settingsSchema.ts: add skills.directories array setting
- core Config: add customSkillDirs param and getCustomSkillDirs()
- SkillManager: append custom dirs to user-level skill base dirs
- CLI config: read skills.directories and pass to core Config

* fix(cli): regenerate settings schema for skills.directories (#7394)

* fix(core): address review feedback for custom skill directories (#7395)

- Use optional chaining for getCustomSkillDirs() to prevent TypeError
  on partial Config mocks (workspace-skill-management, workspace-skills-status)
- Reuse expandHomeDir utility instead of inline tilde expansion
- Fix inaccurate 'scanned recursively' wording to 'one level deep'
- Correct JSDoc: paths are raw, expansion happens in SkillManager
- Trim whitespace from custom dir entries in CLI layer
- Add tests for custom dir expansion, dedup, and partial config safety

* fix(core): address review feedback for custom skill directories (#7395)

* fix(core): address review feedback for custom skill directories (#7395)

* test(core): add relative path resolution test for custom skill dirs (#7395)

* fix(cli): add Array.isArray guard for skills.directories and safe mode test (#7395)

* fix(skills): address review feedback on custom skill directories (#7395)

- Add bare mode test for skills.directories guard
- Include resolved absolute path in relative directory warning
- Clarify that dedup applies to default user dirs, not bundled skills
- Regenerate settings schema

---------

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>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>

* fix(core): add image modality support for qwen3.8-max and kimi-k3 models (#7491)

* fix(core): add image modality support for qwen3.8-max models

qwen3.8-max-preview supports image input but was falling through to the
catch-all text-only rule because no pattern matched it. This caused the
vision bridge to unnecessarily transcribe images via a secondary model
instead of sending them directly to the primary model.

* fix(core): also add image modality for kimi-k3

Kimi K3 officially supports image + video input but was falling through
to the catch-all text-only rule, same issue as qwen3.8-max.

* fix(dingtalk): preserve non-bot mention context (#7473)

* fix(dingtalk): preserve non-bot mention context

* test(dingtalk): cover plural mentions, staffId fallback, and edge cases (#7473)

---------

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

* fix(core): harden the usage salvage around session deletion (#7425)

Post-merge review follow-ups on #7391 (three findings):

- Salvage the archived transcript in the active-branch deletion too:
  when both copies co-exist (an interrupted archive) and the fresh
  active transcript carries no telemetry, the archived copy holds the
  session's usage history and was deleted unsalvaged. The dedup guard
  makes the extra call a no-op whenever the active copy already wrote.
- Enforce the "never blocks deletion" contract at the call site: a
  salvageUsageBestEffort wrapper catches and warns, so the guarantee is
  structural rather than an implementation detail of
  persistUsageBeforeTranscriptDeletion. The new failure-tolerance test
  (salvage rejects -> deletion still succeeds) fails without the
  wrapper — the bare await let the rejection escape through
  removeSessionFiles' rethrowing catch.
- Clear the salvage module mock in beforeEach so the wiring test's
  invocationCallOrder assertions can never read stale calls.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(core): make fork subagents discoverable (#7460)

* test(core): cover Shell truncation without an artifact (#7470)

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

* fix(ci): autofix route checks existing labels on non-trigger label events (#7481)

* fix(ci): autofix route checks existing labels on non-trigger label events

When triage adds multiple labels in sequence, per-issue concurrency
cancels earlier runs. If the last label is not a trigger label
(e.g. scope/build-system), the surviving run skips the issue phase
even though the issue already has autofix/approved +
status/ready-for-agent.

Before ignoring a non-trigger label event, check ISSUE_LABELS_JSON
for both required labels. If present and the issue is open, proceed
with the issue phase. Trust was already established when the trigger
labels were applied (both require triage+ permission).

* fix(ci): require trusted sender for label fallback

* feat(cli): preserve semantic text when copying VP selections (#7286)

* docs(cli): define semantic copy fidelity scope

* docs(cli): address semantic frame review gaps

* docs(cli): preserve soft-wrap source separators

* feat(cli): preserve semantic selection copy

* fix(cli): address semantic copy review findings

* fix(cli): preserve clipped semantic boundaries

* fix(cli): limit separator carrier joiner to visible width in wrap metadata

The greedy /\s+/ match in wrapTextWithMetadata could capture more
source whitespace than the separator carrier row actually consumed
(e.g. a tab following a space), causing duplicated whitespace in
semantic copy. Limit the match to visibleLine.length characters and
add a mixed space/tab regression test.

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>

* test(core): stub the registry methods agent.ts actually calls (#7538)

The shared stubRegistry in agent.test.ts was missing six methods that
agent.ts reaches: bridgeApprovalEvents, getQueuedCount,
registerResidentAgent, restartCompletedAgent, unregisterResidentAgent and
waitForMessages.

That is not a benign omission. The background body wraps its work in a
try/catch that routes any throw into registry.fail(), so a missing method
never surfaces as 'not a function' — it silently converts a successful
run into a failed one. On the GOAL completion path
unregisterResidentAgent is called immediately before complete(), so the
TypeError replaced the completion entirely:

  registry.fail('fork-...', 'registry2.unregisterResidentAgent is not a
  function', ...)

That is what broke 'runs a non-interactive fork through the background
registry' on main. #7460 added the registry.complete assertion, which
exposed the incomplete stub — before it, nothing checked whether the
background body finished successfully and the TypeError was swallowed.

Stub all six with their real return shapes (unregisterResidentAgent
returns boolean, bridgeApprovalEvents returns the unsubscribe callback
agent.ts later invokes, waitForMessages resolves to a list) and assert
registry.fail was not called before asserting completion, so a future
gap reports the actual error instead of 'complete: 0 calls'.

* perf(startup): lazy-load Google GenAI SDK on first use (#7512)

* perf(startup): lazy-load Google GenAI SDK on first use

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

* codex: address PR review feedback (#7512)

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

* codex: address PR review feedback (#7512)

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

---------

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

* fix(vscode): use file picker image paths for vision input (#7493)

* fix(vscode): use image paths from file picker

* fix(vscode): keep image picker paths raw

* fix(vscode): resolve image picker paths on submit

* fix(vscode): send picked images as vision context

* fix(vscode): encode prompt image file URIs

* fix(vscode): address image path review comments

* test(vscode): cover image file reference edge cases

* fix(cli): open the actual serve fallback port (#7501)

* fix(cli): open actual serve fallback port

* test(cli): match serve URL to fallback listener

* docs(cli): clarify serve listen error handling

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(ci): don't let one failing scenario sink the whole visual preview (#7511)

The web-shell visuals render runs every screenshot and flow in a single
`test:e2e:visuals`, and that step had no `continue-on-error`, while the compose
and upload steps had no `if: always()`. So one failing or timing-out scenario
failed the job, the artifact was never uploaded, and the publish workflow had
nothing to post — the entire preview vanished even when every other scenario
passed and its PNG was already on disk. A flow (a long multi-click sequence) is
the most fragile scenario kind, so the fragile one silently takes down the
deterministic screenshots. PR #7498 hit exactly this: 29 scenarios passed, one
new channel-management flow timed out, and the PR got no preview and no comment
at all.

Make the after-capture step `continue-on-error` so the passing captures survive
and the later steps still compose and upload them. The publish job only runs on
a `success` conclusion, so the job must stay green — but a masked failure must
not read as a clean preview. Ship the step's real `.outcome` (which
continue-on-error does NOT mask, unlike `.conclusion`) to the publisher as
`render-status.txt`, and have the comment builder use it: an empty preview whose
render failed says "one or more scenarios failed to render" and is explicitly
NOT the reassuring green check or the coverage-gap prompt (both imply the render
ran); a partial preview is labelled partial above the shots that did render. A
missing status file (older run) defaults to complete, so this only ever adds a
warning, never suppresses a real preview.

The failing scenario still needs fixing — it's now surfaced in the comment
rather than by silently deleting everyone else's preview.

Co-authored-by: wenshao <wenshao@example.com>

* feat(web-shell): add selective shadow DOM isolation (#7551)

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>

* feat(web-shell): add renderChatHeader slot for custom session header (#7553)

* fix(cli): say review coverage gaps in the author's units, not chunk ids (#7550)

The posted review body rendered coverage disclosures with the run's own
bookkeeping as subjects: bare chunk ids, unsorted, one per subject. On a
run that certified nothing (PR #7268) the body enumerated all 49 chunk ids
across two sentences while opening with "Reviewed. Suggestions are
inline." — the opener certified the exact thing every following sentence
took back, and nothing on the PR page maps a chunk id to code.

Three changes, all render-time — the structural entries, the caps, the
caller-echo dedup and the stderr remediation still key on chunk ids, which
is where the id is the selector a reader can act on:

- Coverage now returns the plan's chunk→files table (DiffChunk.files was
  already in the plan JSON; the coverage type slice dropped it).
- compose-review renders chunk gaps through describeChunkGap: every
  planned chunk collapses to "the entire diff", a narrow gap with known
  files names the files, and anything wider is counted against the plan's
  total. Applied to the receipt sentence, the uncoverable sentence (bare
  CLI entries only — caller-authored entries render verbatim) and the
  grouped per-cause sentences.
- The COMMENT opener may no longer say "Reviewed." over a disclosure set
  that denies it: when no chunk is both covered and undisclosed — or no
  chunk universe could be read at all — it opens with a zero-certified
  warning instead. A rewritten launch demonstrably read its chunk, so
  coverage alone is not the test; certified is covered with no disclosure
  against it.

Co-authored-by: verify <verify@local>

* fix(autofix): retry a skipped-Prepare instead of stranding the PR terminal (#7490)

* fix(autofix): retry a skipped-Prepare instead of stranding the PR terminal

A base/infra failure BEFORE the agent runs was misread as an agent crash
and terminated the PR forever. When an early step fails — installing or
building the trusted base, checkout, node setup — the `Prepare branch and
feedback` step is skipped, so NEWEST is empty, and the report step's
"crashed before reading feedback" branch fired: MARK_ROUND=MAX_ROUNDS,
terminal, scan skips it on every future tick.

Observed: a web-shell TypeScript break on `main` failed `Install
dependencies and build` (which builds the trusted base) across a whole
scan batch, and SIX healthy PRs were stranded terminal at round=100 in
one run — including ones at round 9 and 11 that had nothing to do with
the break. `round=100` there is a terminal sentinel, not 100 attempts.

NEWEST-empty now splits on steps.prepare.outcome:
- 'skipped' (an earlier step failed, the agent never ran) is infra/base
  and transient: retry with a sentinel ts so the feedback stays live,
  incrementing the round so a PERSISTENTLY broken base is still bounded
  and stops at the cap (recoverable with /retry).
- 'success'/'failure' (Prepare ran, no feedback produced) is a genuine
  pre-read agent crash: unchanged terminal behaviour.

This is the reverse of the asymmetry #7482 addresses: that bounds a
crash AFTER reading that retried forever; this stops a transient failure
BEFORE reading from going terminal after one.

* docs(autofix): note a pre-Prepare cancel also retries intentionally (#7490)

* fix(autofix): also retry a cancelled/empty prepare outcome, not just skipped

A previous review comment on this PR noted that a job cancelled before
Prepare should retry too. It was right about the intent but the code did
not do it: `steps.prepare.outcome` is 'cancelled' for a cancel and '' for
a job that stopped before Prepare entered the step context — both DISTINCT
from 'skipped', so `== 'skipped'` sent them to the terminal branch, the
same over-termination this PR exists to fix.

Match on "not a real Prepare run" (`!= 'success' && != 'failure'`)
instead, so skipped, cancelled, and empty all retry; only a Prepare that
actually ran to a verdict (success/failure) with no feedback stays
terminal — the genuine pre-read agent crash. Test extended to drive the
cancelled and empty cases (retry) and both real-run outcomes (terminal);
mutation-verified that reverting to `== 'skipped'` reddens the cancelled
case.

* test(autofix): update the pre-read-crash case for the broadened retry

The prior commit broadened NEWEST-empty retry to skipped/cancelled/empty
but left the older 'replays the handoff decision' test asserting the old
terminal behaviour for an unset PREPARE_OUTCOME (which now retries). That
test's terminal cases now set PREPARE_OUTCOME=success/failure explicitly —
the only outcomes that still terminate — so it exercises the genuine
pre-read agent crash rather than the infra/cancel path.

* test(autofix): anchor the skipped-Prepare extraction past the CONSEC block

CI reddened `retries a skipped-Prepare` after main's consecutive-failure
cap (#7482) merged into this branch: that block was inserted between this
decision block and the report `{`, and it calls `gh api`. The test's
`{`-anchored regex over-captured through it, so the extracted script ran
the unstubbed `gh api` and failed. Anchor the end on the same
`# Consecutive-failure` comment the sibling gate-crash test already uses,
so the extraction stops at this decision block's own closing `fi`.

* fix(autofix): exempt skipped-Prepare from the consecutive-failure breaker

A broken base build skips Prepare, producing no API error file — so the
consecutive-failure breaker ran on the new retry path and, after 5
scans, re-introduced the exact mass-stranding this PR exists to prevent.
Exempt pre-agent infra failures (skipped/cancelled/empty outcome) from
the breaker, mirroring the transient 429/5xx exemption: same failure
class (not the PR's fault, self-heals, hits the whole batch). The round
cap + sentinel-ts /retry recovery already bounds a persistently broken
base.

Also trim "checkout" from the retry headlines (checkout failures do not
land in this branch) and hoist the duplicated MARK_TS assignment.

* fix(autofix): reset the consecutive-failure streak on prior infra-failure markers

The streak walker counted prior infra-failure headlines ("AutoFix could
not start —…") as failures, inflating the consecutive-failure count on
subsequent rounds.  A PR with 3 real agent failures, then 3 rounds of
base-build infra failures, then 1 more real failure would trip the
cap-5 breaker even though only 4 rounds were the PR's fault.

Add the two infra-failure headline patterns as reset strings in the
streak walker, alongside the existing push and no-op resets.  The
genuine agent-crash headline ("AutoFix could not start evaluation —…")
is deliberately excluded — it is a real failure and must still count.

* fix(autofix): clarify infra-failure headlines and else-branch comment (#7490)

Address review nits: the retry headline now mentions cancelled runs,
the cap headline says 'reached the round cap' instead of overstating
'could not start for N rounds', the else-branch comment says 'prepare
itself crashed' instead of 'agent crash', and the streak-reset pattern
is simplified now that both infra headlines share the same prefix.

---------

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>

* fix(cli): keep role codenames and brief paths out of the posted review body (#7560)

The posted body still carried two operator registers #7550 left in place:
roster role subjects rendered their internal codenames ("Agent 1c:
Cross-file tracer", "Test coverage matrix (whole-diff)"), and an unread
brief's disclosure interpolated its filesystem path. And when verify and
the reverse audit failed the same way, the body said it twice, in two
near-identical sentences.

- Every Brief now carries a publicLabel — the dimension said as what it
  checks ("the cross-file consistency pass") — and coverage's structural
  disclosures carry it as publicSubject beside the internal subject, plus
  a path-free publicReason for unread briefs. The internal label and the
  path stay on stderr, where they are the selector an operator acts on;
  every dedup and certification check still keys on the internal subject.
- compose-review renders the public fields and groups by the reason the
  body PRINTS, so two unread briefs share one path-free sentence instead
  of repeating it per role.
- verificationGaps merges verify and reverse-audit failures of the same
  delivery shape into one sentence with both subjects and both
  consequences; mixed shapes keep their precise per-role texts, and the
  per-role rebuild commands stay on stderr either way.

Co-authored-by: verify <verify@local>

* fix(autofix): retry an agent timeout instead of advancing past its feedback (#7563)

A timeout evaluated NOTHING — the agent ran out of budget before finishing,
so nothing was committed and the feedback is unaddressed. It was treated as
an evaluated verdict (real ts, watermark advances), which strands that
feedback: the next scan sees "nothing new" and never retries. Observed on
#7471 (round 13/100), a heavily-reviewed 1871-line PR: rounds 11 and 13
timed out, but round 12 pushed — so a timeout is transient far more often
than not, and advancing past it left the round-13 feedback unhandled.

run-agent.mjs now drops an `agent-timeout` signal on result.timedOut, and
the handoff routes it like a pre-verdict crash: sentinel ts (feedback stays
live) and a retry, with a headline that names the real fix at the cap
(split the PR or raise the budget). A PR that PERSISTENTLY times out is
bounded by the round cap and the consecutive-failure cap, so this cannot
loop forever — it just stops treating a one-off budget blip as a verdict.

The loop guard stays terminal (a tool-call loop is a real defect, not a
budget blip). An API error still routes to its own model-key handoff; the
timeout signal is written only when NOT an API error.

Co-authored-by: wenshao <wenshao@example.com>

* feat(serve): add workspace-level generation (#7552)

* feat(serve): add workspace-level generation

* docs(serve): document workspace generation capability

* fix(serve): align workspace generation contracts

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>

* ci: matrix ECS runner update + sudo install + repository_dispatch trigger (#7513)

* ci: matrix ECS runner update with sudo install

- Use matrix strategy (ecs-update-sg, ecs-update-64c) to update both
  physical ECS hosts in parallel (fail-fast: false).
- Always use sudo npm install -g so the package lands in /usr/local
  (system-wide PATH) instead of the runner user's home directory.
- Move concurrency to job level (matrix context not available at
  workflow level per actionlint).
- Add repository_dispatch trigger for release-driven updates.
- Register new runner labels in actionlint.yaml.

* fix(ci): use dispatch version for runner update

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(web-shell): include managed id in artifact open requests (#7570)

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>

* feat(serve): persist workspace channel configuration (#7514)

* feat(serve): persist workspace channel configuration

* fix(serve): harden channel settings snapshots

* fix(serve): validate startup channel names

* fix(serve): reserve all channel name

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(sdk-python): require canonical form in validate_session_id (#7532)

uuid.UUID() accepts several non-canonical spellings — braced
{...}, urn:uuid:..., and dash-less hex — so validate_session_id let them
through after the RFC 4122 variant check. The value is then forwarded to
the CLI verbatim as --session-id/--resume, producing a malformed session
id downstream rather than a clear error at the SDK boundary.

Reject anything whose canonical form differs from the input. Case is
deliberately not part of the comparison: UUID() lowercases, and an
all-uppercase spelling is still valid canonical input.

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(web-shell): sync background agent status (#7561)

* fix(web-shell): sync background agent status

* fix(web-shell): harden background agent reconciliation

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>

* feat(core): propagate trusted daemon invocation context (#7279)

* feat(core): propagate trusted daemon invocation context

* test(cli): update ACP startup expectation

* refactor(core): centralize ACP capability env key

* test(cli): update worktree ACP core mock

* test(integration): run daemon context smoke on PRs

* test(ci): update no-AK smoke expectation

* test(core): cover invocation context isolation

* fix(cli): compare ACP capability safely

* fix(docs): restore GitHub action input names

* fix(core): sanitize private ACP capability from child env

* fix(core): reuse private ACP capability env constant

* test(cli): cover malformed trusted invocation context

* test(acp-bridge): assert exact child environment

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: 易良 <1204183885@qq.com>

* fix(feishu): await stream cancels in media download teardown (#7465)

* fix(feishu): await stream cancels in media download teardown

downloadMedia left two reject paths' stream teardown unawaited:

- the oversize-stream path called reader.cancel() without awaiting, so a
  cancel error during teardown became an unhandled rejection (fatal under
  Node's default --unhandled-rejections=throw);
- the Content-Length reject path returned without cancelling resp.body,
  leaving the connection pinned until GC.

Both were already fixed for the sibling DingTalk downloader in #7361 (which
was itself modelled on this Feishu code), so this brings Feishu to parity.
Adds a regression test that pins the reader.cancel() await via a rejecting
cancel, plus an assertion that the Content-Length path releases the body.

* test(feishu): cover a rejecting body.cancel() on the Content-Length path

Mirrors the existing reader.cancel() teardown test for the other reject
path, per review feedback. Removing the await on resp.body?.cancel()
flips execution onto the 'rejected: size ... exceeds' branch and the
test fails.

* fix(autofix): make the review-address report wrapper lines bilingual (#7569)

The agent's address-summary.md / no-action.md already ends with a
collapsed Chinese translation, but the workflow-appended wrapper lines
around it — the "Addressed/Reviewed the latest feedback" lead-in, the
"Base-conflict check" line, and the "Re-review when you have a moment"
footer — were English-only and sat outside that block. So the posted
comment was only half translated, unlike the takeover-ack comments
(full collapsed Chinese block) and the "model/模型" sign-off in this
same report (already inline-bilingual).

Give each wrapper line an inline Chinese translation, matching the
model/模型 idiom. The English halves are preserved verbatim — the
streak-reset detector globs on "Addressed the latest review feedback"
and "no changes needed", and a test extracts these lines — so behaviour
is unchanged and old English-only comments still match. A new test pins
each English-Chinese pair so a future reword that drops the Chinese
fails. The terminal handoff/failure comment is left English-only for
now (SKILL.md keeps it so by design); that is a separate change.

Co-authored-by: wenshao <wenshao@example.com>

* feat(cli): post the review body bilingually when the PR description is Chinese (#7564)

When the PR author writes Chinese, the posted /review body was
English-only. fetch-pr now records whether the PR description contains
Han characters (prDescriptionHasHan, detected from the same gh pr view
call and stamped into the plan report), and compose-review renders the
body bilingually off that flag: the English body leads, the complete
Chinese version rides collapsed in a <details><summary>中文说明</summary>
block, and the model footer stays outside the fold. The signal is the
CLI's own — the caller cannot toggle the register of a certified body —
and a local plan has no field, so nothing changes for terminal-only
reviews.

Every deterministic body fragment carries an en/zh pair end to end:
compose-review's clause templates and describeChunkGap phrases, the
coverage disclosures (reasons, publicLabel role subjects via a new
publicLabelZh, the path-free unread-brief reason) and the Step 4/5 gap
texts including the combined same-shape sentence. Fragments with no
deterministic translation — model-written findings, caller echoes,
interpolated errors — ride verbatim in both halves. verificationGaps now
returns structural {subject, reason, subjectZh, reasonZh} entries, which
also removes compose-review's last recover-the-boundary-from-prose parse.

SKILL.md instructs the same format for the model-authored inline
comments: English finding first (marker and suggestion block stay in the
English half — tooling filters on them), full Chinese translation
collapsed beneath, footer last.

Co-authored-by: verify <verify@local>

* feat(autofix): auto-rerun a check that died on infrastructure, once (#7562)

* feat(autofix): auto-rerun a check that died on infrastructure, once

A failed check can be red because the machine died, not the code — a
self-hosted runner losing the server, the disk filling. #7490's E2E
failed with "runner lost communication with the server" and went green
on a rerun. The scan now reruns such a check's failed jobs automatically.

Detection is a conservative annotation whitelist (INFRA_FAILURE_SIGNATURES)
— only unambiguous machine failures, never a test-level timeout, which
could be a real regression. The one-shot guard is run_attempt, not a
marker: a run already retried to attempt 2 and still infra-failing is
persistent, so it is left for a human; after a rerun the attempt
increments, so the next scan will not rerun it. Every step is fail-safe
(any API error → no rerun), it runs only when the PR actually has a
failed check, and the gate carries the same review-address carve-out as
the other check selectors so the loop never reruns its own runs.

This is the transient-infra sibling of #7554 (stale-base): that merges
current main when a check is base-inherited; this reruns when a check
died on the runner. Neither touches a check that is a genuine failure.

Note: rerun-failed-jobs needs the PAT to hold `actions: write`.

* fix(autofix): use POSIX ERE groups in infra-failure regex, cover all signatures in tests (#7562)

* fix(autofix): also treat a git fetch/clone transport death as infra

#6506's checkout died mid-transfer — "fetch-pack: invalid index-pack
output" and "RPC failed; curl 92 ... CANCEL" — which then hung the job
into the 20m limit. That is infra, not the PR (it only touches a doc),
and a re-run made it green. But the infra-signature whitelist did not
cover it, so the auto-rerun did not fire and it waited on a human.

Add `invalid index-pack output` and `RPC failed` — the two canonical
git-transport-death phrases — to INFRA_FAILURE_SIGNATURES. A co-present
job-timeout line does not block the match (one matching line classifies
the run), and a BARE timeout with no transport signature is still left
alone, since it can be a real regression. Both new signatures are pinned
in the test's per-signature loop, plus a case on #6506's real composite
annotation and a bare-timeout-is-not-rerun guard.

* fix(autofix): paginate annotations and filter Autofix runs in infra-rerun loop (#7562)

---------

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

* fix(serve): detect stale SSE cursors across daemon restarts via epoch token; preserve turn attribution and surface compaction failures in replay (#7458)

* fix(daemon): epoch-token restart detection, compaction attribution, and degraded-snapshot signaling (DAEMON-001/007/008)

* fix(acp-bridge): field-level turn attribution merge and replayDegraded bridge test (#7458)

* fix(serve): skip bus epoch lookup for virtual subagent SSE streams (#7458)

The REST SSE route looked up the bus epoch for every session id, but
virtual subagent sessions ride their own bus and their compound ids are
not in the bridge's byId map, so the lookup threw and aborted the
subscription — breaking subagent event streams. Skip the lookup for the
virtual path and degrade a torn-down real session to a headerless stream
(mirrors the /acp route). Also bumps the daemon browser SDK bundle budget
(167KB -> 168KB) for the epoch fields and declares eventEpoch on
DaemonSession so the create/attach path drops its inline type cast.

* fix(serve): stamp eventEpoch on accepted continuations and surface replayDegraded in the SDK (#7458)

Address three review suggestions:
- POST /session/:id/continue now returns eventEpoch alongside lastEventId,
  mirroring the prompt 202 envelope so continuation-seeded SSE cursors
  detect daemon restarts (DAEMON-001)
- DaemonSessionClient exposes replayDegraded from the load response so SDK
  consumers can prefer the full transcript over a degraded snapshot
- add /acp dispatch-level regression test for the degraded-snapshot stderr
  breadcrumb (fires only when snapshot.degraded is set)

* test(cli): fix load-reply race in the degraded-breadcrumb transport test

Await each session/load reply frame before opening the session stream so
the GET cannot race conn.ownSession() into a 403; addresses the review
Critical on the deg-0 arm.

* fix(serve): allow and expose X-Qwen-Event-Epoch in CORS headers

Cross-origin SSE clients must send the epoch header through preflight and
read it from the response, or stale-cursor detection (DAEMON-001) is
silently disabled for every CORS client.

---------

Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Autofix <qwen-autofix[bot]@users.noreply.github.com>

* feat(core): Align GenAI telemetry with ARMS (#7536)

* feat(core): align GenAI telemetry with ARMS

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

* fix(core): remove estimated token usage splits

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

* fix(core): address GenAI telemetry review feedback

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(serve): avoid TOCTOU race dropping live sessions from list response (#7556)

* Initial plan

* fix(serve): avoid TOCTOU race dropping live sessions from list response

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 易良 <1204183885@qq.com>

* fix(cli): prevent monitor turns after task_stop (#7573)

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: destire-mio <qppque@gmail.com>
Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
Co-authored-by: Dragon <52599892+DragonnZhang@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: jinye <djy1989418@126.com>
Co-authored-by: chinesepowered <nlai@rediffmail.com>
Co-authored-by: ovochouovo <18212194+ovochouovo@users.noreply.github.com>
Co-authored-by: Edenman <67549719+BZ-D@users.noreply.github.com>
Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: ytahdn <1294726970@qq.com>
Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: Truraly <94105924+Truraly@users.noreply.github.com>
Co-authored-by: zjgzx1988 <zjgzx1988@hotmail.com>
Co-authored-by: hogeheer499-commits <hogeheer499@gmail.com>
Co-authored-by: hogeheer <267467744+hogeheer499-commits@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Nothing Chan <chenliu.cl@alibaba-inc.com>
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: yuanyuanAli <135116774+yuanyuanAli@users.noreply.github.com>
Co-authored-by: verify <verify@local>
Co-authored-by: qqqys <qys177@gmail.com>
Co-authored-by: callmeYe <512217680@qq.com>
Co-authored-by: Qwen Autofix <qwen-autofix[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
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.

4 participants