perf(cli): defer startup prefetch tasks#6303
Conversation
…up prefetch Address three test coverage gaps identified during code review: - Assert mockStartEarlyStartupPrefetches in both kitty protocol tests (C1: API preconnect call was wired but never verified) - Add Zed/ACP integration test verifying deferIdeConnection is false when getExperimentalZedIntegration returns true (C2: Zed path was entirely untested) - Assert mockStartBackgroundHousekeeping in startup-prefetch test (C3: unconditional housekeeping dispatch was never verified)
|
Thanks for the PR! Template looks good ✓ — all required headings present with bilingual description. Problem: The interactive startup path blocks on telemetry SDK init, update checks, and IDE connection before first paint. This is observable — running Direction: Aligned. Startup performance directly affects user experience for a CLI tool. Deferring non-critical work past first render is a well-established pattern. CHANGELOG doesn't have a direct reference to this specific optimization, but the area is relevant — #3219 (startup profiler) and #3221 (lazy tool registration) already landed as part of the same initiative. Approach: The scope feels right. The new One question: the Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ — 所有必要标题齐全,包含双语说明。 问题:交互式启动路径在首次渲染前阻塞了遥测 SDK 初始化、更新检查和 IDE 连接。这是可观测的——交互式运行 方向:对齐。启动性能直接影响 CLI 工具的用户体验。将非关键工作延迟到首次渲染后是成熟的优化模式。CHANGELOG 没有直接引用此优化,但相关领域已有基础——#3219(启动性能分析器)和 #3221(延迟工具注册)是同一优化计划的一部分。 方案:范围合理。新的 一个问题: 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewThe implementation is clean and well-structured. A few observations:
No correctness bugs, security holes, or regressions found. No AGENTS.md violations — the new module follows the project's conventions (collocated tests, ESM, kebab-case naming, The new Test ResultsAll 66 unit tests pass: Typecheck passes for both Build + bundle succeed. Real-Scenario TestingNon-interactive mode (
|
|
Stepping back to look at the whole picture: this PR does exactly what a startup performance optimization should — it moves the right things off the critical path while carefully preserving correctness on every non-TUI codepath. The implementation is straightforward and doesn't try to do more than needed. A single new module ( What I appreciate: the author clearly mapped every codepath (plain TUI, prompt-interactive, The design doc is thorough (perhaps more thorough than needed for 112 lines of code), but it documents the architectural decisions and tradeoffs well — useful for future maintainers. Real-scenario testing confirms non-interactive mode works identically before and after, and interactive REPL renders within 4 seconds. The Ink alternate screen buffer makes clean My independent proposal would have been essentially the same approach — consolidate fire-and-forget operations, add deferred options. The PR's implementation exceeds that with the profiler event integration and idempotency guards. Approving. ✅ 中文说明回顾全局:这个 PR 完美地完成了启动性能优化应该做的事——将正确的操作移出关键路径,同时仔细保留了每条非 TUI 代码路径的正确性。 实现直截了当,没有过度设计。一个 112 行的新模块,两个入口点,一个 值得赞赏的是:作者清晰地映射了每条代码路径(普通 TUI、prompt-interactive、 真实场景测试确认非交互模式前后行为一致,交互式 REPL 在 4 秒内渲染完成。 批准。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
| debugLogger.warn( | ||
| `${name} failed: ${err instanceof Error ? err.message : String(err)}`, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
[Suggestion] runDeferredTask interpolates err.message into a string before passing it to debugLogger.warn, which drops the stack trace. The debugLogger's formatArgs already handles Error objects specially (extracting arg.stack), but it never sees the original error here.
| }); | |
| debugLogger.warn(`${name} failed:`, err); |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed. runDeferredTask now passes the original error object to the logger:
debugLogger.warn(`${name} failed:`, err);This preserves stack traces through formatArgs, and the startup prefetch tests assert the logger receives the original Error object.
| preconnectApi(authType, { resolvedBaseUrl, proxy }); | ||
| recordStartupEvent('startup_prefetch_started', { name: 'api_preconnect' }); | ||
| } catch (error) { | ||
| debugLogger.debug( |
There was a problem hiding this comment.
[Suggestion] api_preconnect records startup_prefetch_started but never emits the corresponding startup_prefetch_completed or startup_prefetch_failed event. All post-render tasks go through runDeferredTask which records all three lifecycle events. This asymmetry means profiling consumers see api_preconnect as perpetually in-flight.
Consider either wrapping the call in runDeferredTask for consistency, or removing the manual recordStartupEvent here so only runDeferredTask-managed tasks appear in the startup_prefetch_* event stream.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed. The manual startup_prefetch_started event for api_preconnect has been removed, so only tasks managed by runDeferredTask enter the balanced startup_prefetch_* lifecycle stream.
There is also a regression test asserting that api_preconnect does not emit an unbalanced lifecycle event.
| }); | ||
| } | ||
|
|
||
| runDeferredTask('background_housekeeping', async () => { |
There was a problem hiding this comment.
[Suggestion] The original code in gemini.tsx guarded housekeeping with if (config.isInteractive()) and a comment explaining that non-interactive modes don't create the file-history dirs this cleans. That guard was dropped in the migration.
This is safe today since startPostRenderPrefetches is only called from startInteractiveUI, but the function's signature accepts any Config — a future non-interactive caller would silently start housekeeping. Consider restoring the guard for defense-in-depth, or adding a comment noting the implicit invariant.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed. background_housekeeping is now guarded by config.isInteractive() inside startPostRenderPrefetches(), preserving the previous invariant even if a future non-interactive caller invokes this helper directly.
The startup prefetch tests cover the non-interactive case and assert housekeeping is not scheduled.
doudouOUC
left a comment
There was a problem hiding this comment.
Startup deferral is well-structured with good idempotency guards. A few findings target files outside this PR's diff (WebShellSidebar, autofix workflow, errorReporting, acpAgent) — flagging them here since inline comments aren't possible on those files:
- WebShellSidebar (
WebShellSidebar.tsx):handleArchive/handleUnarchiveonly reload the target list, not the source list — sessions appear in both active and archived sections simultaneously.confirmDeleteSessionis missing thebusySessionIdRefguard that archive/unarchive both have, allowing concurrent operations. - Autofix workflow (
qwen-autofix.yml): The claim step removesautofix/approvedbefore posting the claim comment. If the comment fails, the issue becomes invisible to all future automated scans (stuck state). - Test gaps: Archive/unarchive error paths, archived session deletion,
actions.tsarchive/unarchive, andloadSession/resumeSessionconcurrent isolation are all untested. - acpAgent:
newSessionstill assignsthis.settingsbefore the firstawait, inconsistent withloadSession/resumeSession.
For files in the PR diff:
startup-prefetch.ts:96-99— IDE connection failure in deferred mode is onlydebugLogger.warn. The symptom (IDE features don't work) and cause (deferred connection swallowed) are far apart for debugging.config.ts:2014—deferTelemetryInitializationdefers forqwen -i "prompt"even though the first model request fires before post-render prefetches run. Consider excluding prompt-interactive from deferral, or add a comment explaining the accepted trade-off.
— qwen3.7-max via Qwen Code /review
Tighten the startup prefetch follow-up fixes from review while keeping prompt-interactive telemetry on the fast interactive startup path. - Preserve Error objects when deferred startup tasks fail - Remove the unbalanced api_preconnect profiler lifecycle event - Guard background housekeeping so it only runs for interactive configs - Document and test prompt-interactive telemetry deferral semantics
Ensure sessions launched with an initial interactive prompt have telemetry ready before the auto-submitted first request runs. - Exclude prompt-interactive startup from telemetry deferral - Pass a post-render telemetry option through interactive UI startup - Skip duplicate post-render telemetry startup for initial prompts - Update tests to cover the first-prompt telemetry guarantee Note: Plain interactive TUI startup still defers telemetry post-render.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
The deferred-startup design is well-structured — WeakSet idempotency guards are correct, all five startup paths (interactive, prompt-interactive, non-interactive, stream-json, ACP/Zed) maintain their correctness guarantees, and the new test suite covers the critical paths. A few low-confidence defensive-programming suggestions were identified but not flagged as inline comments (default-direction mismatch in startInteractiveUI options, duplicated telemetry defer condition across modules, and fragile ide_connect/telemetry_init ordering in startPostRenderPrefetches).
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
| } | ||
|
|
||
| if (options.initializeTelemetry) { | ||
| runDeferredTask('telemetry_init', () => { |
There was a problem hiding this comment.
[Suggestion] telemetry_init is the only synchronous task among the deferred tasks. initializeTelemetry(config) runs as a single microtask inside Promise.resolve().then(task), blocking the event loop until OTel SDK setup completes (resource attributes, exporters, BatchSpanProcessor, PeriodicExportingMetricReader). Every other deferred task starts with await import(...) which yields to the event loop before doing real work.
This has two consequences:
- Performance: The sync microtask can delay React's post-mount
useEffectstate updates and pending I/O, partially undermining the "fast time-to-interactive" goal. - Ordering dependency: Because
telemetry_initis declared beforeide_connectand runs synchronously, it completes beforeconnectIdeForStartup→logIdeConnectionexecutes.logIdeConnectionguards onisTelemetrySdkInitialized()and silently drops the event if false. This ordering is accidental — ifinitializeTelemetryis ever made async or the task declarations are reordered, IDE connection telemetry events will be silently dropped with no test catching it.
| runDeferredTask('telemetry_init', () => { | |
| if (options.initializeTelemetry) { | |
| runDeferredTask('telemetry_init', async () => { | |
| const { initializeTelemetry: initTelemetry } = await import( | |
| '@qwen-code/qwen-code-core' | |
| ); | |
| initTelemetry(config); | |
| }); | |
| } |
Making this async with a dynamic import (consistent with the other tasks) yields to the event loop before running, and also removes the static initializeTelemetry import at the top of the file. To preserve the ordering guarantee with ide_connect, either sequence them explicitly or add a comment documenting the dependency.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Not addressed in this commit.
I kept telemetry_init synchronous for now because changing it to an async dynamic import also changes the implicit ordering with deferred ide_connect; logIdeConnection currently depends on telemetry already being initialized. Fixing this properly should sequence telemetry and IDE connection explicitly, or introduce a shared readiness contract, instead of only adding an async import and risking dropped IDE telemetry events.
This should be handled as a separate follow-up so the scheduling and telemetry-ordering behavior can be reviewed together.
| } | ||
|
|
||
| if (options.connectIde && config.getIdeMode()) { | ||
| runDeferredTask('ide_connect', async () => { |
There was a problem hiding this comment.
[Suggestion] The deferred IDE connection runs fire-and-forget via runDeferredTask with no timeout. If the IDE IPC endpoint is unresponsive (VS Code extension host restarted, stale socket file, JetBrains backend overloaded), the connect() promise hangs indefinitely inside runDeferredTask. The catch handler only fires on rejection, not on hang. The result: startup_prefetch_started with name: ide_connect is recorded but neither completed nor failed ever fires, and there is no diagnostic trail explaining why IDE context is missing.
This is a new failure mode introduced by the PR. The old code awaited connect() inside initializeApp() before first paint — a hang was immediately visible as "the TUI never appears." The new code makes the same hang invisible.
Consider wrapping the connection in a timeout (e.g., Promise.race with a 10s setTimeout that rejects), so the profiler captures the outcome and debugLogger.warn fires on timeout.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed in 6cba2c578.
Deferred ide_connect is now wrapped in a 10s timeout. If the IDE IPC promise hangs, the timeout rejects through runDeferredTask, so the profiler records startup_prefetch_failed and the debug logger emits a clear timeout diagnostic instead of leaving the task permanently in-flight.
A unit test covers the hanging connection case.
| initializationResult!, | ||
| { | ||
| postRenderConnectIde: deferIdeConnection, | ||
| postRenderInitializeTelemetry: !input, |
There was a problem hiding this comment.
[Suggestion] The telemetry deferral decision is split across two files with no type-level or comment-level link:
cli/config/config.tsdecides whether to skip eager init:deferTelemetryInitialization: interactive && !isAcpMode && !question- This line decides whether to run post-render init:
postRenderInitializeTelemetry: !input
The invariant deferTelemetryInitialization === postRenderInitializeTelemetry must hold for the interactive TUI path, or telemetry either never initializes (silent data loss) or initializes twice (masked today by the telemetryInitialized guard in sdk.ts, but that guard is an implementation detail that could change).
Today the conditions are equivalent (interactive && !isAcpMode && !question simplifies to !question on the interactive path, matching !input). But this relies on three unstated premises: ACP mode never reaches this branch, question and input are the same value, and no future condition is added to either side independently.
Consider adding a linking comment at both sites, or computing the deferral flag once and passing it through to both loadCliConfig and startInteractiveUI.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed in 6cba2c578 with the Config-backed option.
Config now stores the telemetry deferral decision as telemetryInitializationDeferred, and startInteractiveUI() defaults post-render telemetry startup from config.isTelemetryInitializationDeferred() instead of recomputing a separate !input condition in gemini.tsx.
This keeps the eager-skip and post-render-init decisions tied to the same source of truth.
| expect(config.getTelemetryEnabled()).toBe(true); | ||
| }); | ||
|
|
||
| it('should initialize telemetry before prompt-interactive startup', async () => { |
There was a problem hiding this comment.
[Suggestion] This test verifies deferTelemetryInitialization: false for prompt-interactive, but there is no corresponding positive test verifying deferTelemetryInitialization: true for the ordinary interactive case (no -p, no -i "prompt", not ACP). The positive path is the primary motivation for the feature — deferring telemetry for ordinary TUI startup — yet it has no test. A regression that accidentally always sets false would go undetected.
Consider adding a test with process.argv = ['node', 'script.js', '-i'] (interactive, no question) that asserts deferTelemetryInitialization: true.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed in 6cba2c578.
Added a positive loadCliConfig test for ordinary interactive startup that sets process.stdin.isTTY = true, uses no initial prompt, and asserts deferTelemetryInitialization: true is passed to Config.
| const { initializeApp } = await import('./core/initializer.js'); | ||
| const initializationResult = await initializeApp(config, settings); | ||
| let input = config.getQuestion(); | ||
| const deferIdeConnection = |
There was a problem hiding this comment.
[Critical] deferIdeConnection only treats a CLI question as an immediate first request, but interactive sessions with --input-file can submit as soon as RemoteInputWatcher.setSubmitFn() is installed. Since IDE connection is now fire-and-forget post-render, an IDE-enabled driver can send that first request before IDE context is connected, regressing the same first-request guarantee this PR preserves for prompt-interactive startup.
| const deferIdeConnection = | |
| const hasImmediateInteractiveSubmit = Boolean( | |
| input || config.getInputFile?.(), | |
| ); | |
| const deferIdeConnection = | |
| config.isInteractive() && | |
| !config.getExperimentalZedIntegration() && | |
| !hasImmediateInteractiveSubmit; |
— gpt-5 via Qwen Code /review
There was a problem hiding this comment.
Addressed in 6cba2c578.
deferIdeConnection now treats config.getInputFile?.() as an immediate interactive-submit source, so interactive sessions using --input-file keep the IDE connection awaited before the first remote prompt can run. The post-render prefetch receives connectIde: false in that case to avoid a duplicate connection attempt.
Added a gemini.test.tsx regression test for interactive + input file.
# Conflicts: # packages/cli/src/gemini.test.tsx
| experimentalLsp: undefined, | ||
| channel: undefined, | ||
| chatRecording: undefined, | ||
| sessionId: undefined, |
There was a problem hiding this comment.
[Critical] [build] fallbackModel is missing from this mock CliArgs object. The same omission exists at line 1511. CliArgs.fallbackModel is required (no ?), so tsc --build reports TS2345 on both lines. Line 1267 already includes fallbackModel: undefined — these two were missed.
| sessionId: undefined, | |
| sessionId: undefined, | |
| fallbackModel: undefined, |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed in 6cba2c578.
Both affected mock CliArgs objects now include fallbackModel: undefined, and packages/cli typecheck passes with tsc --noEmit.
|
|
||
| await vi.dynamicImportSettled(); | ||
|
|
||
| expect(mockCheckForUpdates).toHaveBeenCalledTimes(1); |
There was a problem hiding this comment.
[Suggestion] handleAutoUpdate is mocked (line 20) and wired into the handleAutoUpdate module mock (line 46), but no test asserts it was actually called. This test verifies checkForUpdates was called but skips the second half of the update pipeline — a regression that drops handleAutoUpdate from the .then() chain would pass all tests.
| expect(mockCheckForUpdates).toHaveBeenCalledTimes(1); | |
| expect(mockCheckForUpdates).toHaveBeenCalledTimes(1); | |
| expect(mockHandleAutoUpdate).toHaveBeenCalledWith( | |
| null, | |
| expect.any(Object), | |
| '/repo', | |
| ); |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Not addressed in this commit.
I did not add this assertion because the current test intentionally uses a never-resolving checkForUpdates() promise to verify that startPostRenderPrefetches() starts work without awaiting completion. In that specific test, handleAutoUpdate should not run.
A separate test that resolves checkForUpdates() and asserts the handleAutoUpdate(info, settings, projectRoot) handoff would be useful, but it is a test-strengthening follow-up rather than part of the first-request correctness fixes in 6cba2c578.
Keep deferred startup work from weakening first-request behavior in interactive sessions that submit prompts automatically or remotely. - Store telemetry deferral on Config and reuse that decision at render time - Keep IDE startup awaited for prompt-interactive and input-file sessions - Add a timeout for deferred IDE connection failures - Cover ordinary interactive telemetry deferral and IDE startup edge cases
|
Thanks for the detailed review. Status after Fixed in this PR:
Not fixed here:
|
| }); | ||
| } | ||
| startPostRenderPrefetches(config, settings, { | ||
| connectIde: options.postRenderConnectIde ?? true, |
There was a problem hiding this comment.
[Suggestion] The default options.postRenderConnectIde ?? true makes post-render IDE connection opt-out. Combined with initializeApp()'s default deferIdeConnection: false (eager), any future caller of startInteractiveUI that omits options will get a double IDE connection: once awaited in initializeApp, once fire-and-forget in startPostRenderPrefetches. IdeClient.connect() has no re-entry guard.
The sole production caller in gemini.tsx passes the option correctly, masking this footgun.
| connectIde: options.postRenderConnectIde ?? true, | |
| connectIde: options.postRenderConnectIde ?? false, |
Changing the default to false makes the safe/already-connected state the default, matching initializeApp's eager-connect behavior. The gemini.tsx caller already passes postRenderConnectIde: deferIdeConnection explicitly, so no caller change is needed.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed in d05359056.
startInteractiveUI() now defaults postRenderConnectIde to false, so the already-connected/eager IDE path is the safe default and future callers that omit options will not accidentally connect twice. The production caller in gemini.tsx still passes postRenderConnectIde: deferIdeConnection explicitly, so ordinary TUI startup keeps the deferred IDE behavior when intended.
Updated startInteractiveUI tests to assert the safer default. Verified with:
cd packages/cli && npx vitest run src/gemini.test.tsx
cd packages/cli && npm run typecheck
git diff --checkDefault startInteractiveUI to the already-connected IDE path so future callers do not accidentally connect twice when initializeApp used its eager default. - Change the post-render IDE connection default to false - Update startInteractiveUI tests to assert the safer default
|
@wenshao @doudouOUC All the comments have been addressed. Please take another look. Thanks. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
The startup deferral architecture is well-structured: WeakSet idempotency guards are correct, the runDeferredTask abstraction cleanly separates fire-and-forget work from the critical path, and all five startup flows (interactive, prompt-interactive, non-interactive, ACP/Zed, input-file) are properly handled with comprehensive test coverage (77/77 passing). Typecheck and ESLint clean.
— qwen3.7-max via Qwen Code /review
… (#3222)
What this PR does
This PR moves interactive telemetry SDK startup off the pre-render REPL critical path and starts it after the first Ink render alongside the
existing post-render startup prefetch work. It also keeps IDE startup awaited for prompt-interactive sessions that auto-submit an initial
prompt, while avoiding a duplicate post-render IDE connection in that path.
Why it's needed
Interactive startup should render the REPL before optional background work such as telemetry initialization, update checks, IDE connection for
ordinary TUI sessions, and housekeeping completes. At the same time, prompt-interactive and non-interactive flows must keep their first-
request correctness guarantees, especially IDE context and MCP tool availability.
Reviewer Test Plan
How to verify
Confirm that ordinary interactive startup defers telemetry initialization and IDE connection until after first render, while prompt-
interactive still awaits IDE context before the auto-submitted first request and does not connect IDE a second time after render. Confirm that
non-interactive behavior is unchanged and still waits for MCP readiness before the first model request.
Local verification commands run:
All commands passed locally.
Evidence (Before & After)
Before: Telemetry and checkForUpdates were loaded before rendering in interactive CLI startup.
After: Telemetry and checkForUpdates are no longer loaded before rendering in interactive CLI startup.
Tested on
Environment (optional)
Local npm workspace on macOS; verification used package-level TypeScript checks and targeted Vitest suites.
Risk & Scope
Main risk or tradeoff: Interactive telemetry events emitted before the post-render telemetry startup can be no-ops because this change
intentionally does not add buffering.
Not validated / out of scope: Manual TUI timing capture, Windows-specific behavior, Linux-specific behavior, and full integration suites
were not run locally.
Breaking changes / migration notes: None expected. Non-interactive, stream-json, ACP/Zed, and MCP first-request readiness semantics are
preserved.
Linked Issues
Related to #3011 and #3222.
中文说明
What this PR does
本 PR 将交互式遥测 SDK 的启动从预渲染 REPL 关键路径中移出,改为在首次 Ink 渲染后与现有的渲染后启动预取工作一同启动。同时,对于自动提交初始提示词的 prompt-interactive 会话,保持 IDE 启动的 await 行为,并避免在该路径中重复进行渲染后的 IDE 连接。
Why it's needed
交互式启动应在渲染 REPL 之后再执行可选的后台工作,如遥测初始化、更新检查、普通 TUI 会话的 IDE 连接以及内务处理。与此同时,prompt-interactive 和非交互式流程必须保持其首次请求的正确性保证,尤其是 IDE 上下文和 MCP 工具可用性。
Reviewer Test Plan
How to verify
确认普通交互式启动将遥测初始化和 IDE 连接延迟到首次渲染之后,而 prompt-interactive 在自动提交的首次请求前仍会 await IDE 上下文,且不会在渲染后再次连接 IDE。确认非交互式行为保持不变,仍在首次模型请求前等待 MCP 就绪。
本地验证命令运行通过:
所有命令在本地均通过。
Evidence (Before & After)
修改前:对于普通的交互式的命令行启动方式,会在 Render 前加载 telemetry, 并执行 checkForUpdates
修改前:对于普通的交互式的命令行启动方式,不会在 Render 前加载 telemetry 和 checkForUpdates
Tested on
Environment (optional)
macOS 上的本地 npm workspace;验证使用了包级别的 TypeScript 检查和针对性的 Vitest 套件。
Risk & Scope
-主要风险或权衡:在渲染后遥测启动之前发出的交互式遥测事件可能为 no-op,因为本次变更有意不添加缓冲机制。
未验证 / 超出范围:手动 TUI 时序捕获、Windows 特定行为、Linux 特定行为以及完整集成套件未在本地运行。
破坏性变更 / 迁移说明:无预期变更。非交互式、stream-json、ACP/Zed 以及 MCP 首次请求就绪语义保持不变。
Linked Issues
关联 #3011 和 #3222。