fix(cli): establish extension store generation baseline on first read#7072
Conversation
|
Thanks for the PR! (Re-run — previous scope concern no longer applies after the rebase.) Template looks good ✓ — all required headings present, bilingual. Problem: Observed bug with clear reproduction in #7029 — fresh install fires a spurious "Extensions changed on disk" notice because Direction: Treating the first observed generation as baseline establishment is the right fix. No CHANGELOG reference needed for a cosmetic P3 fix. ✓ Size: 9 production lines in Approach: Minimal, focused change — the diff now carries exactly the fix described in the PR body (2 files). Previous scope concerns about unrelated changes have been resolved. The fix is exactly what you'd write from scratch: early-return when Moving on to code review. 🔍 中文说明感谢贡献!(重新运行 — 之前关于范围的问题在 rebase 后已不再适用。) 模板完整 ✓ — 所有必需的标题齐全,双语。 问题: #7029 中有明确复现的已观测 bug — 全新安装时触发虚假的"Extensions changed on disk"通知,因为 方向: 将首次读取到的 generation 视为基线建立是正确的修复方式。对于 P3 级别的外观修复无需 CHANGELOG 引用。✓ 规模: 方案: 最小化、聚焦的变更 — diff 现在只包含 PR 描述中所述的修复(2 个文件)。之前关于无关变更的顾虑已解决。修复方式正是从零开始会写的方式:在现有比较逻辑之前,当 进入代码审查 🔍 — Qwen Code · qwen3.7-max Reviewed at |
|
Qwen Code review did not complete successfully. Qwen review aborted with an API error before posting comments. See workflow logs. |
Code ReviewIndependent proposal: Given the problem (fresh install: Diff comparison: The implementation matches the independent proposal precisely. The 9-line guard is inserted at the correct location — before the Downstream consumer analysis (Tier 2 — 100% confidence required):
No downstream consumer reads Reuse check: No existing utility covers this pattern. The guard is inline and minimal — no new abstractions. No critical issues. No AGENTS.md violations. Real-Scenario TestingLocal 1. CI (ubuntu, 33 min): all test suites green. ✓ 2. Collaborator E2E by @wenshao (macOS, real bundled CLI, fresh isolated The E2E established:
Scope note (out of scope for this PR, from @wenshao): #7029's visible symptom has two additional trigger paths on truly virgin 中文说明代码审查独立方案: 针对问题(全新安装: Diff 对比: 实现与独立方案完全一致。9 行守卫插入在正确位置 — 在 下游消费者分析(Tier 2 — 要求 100% 置信度):
没有下游消费者在 复用检查: 没有现有工具覆盖此模式。守卫是内联且最小的 — 无新抽象。 无关键问题。无 AGENTS.md 违规。 真实场景测试本环境中 1. CI(ubuntu,33 分钟): 所有测试套件绿色 ✓ 2. 协作者 E2E by @wenshao(macOS,真实 bundle CLI,全新隔离
范围说明(本 PR 之外,来自 @wenshao):#7029 的可见症状在完全原始 — Qwen Code · qwen3.7-max Reviewed at |
|
Confidence: 5/5 — Clean across every stage; a textbook bug fix. This is exactly what a fix PR should look like. The bug is real (issue #7029, deterministic 3/3 reproduction on main), the root cause analysis is correct ( My independent proposal matched the PR's approach precisely. All downstream consumers of Independent E2E verification by @wenshao on macOS with real bundled CLI confirms: bug deterministic on main (3/3), fix works (3/3 silent), no regression on existing installs, tests pin the behavior (reverting the guard flips exactly 2 tests). CI also green. The previous Two minor notes for awareness (non-blocking):
Approving. ✅ 中文说明置信度:5/5 — 每个阶段都干净;教科书级的 bug 修复。 这正是 fix PR 应该有的样子。bug 是真实的(issue #7029,在 main 上 3/3 确定性复现),根因分析正确( 我的独立方案与 PR 方案完全一致。 @wenshao 在 macOS 上用真实 bundle CLI 的独立 E2E 验证确认:bug 在 main 上确定性复现(3/3),修复有效(3/3 静默),既有安装无回归,测试钉住行为(回退守卫恰好翻红 2 个测试)。CI 也绿色。 之前的 两个非阻塞的知会说明:
批准合并。✅ — Qwen Code · qwen3.7-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
🤖 Could not address the latest feedback automatically (round 1/5). A human should take over this PR. What I found before stopping: No Action Required — PR #7072Feedback Classification1. Test (ubuntu-latest Node 22.x): FAILURE — Pre-existing on mainThe 5 failing tests in
All 5 relate to sandbox image resolution — unrelated to this PR's changes The PR's own tests all pass:
2. Qwen Code review aborted with API error — No code commentsThe automated review step failed with an API error before posting any inline 3. Post Coverage CommentRun log: https://github.com/QwenLM/qwen-code/actions/runs/29552946622 |
|
🤖 Could not address the latest feedback automatically (round 1/5). A human should take over this PR. What I found before stopping: No Action Needed — PR #7072Feedback TriageNo human review comments were found on the PR. The only feedback is:
Independent VerificationAll required checks pass on the PR code:
The 5 CLI ConclusionNo code changes are needed. The PR is sound — build, types, lint, and all Run log: https://github.com/QwenLM/qwen-code/actions/runs/29552964462 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: reverse audit — per-chunk auditors returned substantive receipts but harness did not detect brief-file reads.
Not reviewed: reverse audit — its prompt was built, but no agent was launched with it that opened its brief, so the reverse-audit pass did not run.
Not reviewed: verification — its prompt was built, but no agent was launched with it that opened its brief, so the posted findings were not verified.
— qwen3.7-max via Qwen Code /review
| // stranding the entry as paused. | ||
| try { | ||
| registry.assertCanStartBackgroundAgent(); | ||
| registry.assertCanStartBackgroundAgent(entry.model); |
There was a problem hiding this comment.
[Critical] Per-model concurrency cap bypassed for revived background agents — entry.model is always undefined in the revive path because the concrete model ID is never persisted to the AgentMeta sidecar file.
The AgentMeta interface (agent-transcript.ts) has no model field, and writeAgentMeta (agent.ts) never writes one. When loadPausedBackgroundAgents() restores registrations from sidecar files, every entry has model: undefined. This means assertCanStartBackgroundAgent(entry.model) resolves to assertCanStartBackgroundAgent(undefined), which calls resolvePerModelCap(undefined) → undefined, skipping the entire per-model cap check.
Failure scenario: User configures maxParallelAgentsByModel: { 'weak-model': 1 }. Three background agents on 'weak-model' are running, process exits. On restart, all three entries are loaded with model: undefined and revived one by one — each bypasses the per-model cap, so all three revive onto 'weak-model' simultaneously despite the cap of 1.
Suggested fix: Three coordinated changes:
- Add
model?: stringto theAgentMetainterface inagent-transcript.ts - Include
model: subagentModelIdin thewriteAgentMetacall inagent.ts(the background-agent launch path) - Read
meta.modeland include it in the registration constructed inloadPausedBackgroundAgents
| registry.assertCanStartBackgroundAgent(entry.model); | |
| registry.assertCanStartBackgroundAgent(entry.model); | |
| // TODO: persist model in AgentMeta so revive path enforces per-model caps |
| this.backgroundTaskRegistry = new BackgroundTaskRegistry({ | ||
| ...(this.agentsSettings.maxParallelAgents !== undefined | ||
| ? { | ||
| maxConcurrentBackgroundAgents: | ||
| this.agentsSettings.maxParallelAgents, | ||
| }, | ||
| ); | ||
| } | ||
| : {}), | ||
| ...(this.agentsSettings.maxParallelAgentsByModel !== undefined | ||
| ? { | ||
| maxConcurrentBackgroundAgentsByModel: | ||
| this.agentsSettings.maxParallelAgentsByModel, | ||
| } | ||
| : {}), | ||
| }); |
There was a problem hiding this comment.
[Suggestion] Unnecessary conditional spreading — the BackgroundTaskRegistry constructor already handles undefined for both properties: maxConcurrentBackgroundAgents via ?? MAX_CONCURRENT_BACKGROUND_AGENTS, and maxConcurrentBackgroundAgentsByModel via normalizePerModelConcurrency which returns an empty Map for undefined input.
Concrete cost: Every future reader must verify the constructor's undefined tolerance before modifying either side. A direct pass-through reduces this block to two lines.
| this.backgroundTaskRegistry = new BackgroundTaskRegistry({ | |
| ...(this.agentsSettings.maxParallelAgents !== undefined | |
| ? { | |
| maxConcurrentBackgroundAgents: | |
| this.agentsSettings.maxParallelAgents, | |
| }, | |
| ); | |
| } | |
| : {}), | |
| ...(this.agentsSettings.maxParallelAgentsByModel !== undefined | |
| ? { | |
| maxConcurrentBackgroundAgentsByModel: | |
| this.agentsSettings.maxParallelAgentsByModel, | |
| } | |
| : {}), | |
| }); | |
| this.backgroundTaskRegistry = new BackgroundTaskRegistry({ | |
| maxConcurrentBackgroundAgents: this.agentsSettings.maxParallelAgents, | |
| maxConcurrentBackgroundAgentsByModel: this.agentsSettings.maxParallelAgentsByModel, | |
| }); |
— qwen3.7-max via Qwen Code /review
| subagentModelId = resolveModelId( | ||
| subagentConfig.model, | ||
| buildModelIdContext(this.config), | ||
| )?.modelId; |
There was a problem hiding this comment.
[Suggestion] No test verifies that the resolved model ID reaches the background task registry in the agent tool's launch path. The mock registry's tryReserveBackgroundSlot and waitForBackgroundSlot are vi.fn() stubs that accept any arguments, so a regression that drops the model argument or passes the wrong value would go undetected.
Failure scenario: A future change to resolveModelId or buildModelIdContext alters the resolved model ID. The per-model concurrency cap is bypassed for all background agent launches from the agent tool, while background-tasks.test.ts continues to test the cap in isolation.
Suggested fix: Add a test in agent.test.ts that asserts mockRegistry.tryReserveBackgroundSlot receives the expected model ID when the subagent config specifies a model selector.
— qwen3.7-max via Qwen Code /review
|
🤖 Could not address the latest feedback automatically (round 2/5). A human should take over this PR. What I found before stopping: Address Review Summary — PR #7072Feedback Classification1. Critical: Per-model concurrency cap bypassed for revived background agentsClassification: Required (correctness bug) Decision: Fixed. Changes:
Why: Without these three coordinated changes, 2. Failed check: Test (ubuntu-latest Node 22.x)Classification: Required (broken CI) Decision: Fixed. Root cause: The PR added Run log: https://github.com/QwenLM/qwen-code/actions/runs/29556762709 |
|
🤖 Could not address the latest feedback automatically (round 2/5). A human should take over this PR. What I found before stopping: Address Summary — PR #7072Feedback points1. [Critical] Per-model concurrency cap bypassed for revived background agentsDecision: Required — correctness bug confirmed. Root cause: Changes:
Tests added:
Run log: https://github.com/QwenLM/qwen-code/actions/runs/29557646637 |
…branch tree (QwenLM#7076) * ci(autofix): run the schema gate from a trusted staged copy, not the branch tree Live-run failure on the first post-merge address run (PR QwenLM#7072): the verify gate invokes `bash .github/scripts/check-settings-schema.sh`, but by then the working tree is the PR BRANCH (switched in "Prepare branch and feedback"), and a branch that predates the script's merge does not contain it — bash exits 127, the gate dies with no outcome, and only the always-handoff kept the loop from going silent. Every bot branch created before the script's merge hits this. Stage the script from the trusted-base checkout into ${RUNNER_TEMP} before any branch switch (both jobs, for symmetry) and invoke the staged copy in both verify gates. This also means the gate logic always comes from the trusted base, never from the branch under verification. Test asserts: staged invocation in both gates (and no working-tree invocation), exactly two staging cp lines, review-address staging ordered before the branch switch, and the structural before-no-op ordering unchanged. 49/49. * test(autofix): also assert issue-job staging precedes its branch checkout Review: the lastIndexOf ordering assertion only protected the review-address job. The issue job's agent commits can touch .github/scripts, so staging after the verify gate's checkout would copy the agent's gate instead of the trusted base's. Assert the first staging occurrence (issue job) precedes the first git checkout "${BRANCH}" (also the issue job's). --------- Co-authored-by: wenshao <wenshao@example.com>
On a fresh install, state.json doesn't exist yet so the watcher's observedStoreGeneration is undefined. When the extension store writes its initial snapshot (generation:0), the next poll sees undefined→0 and incorrectly fires ExtensionRefreshNeeded, printing a spurious 'Extensions changed on disk' notice. Treat the first observed generation as baseline establishment rather than a change, so the undefined→N transition does not emit a refresh notification. Fixes #7029
6034219 to
148ccf9
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Unresolved, please confirm: [Critical] Existing PR blocker (comment 3600479035) on background-agent-resume.ts — per-model concurrency cap bypass. Defect verified in codebase (AgentMeta.model exists at agent-transcript.ts:141 but writeAgentMeta and loadPausedBackgroundAgents do not use it). Ruling: still stands, but this blocker is about code in packages/core/src/agents/ that is entirely outside this PR's scope (packages/cli/src/config/extension-file-watcher.ts). Filed by a previous automated review on unrelated code; relevance to this PR is uncertain. Not reviewed: code-quality — Agent 3 never opened its brief file despite substantive output. Not reviewed: Agent 3: Code quality — never opened its brief (/home/runner/work/qwen-code/qwen-code/.qwen/tmp/qwen-review-pr-7072-fetch-prompts/3.brief.md), so it reviewed without the instructions it was launched to follow.
— qwen3.7-max via Qwen Code /review
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: No Action Taken — PR #7072SummaryNo actionable feedback was found for this PR. The only substantive finding is Feedback Triage[Critical] background-agent-resume.ts — per-model concurrency cap bypass
"Not reviewed: code-quality" / "Not reviewed: Agent 3"
Failed checks
Inline comments / Issue-level comments
ConclusionThe PR's two-file change (extension-file-watcher baseline establishment) has no Base-conflict check: no conflict with main. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Unresolved, please confirm: [Critical] background-agent-resume.ts — per-model concurrency cap bypass (existing Critical from comment 3600479035). Defect still stands in codebase (AgentMeta.model declared at agent-transcript.ts:141 but never written by writeAgentMeta or read by loadPausedBackgroundAgents). However, this targets packages/core/src/agents/ code entirely outside this PR's 2-file scope (packages/cli/src/config/extension-file-watcher.ts). Filed by a previous automated review on unrelated code; this PR cannot fix it.
— qwen3.7-max via Qwen Code /review
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: No Action Required — PR #7072Feedback Triaged[Critical] background-agent-resume.ts — per-model concurrency cap bypass
SummaryNo actionable feedback was provided for PR #7072. The single Critical finding is a pre-existing issue in unrelated code that no PR of this scope can resolve. Base-conflict check: no conflict with main. |
Local build & real-run verification — LGTM at
|
| # | Scenario (real bundled CLI, fresh isolated $HOME) |
baseline origin/main |
PR #7072 |
|---|---|---|---|
| 1 | Fresh install: store writes generation:0 ~1.5 s after launch — 3 rounds |
❌ spurious notice at ~30.9 s (3/3) | ✅ silent through 45 s (3/3) |
| 2 | #1, then a real change (gen 0→1 at t=48 s, after baseline locked) | n/a — notice already latched | ✅ notice at ~60.9 s (next poll tick) |
| 3 | Existing install (state.json gen 5), bumped 5→6 while running |
✅ notice +0.3 s | ✅ notice +0.3 s (identical) |
| 4 | Unit: extension-file-watcher.test.ts, clean HOME |
❌ 2 failed under PR tests (negative control) | ✅ 23/23 |
What the E2E established
- The bug is real and deterministic on main. With the generation path isolated, the notice fires at ~30.9 s in 3/3 rounds — it's the first 30 s generation-poll tick doing
undefined → 0. (On macOS, chokidar never delivers events for thestate.jsonwatch root when the file didn't exist at arm time, so the poll is what actually fires this path; the 30 s delay + the other race paths below are why Avoid false "Extensions changed on disk" notice on first launch (initialize store generation baseline) #7029 says "sometimes".) - The fix works. PR build: 3/3 rounds silent through 45 s, with the debug-log referee confirming zero other watcher events — the
undefined → 0transition was reached and absorbed as baseline. - No regression. After the baseline locks in, an external gen 0→1 bump still produces the notice at the next tick (Where is the config saved? #2). On an existing install the chokidar fast path is untouched — notice 0.3 s after the change, byte-identical timing on both builds (如何自定义密钥文件 .env可能与其他文件冲突 #3).
- Tests pin the behavior. Reverting only the 9-line guard flips exactly the 2 fresh-install tests; the other 21 pass unchanged (Are you interested in AI Terminal? #4).
- CI at this head: ubuntu Test suite (33 m) green, review green; the failed
routejob is bot infra, not code.
One scope note for the maintainers (out of scope here, but affects #7029)
The visible symptom of #7029 has two more trigger paths that this PR intentionally doesn't touch, and I could still reproduce the notice on a truly virgin ~/.qwen on both builds (4/4 runs, at ~1.3 s):
extension directory created— the bootstrap watcher fires when the store creates~/.qwen/extensions/during first-launch init (this is what fired in my virgin runs; debug log shows the INFO item lands before any classified watcher event).- Enablement projection write —
writeSnapshotUnlockedalways (re)writesextensions/extension-enablement.jsonon fresh install; when chokidar arms before that write, the event classifiesstale→ same notice text.
So: merge this PR (it deterministically kills the root cause named in #7029's analysis), but I'd suggest keeping #7029 open (or filing a follow-up) for the two startup-race paths above.
Minor accepted trade-off, for the record: a real generation change landing before the first successful read (e.g. within the first 30 s of a fresh install when chokidar missed the file events) is now absorbed into the baseline silently — observed once in an early exploratory run. Previously that window produced a notice indistinguishable from the false positive; the next real change self-corrects. Reasonable exchange.
验证环境 / How this was verified (commands)
- Worktree at PR head
0f4fb5442; baseline = same tree with onlypackages/cli/src/config/extension-file-watcher.tschecked out fromorigin/main(20e4351d2; re-checked at post time — no watcher changes on main since). - Two bundles via
npm run generate && node esbuild.config.js && …(npm run bundle); chunk-level diff ofmarkStoreGenerationChangedconfirms the only delta is theprevious === void 0guard (screenshot 3). - E2E: spawn
node dist/cli.js --no-chat-recordingin a pty, feed@xterm/headless, scan full scrollback for the notice string; per-run fresh$HOMEwith settings pre-seeded to skip onboarding;~/.qwen/extensionspre-created (+ read-only in scenario 1 so the projection write can't emit an unrelatedstaleevent — that write failure is swallowed by design,state.jsonremains the commit point). - Referee:
QWEN_DEBUG_LOG_FILE=1; a trial counts only if the debug log shows zero unrelated watcher events. - Unit:
HOME=<clean tmp> npx vitest run src/config/extension-file-watcher.test.ts(avoids the zh-locale~/.qwenleak into CLI tests).
中文版本
本地构建 + 真实运行验证 — 0f4fb54 LGTM,附一条范围说明
构建了两个真实 bundle(npm run bundle):PR head 0f4fb5442,以及仅将 extension-file-watcher.ts 回退到 origin/main 的对照版(单文件覆盖,chunk 级 diff 确认唯一差异就是这 9 行守卫)。用 node-pty + @xterm/headless 驱动真实 TUI,每次运行使用全新隔离 $HOME,在 macOS 上验证(补充 PR 描述中仅 Linux 的测试声明)。以 QWEN_DEBUG_LOG_FILE=1 日志为裁判,确保每个试次只触发 store-generation 路径。
结果矩阵
| # | 场景(真实 bundle CLI,全新隔离 $HOME) |
基线 origin/main |
PR #7072 |
|---|---|---|---|
| 1 | 全新安装:启动 ~1.5 s 后 store 写入 generation:0(3 轮) |
❌ ~30.9 s 出现误报(3/3) | ✅ 45 s 内静默(3/3) |
| 2 | 场景 1 基线锁定后,t=48 s 外部真实变更(gen 0→1) | n/a — 提示已锁存 | ✅ ~60.9 s(下一轮询 tick)出现提示 |
| 3 | 既有安装(state.json gen 5),运行中 5→6 |
✅ 变更后 +0.3 s | ✅ +0.3 s(完全一致) |
| 4 | 单测:extension-file-watcher.test.ts(干净 HOME) |
❌ PR 测试下 2 个失败(负向对照) | ✅ 23/23 |
E2E 结论
- bug 在 main 上真实且确定性复现:隔离 generation 路径后,3/3 轮都在 ~30.9 s 触发 — 是第一个 30 秒轮询 tick 执行
undefined → 0。(macOS 上 chokidar 对启动时尚不存在的state.json监听根永远收不到事件,该路径实际由轮询触发;30 秒延迟 + 下述其他竞态路径正是 Avoid false "Extensions changed on disk" notice on first launch (initialize store generation baseline) #7029 说 "sometimes" 的原因。) - 修复有效:PR 构建 3/3 轮 45 s 内静默,裁判日志确认无其他 watcher 事件干扰 —
undefined → 0被作为基线吸收。 - 无回归:基线锁定后外部 0→1 变更在下一 tick 正常提示(场景 2);既有安装的 chokidar 快速路径不受影响,变更后 0.3 s 提示,两个构建时序完全一致(场景 3)。
- 测试钉住行为:仅回退 9 行守卫,恰好 2 个新装测试翻红,其余 21 个不变(场景 4)。
- 此 head 的 CI:ubuntu Test(33 分钟)绿、review 绿;失败的
routejob 是 bot 基础设施问题,与代码无关。
给维护者的范围说明(本 PR 之外,但影响 #7029)
#7029 的可见症状还有另外两条触发路径,本 PR 有意未涉及;在完全原始的 ~/.qwen 上,两个构建都仍能复现该提示(4/4 次,~1.3 s):
extension directory created— 首次启动时 store 创建~/.qwen/extensions/,bootstrap watcher 触发(我的原始 HOME 试次即为此路径;debug 日志显示 INFO 在任何 classified 事件之前入列)。- Enablement 投影写入 — 全新安装时
writeSnapshotUnlocked总会(重)写extensions/extension-enablement.json;若 chokidar 先于该写入装载,事件被归类为stale→ 同样文案的提示。
因此:建议合并本 PR(确定性消除了 #7029 根因分析所指的路径),同时建议保持 #7029 开启(或另立 follow-up)处理上述两条启动竞态路径。
顺带记录一个可接受的权衡:落在首次成功读取之前的真实 generation 变更(如全新安装最初 30 秒内、chokidar 又错过文件事件时)现在会被静默吸收进基线 — 早期探索运行中实际观察到一次。此前该窗口会产生一条与误报无法区分的提示;后续真实变更会自我纠正。合理的取舍。
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: No action needed for PR #7072SummaryThe review from @wenshao is an LGTM approval with detailed E2E verification
CI on ubuntu is green. The failed Scope note (no action — for maintainer follow-up)The reviewer identified two additional trigger paths for the visible symptom of
The reviewer explicitly recommends merging this PR ("it deterministically DecisionNo code changes needed. The PR is approved and ready to merge. Base-conflict check: no conflict with main. |



What this PR does
Treats the first observed store generation as baseline establishment rather than a change. When
observedStoreGenerationisundefined(state.json didn't exist yet) and the extension store writes its initial snapshot, the watcher now records the new generation without firingmarkExtensionsChanged.Why it's needed
On a fresh install,
~/.qwen/extension-store/state.jsondoesn't exist, soreadStoreGeneration()returnsundefined. The extension store then initializes and writesgeneration: 0. The next generation poll seesundefined !== 0, treats it as a change, and emitsExtensionRefreshNeeded— causing a spurious "Extensions changed on disk. Run /reload-plugins to apply updates." notice even though nothing actually changed. The in-memory extension state already matches the freshly written on-disk state, so there is nothing to reload.Reviewer Test Plan
How to verify
~/.qwen(noextension-store/state.json).qwenfor the first time./extensions), confirm generation changes ARE still detected normally afterward.Unit tests:
npx vitest run packages/cli/src/config/extension-file-watcher.test.ts— 23 tests pass, including 2 new tests covering the fresh-install baseline scenario.Evidence (Before & After)
Before: fresh launch prints "Extensions changed on disk. Run /reload-plugins to apply updates."
After: fresh launch prints nothing; subsequent real generation changes still trigger the notice.
Tested on
Environment (optional)
Local vitest; no Docker sandbox needed (unit tests only).
Risk & Scope
undefined → Ntransition, which previously produced a cosmetic false positive. Real generation changes (N → M where N !== M) are unaffected.Linked Issues
Fixes #7029
中文说明
这个 PR 做了什么
将首次读取到的 store generation 视为基线建立,而非变更。当
observedStoreGeneration为undefined(state.json 尚不存在)且扩展存储写入初始快照时,watcher 只记录新 generation,不触发markExtensionsChanged。为什么需要这个改动
全新安装时
~/.qwen/extension-store/state.json不存在,readStoreGeneration()返回undefined。随后扩展存储初始化写入generation: 0。下次 generation 轮询发现undefined !== 0,将其判定为变更并发出ExtensionRefreshNeeded事件,导致出现误导性的"Extensions changed on disk. Run /reload-plugins to apply updates."提示。实际上内存中的扩展状态与刚写入的磁盘状态一致,无需重新加载。审查测试计划
如何验证
~/.qwen(没有extension-store/state.json)。qwen。/extensions),确认后续正常的 generation 变更仍能被检测到。单元测试:
npx vitest run packages/cli/src/config/extension-file-watcher.test.ts— 23 个测试全部通过,包括 2 个新增的覆盖首次安装基线场景的测试。证据(修改前后对比)
修改前:全新启动会打印"Extensions changed on disk. Run /reload-plugins to apply updates."
修改后:全新启动无此提示;后续的真实 generation 变更仍能正常触发提示。
测试环境
环境(可选)
本地 vitest,无需 Docker 沙箱(仅单元测试)。
风险与范围
undefined → N的转换,此前该场景产生的是外观误报。真实的 generation 变更(N → M,N !== M)不受影响。关联 Issue
Fixes #7029