Skip to content

refactor(serve): default project memory to workspace scope - #8856

Merged
wenshao merged 9 commits into
QwenLM:mainfrom
qqqys:codex/issue-8854-daemon-workspace-memory-default
Aug 12, 2026
Merged

refactor(serve): default project memory to workspace scope#8856
wenshao merged 9 commits into
QwenLM:mainfrom
qqqys:codex/issue-8854-daemon-workspace-memory-default

Conversation

@qqqys

@qqqys qqqys commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Makes exact-workspace project-memory isolation the default for every runtime managed by qwen serve. Explicit CLI configuration still wins over the launch environment, and an explicit git-root selection remains available as a legacy migration path.

Updates the user-facing help and memory documentation to distinguish the daemon default from the unchanged standalone CLI behavior and to explain that existing Git-root memory is not migrated automatically.

Why it's needed

The daemon already creates and routes a distinct runtime for each registered workspace, but project memory previously stayed shared at the Git root unless operators supplied an opt-in flag. That made the normal multi-workspace daemon configuration share one runtime-owned resource across otherwise isolated workspaces.

Reviewer Test Plan

How to verify

  • Start qwen serve with two registered directories under the same Git repository and omit both memory-project-scope settings. Confirm every runtime receives workspace and project-memory operations resolve independently for the two directories.
  • Set the launch environment to git-root without passing the option. Confirm every runtime retains the legacy shared scope.
  • Set the launch environment to workspace and pass --memory-project-scope git-root. Confirm the option wins for every runtime and the daemon does not mutate its launch environment.
  • Confirm a standalone qwen invocation still uses its existing Git-root default.

Evidence (Before & After)

Before: the new default-isolation regression test failed because the runtime environment contained no project-memory scope (expected undefined to be "workspace").

After: the default, launch-environment compatibility, and CLI-over-environment cases pass for both primary and secondary runtimes. The complete related suite passes 298/298 tests; the repository build, typecheck, focused ESLint, and Prettier checks also pass.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS with Node.js 24.14.1; package-specific Vitest coverage plus repository build and typecheck.

Risk & Scope

  • Main risk or tradeoff: daemon users who relied on the implicit Git-root scope will see a new workspace-specific project-memory view after upgrading.
  • Not validated / out of scope: automatic migration, standalone CLI defaults, user memory, team memory, and local validation on Windows or Linux.
  • Breaking changes / migration notes: existing Git-root project memory is not copied or merged; pass --memory-project-scope git-root or set QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root to retain the previous view during migration.

Linked Issues

Closes #8854

中文说明

本 PR 做了什么

qwen serve 管理的每个 runtime 默认按精确 workspace 隔离项目记忆。显式 CLI 配置仍优先于启动环境变量,同时保留显式 git-root,作为旧存储迁移期间的兼容入口。

同步更新用户帮助和记忆文档,明确 daemon 默认值与保持不变的 standalone CLI 行为,并说明不会自动迁移已有的 Git-root 记忆。

为什么需要

daemon 已经为每个注册 workspace 创建并路由独立 runtime,但项目记忆此前仍默认按 Git 根目录共享,只有传入 opt-in 参数才会隔离。这导致常规多 workspace daemon 配置中,一个由 runtime 管理的资源仍跨越了其他资源已经遵守的 workspace 边界。

Reviewer 测试计划

如何验证

  • 在同一个 Git 仓库下注册两个目录并启动 qwen serve,不要设置任何 memory-project-scope 配置。确认每个 runtime 都收到 workspace,两个目录的项目记忆操作分别解析到独立存储。
  • 只把启动环境变量设置为 git-root,不传 CLI 选项。确认每个 runtime 都保留旧的共享 scope。
  • 把启动环境变量设置为 workspace,同时传入 --memory-project-scope git-root。确认 CLI 对每个 runtime 生效且 daemon 不修改启动环境。
  • 确认 standalone qwen 仍使用原有的 Git-root 默认值。

前后证据

改动前:新增的默认隔离回归测试失败,因为 runtime 环境中没有项目记忆 scope(expected undefined to be "workspace")。

改动后:默认值、启动环境兼容、CLI 覆盖环境变量三个场景都在 primary 和 secondary runtime 上通过。完整相关测试 298/298 通过;仓库 build、typecheck、聚焦 ESLint 和 Prettier 检查也全部通过。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS,Node.js 24.14.1;执行 package 级 Vitest、仓库 build 和 typecheck。

风险与范围

  • 主要风险或取舍:之前依赖隐式 Git-root scope 的 daemon 用户,升级后会看到新的 workspace 项目记忆视图。
  • 未验证 / 不在范围:自动迁移、standalone CLI 默认值、用户记忆、团队记忆,以及 Windows 或 Linux 本地验证。
  • 破坏性变化 / 迁移说明:不会复制或合并已有 Git-root 项目记忆;迁移期间可传入 --memory-project-scope git-root 或设置 QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root 保留旧视图。

关联 Issue

Closes #8854

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 10, 2026
@qqqys
qqqys marked this pull request as ready for review August 10, 2026 09:11
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Re-running the gate on a520f352. The only commit since the last pass is a merge of main — the PR's own diff is byte-identical to what was reviewed at c6ed41cf, and the merge-base is now current with main (no conflicts left). The gate was re-evaluated against the new head regardless.

Template still looks good ✓ — all sections filled in, bilingual summary and migration note intact.

Problem: unchanged — real and concrete, not theoretical. The daemon routes every other resource per registered workspace, so project memory defaulting to the shared Git-root scope was an observable inconsistency; #8854 documents it with acceptance criteria, and this completes the opt-in isolation landed in #8056. Still self-reported by the same author (review/self-reported), accepted during issue triage.

Direction: aligned. Workspace isolation as the safe daemon default, with the legacy git-root scope kept as an explicit migration path and the standalone CLI default untouched. The merge commit changed nothing about the PR's direction — it only caught the branch up with main, and nothing that landed on main touches this PR's seams (the exclusion list, the serve base-env freeze, or core memory scope resolution).

Size: unchanged from the previous pass — one core path touched, packages/cli/src/config/shared-env-keys.ts (packages/*/src/config/**, +3 lines). Still small: 43 production lines, 76 test lines, 50 docs lines. Far below any Tier 1 threshold; the fork-refactor guardrail applies at the end regardless.

Approach: unchanged and still right — one injection point (option → non-blank launch env → workspace), one exclusion-list entry making the scope operator-owned, docs updated on all three surfaces. No drive-by changes in the merge commit either.

Risk: no elevated risk signals — no files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

a520f352 上重新过门禁。距上次审查仅新增一个合并 main 的提交 —— PR 自身 diff 与 c6ed41cf 逐字节一致,merge-base 已与 main 齐平(无冲突)。门禁仍按新 head 重新评估。

模板依然完整 ✓ —— 各节齐全,中文摘要与迁移说明俱在。

问题:不变 —— 真实具体,非理论性问题。daemon 的其他资源均按注册 workspace 路由,项目记忆默认共享 Git-root scope 是可观测的不一致;#8854 已带验收标准记录,本 PR 收尾 #8056 落地的 opt-in 隔离。仍为同一作者自报(review/self-reported),issue 分诊阶段已接受。

方向:对齐。workspace 隔离作为 daemon 的安全默认值,legacy git-root 保留为显式迁移路径,standalone CLI 默认值不变。合并提交未改变方向 —— 只是把分支同步到 main,且 main 新落地的内容没有触及本 PR 的接缝(排除清单、serve base-env 冻结、core 记忆 scope 解析)。

规模:与上轮一致 —— 触及一条核心路径 packages/cli/src/config/shared-env-keys.ts(命中 packages/*/src/config/**,+3 行)。仍小:生产 43 行、测试 76 行、文档 50 行。远低于 Tier 1 阈值;fork-refactor 守卫规则在最后环节照常适用。

方案:不变且依然合理 —— 单一注入点(选项 → 非空白启动环境 → workspace)、一条排除项使 scope 归 operator 独占、三处文档同步更新。合并提交也没有夹带改动。

风险:无升级风险信号 —— 无文件命中与 revert 相关的高风险路径。

进入代码审查。🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Code review

Re-reviewing at the new head a520f352. Exactly one commit landed since the last pass — a merge of main — and I verified by diff that it left all ten PR files untouched, so the reviewed change is byte-identical to the one the previous round, the sandboxed /verify round, and the maintainer's local real-stack run all examined at c6ed41cf. My read of the code was re-confirmed against the fetched head, not carried on faith:

  • The validate-before-freeze guard is intact above the injection: an invalid optsIn.memoryProjectScope still throws before anything is baked into the immutable base env, the yargs flag has no default: and choices rejects bad CLI values, so the option stays absent unless the operator passes it.
  • The precedence chain is exactly option → non-blank launch env → workspace: blank/whitespace-only launch values are treated as unset, the raw value is injected into baseEnv only (never process.env), and the normalized resolvedMemoryProjectScope feeds only the boot log line with its source (option / environment / default).
  • The exclusion entry in PROJECT_ENV_HARDCODED_EXCLUSIONS is present with an accurate rationale comment, enforced through the case-folded isHardcodedProjectEnvExclusion predicate that every project .env / settings.env application path goes through; home .env, launch env, and the CLI flag are unaffected.
  • No packages/core files in the diff — the downstream trim/lowercase/warn-once/fallback semantics for unrecognized values are existing core behavior, unchanged.

No critical issues, no project-convention violations. The one scope question from the previous round — the exclusion also closes the standalone CLI's project-.env route — is now largely answered by the diff itself: the docs no longer advise pinning the scope in a workspace .env (that advice was removed and replaced with "export it in the shell or service environment"), and the maintainer's own verification called the closure good defence in depth. It remains a conscious policy call, not a defect.

CI evidence

Unattended re-run — I never build or run PR code; the evidence below is the PR's own CI on the reviewed commit, fetched through the API. Everything is green on a520f352, including the two lanes that matter most here:

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Classify PR ✅ success
macos-latest / Java 21 ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

No workflow runs with the pull_request event are still pending on this head — this is the settled CI picture, not a mid-flight one.

On sandboxed evidence: the ✅ /verify round (76/76 assertions, 12-cell A/B matrix, 9-mutant matrix) covered c6ed41cf, and the head has moved only by a merge that provably did not touch the PR's diff or its seams, so that evidence carries over. Sandboxed verification is being re-run at the new head anyway: @qwen-code /verify — a fresh run was triggered by this re-run and is in flight at post time; it should re-pin on a520f352 that two registered workspaces resolve project memory into separate stores under the injected default, and that a workspace .env / settings.env can no longer override the frozen scope.

中文说明

代码审查:在新 head a520f352 上重审。距上次审查仅新增一个提交 —— 合并 main —— 已用 diff 确认它未改动全部十个 PR 文件,因此本次审查对象与上一轮、沙箱 /verify、以及维护者本地真实环境验证在 c6ed41cf 上检验的改动逐字节一致。以上结论是对拉取到的 head 重新读码确认的,不是沿用旧结论:冻结前校验仍位于注入之前(非法 optsIn.memoryProjectScope 先抛错),yargs 选项无 default:choices 拒绝非法 CLI 取值;优先级链恰为 选项 → 非空白启动环境 → workspace,空白/纯空白视作未设置,原始值只注入 baseEnv(从不写 process.env),归一化结果仅用于启动日志(含 option / environment / default 来源);排除项在 PROJECT_ENV_HARDCODED_EXCLUSIONS 中且注释理由准确,经大小写折叠谓词在全部项目 .env/settings.env 应用路径生效,home .env、启动环境与 CLI 选项不受影响;diff 不含任何 packages/core 文件 —— 未识别取值的 trim/小写/告警一次/回退语义是既有 core 行为,未被改动。未发现阻塞问题或项目规范违规。上一轮的范围问题(排除项同时关闭 standalone CLI 的项目 .env 路径)现已基本由 diff 自身回答:文档删除了"在 workspace .env 中钉住 scope"的建议、改为"在 shell 或服务环境中导出",维护者本人的验证也称该关闭是良好的纵深防御。它仍是一项有意识的策略决定,而非缺陷。

CI 证据:无人值守 re-run,不构建/运行 PR 代码;以上为所审提交上 PR 自身 CI 经 API 获取的结果 —— a520f352 全绿,含最关键的两条 lane(单测与 Serve A/B)。该 head 上没有仍在进行的 pull_request 事件工作流 —— 这是落定的 CI 图景,不是进行中的。

沙箱证据:此前的 ✅ /verify(76/76 断言、12 cell A/B 矩阵、9 突变体矩阵)覆盖 c6ed41cf;head 仅移动了一个可证明未触及 PR diff 与其接缝的合并,该证据随之迁移。沙箱验证仍在新 head 上重跑:本次 re-run 已触发新的 @qwen-code /verify,发布时正在运行;它应钉住 a520f352 上的两条声明 —— 两个注册 workspace 在注入默认值下解析到独立记忆存储,且 workspace .env/settings.env 无法再覆盖冻结的 scope。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — every stage is green and the diff is now verified three independent ways; the cap is pure policy — fork refactor PRs are never auto-approved, so the final vote belongs to a human.

Stepping back: this re-run added one commit — a merge of main — and I verified it changed nothing about the PR itself: all ten PR files are byte-identical to the c6ed41cf tree, the merge-base is now current with main, and nothing that landed on main touches this PR's seams. So the picture at the new head is the same one the evidence already supports: the sandboxed /verify round passed 76/76 assertions with a mutation matrix showing the tests actually pin the behavior, @wenshao's local real-stack run drove a real daemon through the full precedence matrix (with a load-bearing check: the PR's tests fail 7/348 against merge-base source), and CI is fully green on this exact commit with nothing pending. Forty-three production lines, every one traceable to an acceptance criterion in #8854 or a verify finding; I still haven't found a simpler shape for this change.

Two things a human should own, neither a defect:

  1. The policy calls are settled but worth a final nod. The exclusion closing the standalone project-.env route is now consistent with the docs (the old "pin it in your workspace .env" advice was removed) and the maintainer's verification endorsed it as defence in depth. The git-root compatibility window — how long the escape hatch stays — remains the maintainer's decision, open since issue triage.
  2. Follow-up candidates from the maintainer's own verification, if they aren't already tracked: an unrecognized launch-env value (a typo like workspce) silently lands on the legacy shared scope — pre-existing core behavior, but its meaning flipped now that isolation is the default; and a one-time boot notice when a non-empty legacy store exists would make the silent upgrade less surprising.

Verdict: defer. ⏸️ @wenshao approved this exact commit minutes before this re-run — that is one of the two approvals main requires, and it is a human vote this gate should not second-guess. The bot's own approval stays withheld regardless: this is a cross-repository refactor PR, and the fork-refactor guardrail forbids auto-approval no matter how clean the review is. No approval — immediate or CI-conditional — has been posted, and none will be by this bot; the second approval and the merge are the maintainers' call.

中文说明

冷静回看:本次 re-run 只新增了一个提交 —— 合并 main —— 已验证它对 PR 本身毫无改动:全部十个 PR 文件与 c6ed41cf 逐字节一致,merge-base 已与 main 齐平,main 新落地的内容也未触及本 PR 的接缝。因此新 head 上的图景就是证据已经支撑的那一个:沙箱 /verify 76/76 断言通过、突变矩阵证明测试确实钉住行为;@wenshao 的本地真实环境验证驱动真实 daemon 走完了完整优先级矩阵(并做了吃紧性检验:PR 的测试套到 merge-base 源码上 7/348 失败);CI 在该提交上全绿且无进行中项。43 行生产代码,每行都可追溯到 #8854 的验收标准或 verify 发现;至今未找到更简形态。

两项交由人工决定,均非缺陷:其一,策略决定已基本落定但值得最后点头 —— 排除项关闭 standalone 项目 .env 路径,现已与文档一致(旧的"在 workspace .env 中钉住"建议已删除),维护者的验证也认可其为纵深防御;git-root 兼容入口的保留窗口自 issue 分诊起仍待维护者决定。其二,来自维护者本人验证的两个后续候选(若尚未记录):无法识别的启动环境取值(如拼错的 workspce)会静默落到 legacy 共享 scope —— 早于本 PR 的 core 行为,但在隔离成为默认后其含义发生了反转;以及在存在非空 legacy 存储时打一条一次性启动提示,可让这次无声升级不那么意外。

结论:暂缓。⏸️ @wenshao 在本次 re-run 前几分钟已批准该提交 —— 那是 main 所需两个批准中的一个,是人工投票,门禁不应越俎代庖。机器人的批准无论如何继续保留:这是来自 fork 的 refactor PR,fork-refactor 守卫规则禁止自动批准,与审查多干净无关。本次不发布任何批准(含 CI 转绿后生效的延迟批准)标记,该机器人之后也不会发布;第二个批准与合入由维护者决定。

Qwen Code · qwen3.8-max

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

@qqqys

qqqys commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /verify

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 31 passed · 0 failed · 31 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:31 通过 · 0 失败 · 31 总计

Verification report

PR 8856 deep verification

Verdict: merge-ready — 31/31 scripted assertions passed (assertions.json), verified head 31e3061d9bb03956fe198c60070768dd5c79b5bb (merge-ref checkout pull/8856/merge, base tip 0d56e50b64, single commit matching the metadata snapshot).

中文摘要
  • 结论merge-ready。31/31 脚本化断言通过,无阻塞性发现。
  • A/B 结论:在 HEAD^1(改前)与 HEAD^2(改后)上用真实 runQwenServe 启动(双 workspace)跑了 7 个 launch-env/option 组合。唯一差异的 cell 是"两者都未配置":改前两个 runtime 的 effectiveEnv 均无该键(下游回落到 git-root 共享),改后两个 runtime 均注入 "workspace" —— 正是 PR 的中心声明。其余 6 个 cell(含空串、大写、无法识别值、option 覆盖 env)两臂逐字节一致,且两臂均未改动启动环境。
  • 变更承重证明:把 PR 新增的默认用例放回 base 代码运行,按预期变红(expected undefined to be 'workspace');5 个突变体(整体回退、优先级互换、去掉 env 回退、直接改 process.env、错误默认值)全部只杀死预期用例,无存活突变。
  • 门控run-qwen-serve.test.ts 243/243、相邻 serve/fast-path 套件 140/140、core 记忆下游套件 17/17、typecheck/eslint/prettier 全绿(lint 门以植入违规验证为活门)。
  • 未覆盖:真实会话内的记忆读写端到端(在 env 注入缝与 core 套件层验证);workspace 设置 overlay 对 base env 的覆盖优先级(既有机制,两臂一致,非本 PR 变更)。

Central claim and A/B

Central claim: a qwen serve daemon started with neither --memory-project-scope nor QWEN_CODE_MEMORY_PROJECT_SCOPE now stamps QWEN_CODE_MEMORY_PROJECT_SCOPE=workspace into the frozen daemon base env, so every workspace runtime (and the ACP children spawned from it) resolves project memory by exact workspace instead of the legacy shared git-root scope. Secondary claims: (a) precedence CLI option > launch env > workspace with the launch environment never mutated; (b) the standalone CLI default (git-root) is untouched.

The oracle is the per-runtime env.effectiveEnv observed through a real runQwenServe boot (real TCP listener, real baseEnv freeze, real workspace registry; telemetry/settings/trust/bridge mocked exactly as the repo's own test does). The same 7-cell harness ran on a HEAD^1 worktree and on HEAD^2:

cell launch env option base effective (2 runtimes) head effective verdict
unset (unset), (unset) "workspace", "workspace" DIFFERS — the change
env-git-root git-root git-root, git-root git-root, git-root identical
env-workspace workspace workspace, workspace workspace, workspace identical
env-empty "" "", "" "", "" identical
env-upper WORKSPACE WORKSPACE, WORKSPACE WORKSPACE, WORKSPACE identical
env-unrecognized banana banana, banana banana, banana identical
option-beats-env workspace git-root git-root, git-root git-root, git-root identical

process.env after boot equals the launch value in all 14 records (no launch-env mutation on either arm). Witness: evidence/01-ab-probe-matrix-base-vs-head.png; raw records in logs/base-probe.jsonl / logs/head-probe.jsonl.

Load-bearing proof of the default (evidence/03-base-arm-default-case-red.png): the PR's own new default case, executed against the base tree, fails with AssertionError: expected undefined to be 'workspace' — the exact "Before" evidence the PR body claims — and passes 3/3 on head.

Mutation matrix (no survivors)

Each mutant edits exactly one guard of the new ?? chain in a scratch worktree, runs the PR's three scope tests, and is reverted. Witness: evidence/02-mutation-matrix-all-mutants-killed.png; per-mutant logs in logs/mutant-M*.log.

mutant default-workspace option-beats-env env-preserved first failing assertion
unmutated head (control) GREEN GREEN GREEN
M1 revert whole hunk (= base) RED GREEN GREEN expected undefined to be 'workspace'
M2 env before option GREEN RED GREEN expected 'workspace' to be 'git-root'
M3 env fallback removed GREEN GREEN RED expected 'workspace' to be 'git-root'
M4 mutates process.env RED RED GREEN expected 'workspace' to be undefined
M5 wrong default (git-root) RED GREEN GREEN expected 'git-root' to be 'workspace'

Every mutant is killed by its intended test with an expected-vs-actual assertion (behavioural, not import/compile failure); each mutant kills only what its guard pins (M4 also trips case 1 because writing the default into process.env changes the unset→workspace observation). The unmutated control is green, and the base arm proves the harness can go red — the positive control for the matrix.

Reviewer Test Plan walk-through

  1. Two dirs, no config → every runtime workspace: head probe cell unset = ["workspace","workspace"] over two registered workspaces. Memory operations resolving independently is verified at the env-injection seam; downstream keying (getAutoMemoryRoot workspace scope) is pinned by the unchanged core suite (17/17, incl. ' Workspace ' normalization and unrecognized-value fallback).
  2. Launch env git-root, no option → legacy shared: cell env-git-root identical on both arms.
  3. Launch env workspace + --memory-project-scope git-root → option wins, env unmutated: cell option-beats-env effective git-root with processEnvAfter still workspace on both arms; PR test 2 green.
  4. Standalone CLI keeps git-root default: git diff HEAD^1..HEAD -- packages/core package.json package-lock.json is empty (the consumer resolveWorkspaceProjectScope() returns false on unset env), so the standalone path is provably untouched.

Findings (non-blocking)

  1. Empty-string env is now frozen into the daemon base env, not left absent. With QWEN_CODE_MEMORY_PROJECT_SCOPE='' and no option, head injects "" (empty string is not nullish to ??). Downstream, core treats '' as "not workspace" → git-root with no warning — byte-identical to base behavior (measured in both arms), so no regression; but the runtime env now carries an explicit empty value where base carried no key. Any future reader that treats key-presence as "operator configured scope" would misread it. Severity: nit; behavior unchanged today.
  2. The non-mutation assertion has a blind spot by construction: a mutant that writes the same value back into process.env passes case 3 (M4 row: case 3 GREEN). Cases 1–2 catch it, so the suite as a whole pins non-mutation; noted for completeness, not as a gap to fix.
  3. Workspace settings env overlay still wins over the daemon base env (buildRuntimeEnvironment layers trusted workspace settings on top of daemonRuntimeBaseEnv on both arms). Pre-existing mechanism, unchanged by this PR; a workspace .env can still override the injected default per workspace. Mentioned so the "daemon default" claim is read at the right layer.

Not covered

  • Live-session memory operations: I verified the injection seam (effectiveEnv that children spawn with) and the downstream core contract via its own suite, but did not drive a real ACP session through remember/forget in two workspaces. The probe reproduces the mechanism the PR changes; core resolution is pre-existing and separately pinned.
  • Per-workspace settings-overlay precedence against the new default (pre-existing, identical on both arms — see Finding 3).
  • Windows/macOS: container is Linux; the change is env-string semantics with no platform branch.
  • Repo-wide gates beyond the affected suites (the PR's own CI covers them; I ran the affected workspaces' suites plus typecheck/eslint/prettier on the changed files).

Methodology

Environment: CI verify container (node:22-bookworm), merge-ref checkout at depth 2; npm ci + build pre-run at head. A/B used scratch git worktrees at HEAD^1 and HEAD under tmp/ (removed after capture), wired to the root node_modules via symlinks; vitest aliases @qwen-code/qwen-code-core to each tree's own ../core source (verified by the base arm's import errors naming base-tree paths), and the PR touches no package.json/lockfile, so unchanged workspace deps are byte-identical across arms. Harnesses (render-ab-table.mjs, render-mutation-matrix.sh, assert-all.mjs, probe appended to a scratch copy of the test file) drive the real compiled-from-source runQwenServe with the repo's own mock perimeter; raw logs in logs/, all 31 assertions re-executable via node assert-all.mjs (witness: evidence/04-scripted-assertions-all-pass.png).

Evidence images

01-ab-probe-matrix-base-vs-head

02-mutation-matrix-all-mutants-killed

03-base-arm-default-case-red

04-scripted-assertions-all-pass

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

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

Qwen Code · serve A/B

@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. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Not explored to full depth (tool budget reached): This PR makes workspace-scoped project memory the default...: did not run npm run typecheck or the changed vitest file to confirm compilation/green tests (review-only evidence from code reading).; This PR makes workspace-scoped project memory the default...: did not fully trace the workspace .env / settings.env overlay allowlist ( shared-env-keys.ts ) — the merge mechanism is pre-existing and unchanged by this dif…; This PR makes workspace-scoped project memory the default...: one repo-wide grep returned 31 truncated trailing lines; the visible portion plus follow-up targeted greps covered cli, core, desktop, and integration-tests, bu…; This PR makes workspace-scoped project memory the default...: did not trace whether any pre-handler CLI bootstrap ( loadEnvironment mutating process.env from the primary workspace's .env ) runs before runQwenServeImpl…; You are review agent reverse-audit — Reverse audit agen...: couldn't verify whether the session-shell subprocess env includes the injected scope (code comment suggests the daemon's process.env) — a potential new default-…, and 3 more.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。 未探索到全部深度(达到工具调用预算):This PR makes workspace-scoped project memory the default...:did not run npm run typecheck or the changed vitest file to confirm compilation/green tests (review-only evidence from code reading).;This PR makes workspace-scoped project memory the default...:did not fully trace the workspace .env / settings.env overlay allowlist ( shared-env-keys.ts ) — the merge mechanism is pre-existing and unchanged by this dif…;This PR makes workspace-scoped project memory the default...:one repo-wide grep returned 31 truncated trailing lines; the visible portion plus follow-up targeted greps covered cli, core, desktop, and integration-tests, bu…;This PR makes workspace-scoped project memory the default...:did not trace whether any pre-handler CLI bootstrap ( loadEnvironment mutating process.env from the primary workspace's .env ) runs before runQwenServeImpl…;You are review agent reverse-audit — Reverse audit agen...:couldn't verify whether the session-shell subprocess env includes the injected scope (code comment suggests the daemon's process.env) — a potential new default-…,另有 3 条。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment thread packages/cli/src/serve/run-qwen-serve.ts Outdated
Comment thread packages/cli/src/serve/run-qwen-serve.ts Outdated
Comment thread packages/cli/src/serve/run-qwen-serve.ts Outdated
Comment thread packages/cli/src/serve/run-qwen-serve.test.ts
Comment thread packages/cli/src/commands/serve.ts
Comment thread docs/developers/daemon/17-configuration.md Outdated

@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. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Not explored to full depth (tool budget reached): This PR makes workspace-scoped project memory the default...: did not execute run-qwen-serve.test.ts / serve.test.ts (verification relied on code inspection; CI covers execution)..

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。 未探索到全部深度(达到工具调用预算):This PR makes workspace-scoped project memory the default...:did not execute run-qwen-serve.test.ts / serve.test.ts (verification relied on code inspection; CI covers execution).

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines +1273 to +1275
expect(logContent).toContain('project memory scope resolved');
expect(logContent).toContain('projectMemoryScope=workspace');
expect(logContent).toContain('projectMemoryScopeSource=default');

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.

[Suggestion] The new scope-log assertions depend on QWEN_CODE_MEMORY_PROJECT_SCOPE being absent from the ambient environment, but this test never saves/deletes/restores it — unlike the it.each scope tests this same diff rewrites (which snapshot the variable around line 3783). Probe-confirmed: with the variable exported, this test fails spuriously at projectMemoryScopeSource=default even though the code under test is correct; with a snapshot/delete/restore guard the contaminated arm passes. — Failure scenario: a developer or CI runner with QWEN_CODE_MEMORY_PROJECT_SCOPE exported (plausible for anyone working on this very feature) boots the daemon with source=environment, producing a red test about logging whose cause is invisible from the test name.

Fix (mirrors the originalScope pattern from the it.each block — applied before the runQwenServe call and in the existing finally):

const origScope = process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];
delete process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];
// ... in the existing finally block:
if (origScope === undefined) {
  delete process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];
} else {
  process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'] = origScope;
}
中文说明

[建议] 新增的 scope 日志断言依赖运行环境中不存在 QWEN_CODE_MEMORY_PROJECT_SCOPE,但该测试从未保存/删除/恢复这个变量——同一 diff 重写的 it.each scope 测试(约第 3783 行处)却做了快照保护。已通过探测复现:导出该变量时,此测试会在 projectMemoryScopeSource=default 处误报失败,尽管被测代码是正确的;加上快照/删除/恢复保护后,被污染的场景也能通过。— 失败场景:开发者或 CI runner 导出了 QWEN_CODE_MEMORY_PROJECT_SCOPE(对正在开发这个功能的人来说很常见),daemon 以 source=environment 启动,产生一个与日志本身无关的红色测试,且从测试名称完全看不出原因。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复 + 验证证据:65139068f8;在外部导出 QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root 时,定向日志测试 1/1 通过;完整 run-qwen-serve.test.ts 246/246 通过。

Comment on lines +2685 to +2688
daemonLog.info('project memory scope resolved', {
projectMemoryScope: resolvedMemoryProjectScope,
projectMemoryScopeSource: memoryProjectScopeSource,
});

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.

[Suggestion] The boot log records only the interpreted scope and source, never the raw configured value, and reports projectMemoryScopeSource=environment even when the environment value was unrecognized and merely fell through to git-root. Probe-confirmed: QWEN_CODE_MEMORY_PROJECT_SCOPE=workspce (typo) and a deliberate git-root produce byte-identical daemon log lines. — Failure scenario: a maintainer debugging "why do two daemon workspaces share project memory?" reads projectMemoryScope=git-root projectMemoryScopeSource=environment, which reads as "the environment explicitly chose git-root", sending them to argue about a setting nobody ever made, while the raw typo'd value appears nowhere in the log.

Suggested change
daemonLog.info('project memory scope resolved', {
projectMemoryScope: resolvedMemoryProjectScope,
projectMemoryScopeSource: memoryProjectScopeSource,
});
daemonLog.info('project memory scope resolved', {
projectMemoryScope: resolvedMemoryProjectScope,
projectMemoryScopeSource: memoryProjectScopeSource,
projectMemoryScopeRaw: memoryProjectScopeValue,
});
中文说明

[建议] 启动日志只记录解析后的 scope 和来源,从不记录原始配置值;即使环境变量的值无法识别、只是回落到 git-root,日志仍显示 projectMemoryScopeSource=environment。已通过探测复现:QWEN_CODE_MEMORY_PROJECT_SCOPE=workspce(拼写错误)和显式设置的 git-root 会产生逐字节相同的 daemon 日志行。— 失败场景:维护者排查"为什么两个 daemon workspace 共享项目记忆?"时读到 projectMemoryScope=git-root projectMemoryScopeSource=environment,会理解为"环境变量明确选择了 git-root",从而去争论一个根本没人做过的设置,而真正拼错的原始值在日志中无处可见。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复 + 验证证据:65139068f8;启动日志现记录 projectMemoryScopeRaw,定向日志测试 1/1、完整 run-qwen-serve.test.ts 246/246 通过。

@qqqys

qqqys commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

Real daemon E2E: PASS

Commands:

npm run build && npm run bundle
npx tsx .qwen/e2e-tests/issue-8854-daemon-memory-workspace.ts

The harness started the built qwen serve binary with two sibling workspaces nested under one temporary Git root, used a temporary memory base, and drove four real workspace-qualified managed-memory remember tasks through HTTP. A deterministic local OpenAI-compatible server supplied the hidden agents tool calls.

Evidence:

  • Daemon boot reported projectMemoryScope=workspace projectMemoryScopeSource=default.
  • Both workspace runtimes were registered and trusted.
  • The frontend and backend remember agents received different PROJECT directories and returned touchedScopes: ["project"].
  • A USER write returned touchedScopes: ["user"]; the backend remember agent then received the same USER directory and rebuilt shared user-memory index.
  • Seeded legacy Git-root project memory remained on disk but appeared in neither workspace prompt, confirming no migration or fallback.
  • Final filesystem checks confirmed both isolated project writes and the shared user write.
  • npm run build && npm run bundle completed successfully before the E2E run.

The E2E artifacts remain under the gitignored .qwen/e2e-tests/ directory and are not part of this PR.

@qqqys

qqqys commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

Final-head verification update: reran after fast-forwarding to 65139068f8.

  • cd packages/cli && npx vitest run src/serve/run-qwen-serve.test.ts: 246/246 passed.
  • CLI package build and root bundle: passed.
  • Real multi-workspace daemon memory E2E: passed again with distinct PROJECT roots, one shared USER root, preserved-but-unloaded legacy Git-root memory, and touched scopes [project], [project], [user], [].
  • Independent read-only test-agent run also passed 13/13 assertions through the real HTTP route, ACP child, remember agent, and write_file chain.
  • Local branch matches the remote branch and the tracked worktree is clean.

@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.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the unit suite was not run on Windows locally (the platform relevant to the Windows env-case finding in Needs Human Review).

Not explored to full depth (tool budget reached): This PR makes exact-workspace project-memory isolation th...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; This PR makes exact-workspace project-memory isolation th...: none — all checks above completed within budget..

中文说明

已审查。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the unit suite was not run on Windows locally (the platform relevant to the Windows env-case finding in Needs Human Review)。

未探索到全部深度(达到工具调用预算):This PR makes exact-workspace project-memory isolation th...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;This PR makes exact-workspace project-memory isolation th...:none — all checks above completed within budget.

— qwen3.8-max via Qwen Code /review (v0.21.8)

| `QWEN_SERVE_RATE_LIMIT_WINDOW_MS` | Env fallback for `--rate-limit-window-ms`. |
| `QWEN_CODE_MEMORY_PROJECT_SCOPE` | `workspace` keys project memory by the exact workspace dir; `git-root` selects the legacy shared scope. When unset, the daemon injects `workspace`; unrecognized values warn once and retain the legacy `git-root` behavior. Propagates via the runtime base env, not `childEnvOverrides`; `--memory-project-scope` wins. Each workspace remember/forget/dream lane caps pending tasks at `MAX_PENDING = 16`; N workspaces allow up to 16·N queued tasks with no daemon-wide cap. |

Blank `QWEN_CODE_MEMORY_PROJECT_SCOPE` values are treated as unset and therefore default to `workspace`; unrecognized non-empty values still warn once and retain the legacy `git-root` behavior.

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.

[Suggestion] The documented guarantee that a workspace .env/settings.env cannot override the frozen memory-project scope does not hold on the serve fast path (runtime-probe verified): loadServeFastPathEnvironment writes every non-excluded key from the primary workspace's .env/settings.env into process.env when the launch env lacks it, and QWEN_CODE_MEMORY_PROJECT_SCOPE is in none of the exclusion lists (PROJECT_ENV_HARDCODED_EXCLUSIONS, isLoaderEnvKey, DEFAULT_EXCLUDED_ENV_VARS) — Failure scenario: plain qwen serve with no flag and no launch env in a repo whose .env sets QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root → the key lands in process.env before runQwenServeImpl snapshots it, the daemon freezes git-root into every runtime (the exact sharing this PR removes), and the boot log records projectMemoryScopeSource=environment, pointing operators at the launch env when the value came from a workspace file. The adjacent DEV key is already hardcoded-excluded with a dedicated test in run-qwen-serve.test.ts.

Suggested fix: add QWEN_CODE_MEMORY_PROJECT_SCOPE to PROJECT_ENV_HARDCODED_EXCLUSIONS in packages/cli/src/config/shared-env-keys.ts so every project-env loader rejects it from workspace env files; alternatively correct the docs to state that a workspace env file at the serve startup directory can still seed the scope when flag and launch env are both absent.

中文说明

文档中"workspace 的 .env/settings.env 无法覆盖冻结的 memory-project scope"这一保证在 serve 快速路径上不成立(已通过运行时探针验证):loadServeFastPathEnvironment 会在启动环境缺少某个键时,把主 workspace 的 .env/settings.env 中所有未被排除的键写入 process.env,而 QWEN_CODE_MEMORY_PROJECT_SCOPE 不在任何排除列表(PROJECT_ENV_HARDCODED_EXCLUSIONSisLoaderEnvKeyDEFAULT_EXCLUDED_ENV_VARS)中——失败场景:在 .env 设置了 QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root 的仓库中,不传参数、不设启动环境变量直接运行 qwen serve → 该键在 runQwenServeImpl 快照之前进入 process.env,daemon 会把 git-root 冻结到每个 runtime(正是本 PR 要消除的共享),而启动日志记录 projectMemoryScopeSource=environment,把排查方向指向启动环境,实际值却来自 workspace 文件。相邻的 DEV 键已在硬编码排除列表中并有专门测试(run-qwen-serve.test.ts)。

建议修复:把 QWEN_CODE_MEMORY_PROJECT_SCOPE 加入 packages/cli/src/config/shared-env-keys.tsPROJECT_ENV_HARDCODED_EXCLUSIONS,让所有项目 env 加载器都拒绝从 workspace env 文件读取该键;或者修正文档,说明当参数和启动环境都未设置时,serve 启动目录下的 workspace env 文件仍可为 scope 赋值。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复 + 验证证据:c6ed41cf6c;共享排除表回归 45/45、既有 full-loader 门控 probe 2/2 通过;ESLint、Prettier、git diff --check 通过。

Comment on lines +2132 to +2135
const resolvedMemoryProjectScope =
memoryProjectScopeValue.trim().toLowerCase() === 'workspace'
? 'workspace'
: 'git-root';

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.

[Suggestion] The scope-normalization rule (trim + lowercase; only workspace opts in, everything else is git-root) is now implemented a second time here; the original is resolveWorkspaceProjectScope() in packages/core/src/memory/paths.ts, and nothing couples the two implementations — Failure scenario: any future change to the core rule (a new alias, different normalization, a different fallback) leaves this boot-log computation silently asserting a scope the runtimes don't apply — the exact diagnostic this log line was added for becomes wrong, and the divergence is invisible. Cost today: one decision rule maintained in two packages, with the runtime side authoritative and the daemon side log-only.

Suggested fix: extract the pure decision into packages/core/src/memory/scopes.ts — a zero-import leaf, already subpath-exported as @qwen-code/qwen-code-core/memoryScopes and already imported in this file — e.g. export function normalizeMemoryProjectScope(raw: string | undefined): MemoryProjectScope, and call it from both resolveWorkspaceProjectScope() and here; the warn-once side effect and the direct env read stay in paths.ts.

中文说明

scope 归一化规则(trim + 小写;仅 workspace 生效,其余一律 git-root)在这里被第二次实现;原始实现在 packages/core/src/memory/paths.tsresolveWorkspaceProjectScope(),两处实现之间没有任何耦合——失败场景:未来对 core 规则的任何修改(新增别名、不同的归一化、不同的回退值)都会让这条启动日志的计算悄悄断言一个 runtime 实际并未应用的 scope——这条日志正是为排查而加的,届时它会给出错误信息,且分歧不可见。当前成本:一条判定规则分散在两个包中维护,runtime 侧才是权威,daemon 侧仅用于日志。

建议修复:把纯判定逻辑抽到 packages/core/src/memory/scopes.ts——它是零依赖叶子模块,已通过 @qwen-code/qwen-code-core/memoryScopes 子路径导出,且本文件已经引入——例如 export function normalizeMemoryProjectScope(raw: string | undefined): MemoryProjectScope,然后让 resolveWorkspaceProjectScope() 和这里都调用它;warn-once 副作用和直接读 env 的逻辑保留在 paths.ts

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines 3983 to +3985
expect(
runtime.env.effectiveEnv?.['QWEN_CODE_MEMORY_PROJECT_SCOPE'],
).toBe('git-root');
).toBe(expectedScope);

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.

[Suggestion] No test pins the newly documented guarantee that a workspace .env/settings.env cannot override the daemon's frozen scope — the it.each workspaces are empty mkdtemp dirs with no .env file, so the always-inject × setRuntimeEnvIfUnset interaction is never exercised — Failure scenario: if a future change re-introduces conditional injection (key absent in the default case) or switches the overlay to override mode, a single workspace's .env containing QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root would silently flip that runtime back to shared git-root memory — recreating exactly the cross-workspace sharing this PR removes — while the whole suite stays green.

Suggested fix: add a case that writes a .env containing QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root into one registered workspace directory and asserts that runtime's effective value stays workspace.

中文说明

没有测试固化本文档新增的保证——workspace 的 .env/settings.env 无法覆盖 daemon 冻结的 scope——it.each 中的 workspace 是空的 mkdtemp 目录,没有 .env 文件,因此"始终注入 × setRuntimeEnvIfUnset(仅空位写入)"的交互从未被验证——失败场景:如果未来某个改动恢复了条件注入(默认情况下不注入该键),或把 overlay 改为覆盖模式,那么某个 workspace 的 .env 中的 QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root 就会悄悄把该 runtime 切回共享的 git-root 记忆——恰好重现本 PR 要消除的跨 workspace 共享——而整个测试套件仍是绿的。

建议修复:新增一个用例,向某个已注册的 workspace 目录写入包含 QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root.env,并断言该 runtime 的有效值仍为 workspace

— qwen3.8-max via Qwen Code /review (v0.21.8)

}
: {}),
};
const baseEnv: NodeJS.ProcessEnv = { ...process.env };

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.

[Suggestion] Second seeding path for the same documented-guarantee violation (runtime-probe verified): the yargs fallback serve handler (commands/serve.ts:688-690) calls loadSettings(...) for the YOLO warning without skipLoadEnvironment, so loadEnvironment() writes QWEN_CODE_MEMORY_PROJECT_SCOPE from the primary workspace's .env/settings.env into process.env before this snapshot is taken — Failure scenario: qwen serve booted via an argv shape the fast-path parser rejects (fallback to full yargs), no scope exported, repo-committed .env sets git-root → the snapshot freezes git-root daemon-wide, the boot log misattributes projectMemoryScopeSource=environment, and the new isolation default is silently disabled. Blank-export asymmetry: the fast path's Object.hasOwn guard blocks .env seeding when the variable is exported blank, but this path's isEffectivelyUnset treats '' as unset and lets the .env value win — identical inputs produce different scopes per entry path. Fixing only loadServeFastPathEnvironment (see the separate comment on the docs page) leaves this path open.

Suggested fix: pass skipLoadEnvironment: true to the boot-time loadSettings call in the serve handler (it only reads merged.tools for the YOLO predicate), and/or add the key to PROJECT_ENV_HARDCODED_EXCLUSIONS, which closes both this path and the fast path in one place.

中文说明

同一个"文档保证被破坏"问题的第二条注入路径(已通过运行时探针验证):yargs 回退的 serve handler(commands/serve.ts:688-690)为了 YOLO 警告调用 loadSettings(...) 时没有传 skipLoadEnvironment,因此 loadEnvironment() 会在本快照之前,把主 workspace 的 .env/settings.env 中的 QWEN_CODE_MEMORY_PROJECT_SCOPE 写入 process.env——失败场景:通过快速路径解析器拒绝的 argv 形式启动 qwen serve(回退到完整 yargs),未导出 scope,仓库提交的 .env 设置了 git-root → 快照把 git-root 冻结到整个 daemon,启动日志错误地记录 projectMemoryScopeSource=environment,新的隔离默认值被悄悄禁用。空白导出的不对称:快速路径的 Object.hasOwn 守卫在变量被导出为空时阻止 .env 注入,而本路径的 isEffectivelyUnset'' 视为未设置、让 .env 的值生效——相同输入在不同入口路径下产生不同的 scope。只修 loadServeFastPathEnvironment(见文档页上的另一条评论)会留下这条路径。

建议修复:给 serve handler 启动时的 loadSettings 调用传入 skipLoadEnvironment: true(它只为 YOLO 判断读取 merged.tools),和/或把该键加入 PROJECT_ENV_HARDCODED_EXCLUSIONS,一次性同时关闭这条路径和快速路径。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复 + 验证证据:c6ed41cf6c;共享排除表回归 45/45、既有 full-loader 门控 probe 2/2 通过;ESLint、Prettier、git diff --check 通过。

await startServeHandlerWithArgs(
'--no-web --memory-project-scope workspace',
);
await startServeHandlerWithArgs('--no-web --memory-project-scope git-root');

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.

[Suggestion] No test pins the new default chain at the yargs CLI boundary: flag omitted → memoryProjectScope absent from the options → daemon-side workspace default. The only handler-level test now exercises an explicit non-default flag (this one), and the parser tests also only pass explicit values; the fast-path parser is pinned by an exact-shape assertion, but the yargs boundary is not — Failure scenario: a future change reintroducing a default at the CLI layer (e.g. default: 'git-root' on the yargs option or argv['memory-project-scope'] ?? 'git-root' in the handler spread) would make every flag-less qwen serve keep legacy git-root partitioning while the docs promise workspace isolation; the it.each cases call runQwenServe directly and would stay green, and this suite never asserts the option's absence, so the regression ships green.

Suggested fix: add a handler test asserting startServeHandlerWithArgs('--no-web') calls mockRunQwenServe with an options object that does not contain memoryProjectScope.

中文说明

没有测试在 yargs CLI 边界固化新的默认链:不传参数 → options 中不含 memoryProjectScope → daemon 侧默认 workspace。唯一的 handler 级测试现在只验证显式传入非默认值(即本测试),parser 测试也只传显式值;快速路径 parser 有精确形状断言固化,但 yargs 边界没有——失败场景:未来在 CLI 层重新引入默认值(例如给 yargs 选项加 default: 'git-root',或在 handler 的展开中写 argv['memory-project-scope'] ?? 'git-root')会让每个不带参数的 qwen serve 继续使用旧的 git-root 分区,而文档承诺的是 workspace 隔离;it.each 用例直接调用 runQwenServe,仍是绿的,且本套件从未断言该选项的缺失,回归会带着全绿上线。

建议修复:新增 handler 测试,断言 startServeHandlerWithArgs('--no-web') 调用 mockRunQwenServe 时 options 对象不包含 memoryProjectScope

— qwen3.8-max via Qwen Code /review (v0.21.8)

@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.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): Context: this PR makes workspace-scoped project memory th...: none — all planned checks completed within budget.; Context: reverse audit round 2 for a review of PR #8856 (...: none — all checks above were completed within budget.; Context: reverse audit round 1 for a review of PR #8856 (...: none — all planned checks completed within budget..

中文说明

已审查。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):Context: this PR makes workspace-scoped project memory th...:none — all planned checks completed within budget.;Context: reverse audit round 2 for a review of PR #8856 (...:none — all checks above were completed within budget.;Context: reverse audit round 1 for a review of PR #8856 (...:none — all planned checks completed within budget.

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +39 to +41
// Project memory routing is frozen daemon-wide before workspace env files
// load, so only the operator's launch environment or CLI flag may set it.
'QWEN_CODE_MEMORY_PROJECT_SCOPE',

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.

[Suggestion] The new hardcoded exclusion silently rejects workspace .env/settings.env scope pins at every entry point (probe-verified at HEAD; reverting this hunk flips every observation). Two surfaces: (1) daemon — rejected entries are never reported (reportRejectedLoaderKeys filters on loader-class keys only, and every isHardcodedProjectEnvExclusion use site is a silent skip), so the boot log reads projectMemoryScopeSource=default while the on-disk workspace config says otherwise, and nothing names the ignored file entry; (2) standalone — the pin the pre-change docs recommended ("pin the scope in the workspace .env or settings.env", a sentence this PR removes from docs/users/qwen-serve.md) silently repartitions project memory back to git-root with no warning anywhere: the key is not loader-class, standalone has no boot log and no flag alternative, and the home-.env exemption does not mitigate — home settings.env is rejected unconditionally. docs/design/auto-memory/memory-system.md still presents the variable as a plain env override without stating the new restriction. — Failure scenario: an operator who pinned QWEN_CODE_MEMORY_PROJECT_SCOPE in a workspace env file per the pre-change docs upgrades → the daemon freezes workspace (boot log claims source=default against the on-disk config) and a standalone qwen in the same repo silently falls back to git-root partitioning → workspace-partitioned project memory becomes invisible with no migration and no diagnostic that names the ignored entry.

Suggested fix: emit a one-time rejection notice for hardcoded-excluded keys present in workspace env files (mirroring reportRejectedLoaderKeys, covering the standalone loaders as well, routed to the durable daemon log for serve-managed loads), and state in docs/design/auto-memory/memory-system.md that only the process launch environment (or --memory-project-scope for the daemon) may set this variable.

中文说明

新增的硬编码排除项会在所有入口静默拒绝 workspace .env/settings.env 中的 scope 固定(已在 HEAD 用探针验证;回滚本 hunk 后所有观测结果翻转)。两个层面:(1) daemon——被拒绝的条目不会有任何上报(reportRejectedLoaderKeys 只过滤 loader 类键,且每个 isHardcodedProjectEnvExclusion 使用点都是静默跳过),因此启动日志显示 projectMemoryScopeSource=default,与磁盘上的 workspace 配置矛盾,且没有任何日志指出被忽略的文件条目;(2) standalone——旧文档推荐的固定方式("把 scope 固定在 workspace 的 .envsettings.env",本 PR 已从 docs/users/qwen-serve.md 删除该句)会静默把项目记忆重新分区回 git-root,全程无告警:该键不是 loader 类,standalone 没有启动日志也没有等价的命令行参数,且 home .env 豁免并不足以补救——home settings.env 会被无条件拒绝。docs/design/auto-memory/memory-system.md 仍把该变量描述为普通环境变量覆盖,未说明新限制。——失败场景:按旧文档把 QWEN_CODE_MEMORY_PROJECT_SCOPE 固定在 workspace env 文件中的运维人员升级后 → daemon 冻结为 workspace(启动日志却声称 source=default,与磁盘配置矛盾),同一仓库下的 standalone qwen 静默回退到 git-root 分区 → 按 workspace 分区的项目记忆变得不可见,没有迁移,也没有任何指出被忽略条目的诊断。

建议修复:对 workspace env 文件中出现的硬编码排除键发出一次性拒绝通知(仿照 reportRejectedLoaderKeys,同时覆盖 standalone 加载器,serve 管理的加载路由到持久 daemon 日志),并在 docs/design/auto-memory/memory-system.md 中说明只有进程启动环境(或 daemon 的 --memory-project-scope)可以设置该变量。

— qwen3.8-max via Qwen Code /review (v0.21.9)

@wenshao

wenshao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 76 passed · 0 failed · 76 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:76 通过 · 0 失败 · 76 总计

Verification report

<!-- qwen-triage:verify -->
<!-- qwen-triage:verify-substantive -->

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - follow-up round at head c6ed41cf6c5109109380024a8e39161d0d9ae25d

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 76 passed · 0 failed · 76 total

中文 — 判定:✅ 通过 · 可合入(agent 判定,第二轮)
  • 结论merge-ready。76/76 脚本化断言通过;上一轮 3 个非阻塞发现中 2 个已修复(空/空白 env 不再冻结为原值;项目 .env/settings.env 不再能改写 scope),1 个按构造保留(非变更断言的单点盲区,套件整体仍能杀死该突变)。
  • A/B 结论:与上轮相同的 12 cell harness 在新 head 与 base 各跑一遍。差异 cell 从上轮的 1 个变为 6 个:unset(中心声明)、env-empty/env-blank(空白视为 unset → workspace)、env-file-override 及两个直接 buildRuntimeEnvironment 隔离 cell(项目文件尝试 git-root 被新硬排除挡住;base 上仍生效)。其余 6 个 cell 两臂逐字节一致,两臂均未改动启动环境。
  • 突变矩阵:9 个单点突变(含标签互换存活突变 M9)全部按预期:8 个被目标断言杀死,M9 存活并归类为覆盖缺口(projectMemoryScopeSourceoption/environment 标签无测试钉住,仅 default 被钉)。
  • 门控:run-qwen-serve 248/248、相邻三套件 130/130、fast-path 86/86、core 记忆 17/17、typecheck/eslint/prettier 全绿(lint 门以植入违规验证为活门)。
  • 未覆盖:逐 commit 归因(浅克隆仅可达 PR head);真实 ACP 会话内记忆读写端到端;createServeApp 嵌入路径;Windows/macOS。

Previous-round finding status (follow-up round)

Previous round verified head 31e3061d (first commit); the PR has since grown six commits (9a41641f, 71a7171c, 7a1946c6, 65139068, eb69b139, c6ed41cf). All carried-forward measurements were re-run at the new head — nothing was diffed off the old report.

# finding (round 1) severity status at c6ed41cf
1 Empty-string env frozen into the daemon base env ("" carried where base carried no key) nit fixed — re-measured: cell env-empty now yields workspace,workspace on head (base still freezes ""); new cell env-blank shows whitespace handled identically; the launch process.env itself is still untouched ("" preserved, not rewritten)
2 Non-mutation assertion blind spot: a mutant writing the same value back into process.env passes that one assertion note stands (by construction) — re-measured via M6: case preserves the launch environment scope stays GREEN under the process.env-mutating mutant, but the mutant is killed by 4 other assertions (default, option-beats-env, blank, whitespace), so the suite as a whole still pins non-mutation
3 Workspace settings env overlay wins over the daemon base env (a workspace .env/settings.env could set the scope) informational fixed — the key is now in PROJECT_ENV_HARDCODED_EXCLUSIONS; re-measured end-to-end: with a project .env containing QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root, base runtimes resolve git-root (overlay) while head runtimes stay workspace; direct buildRuntimeEnvironment cells show both the .env and settings.env paths blocked on head and applied on base

Central claim and A/B at the new head

Central claim (unchanged): a qwen serve daemon started with neither --memory-project-scope nor QWEN_CODE_MEMORY_PROJECT_SCOPE stamps QWEN_CODE_MEMORY_PROJECT_SCOPE=workspace into the frozen daemon base env, so every workspace runtime resolves project memory by exact workspace; precedence is option > non-blank launch env > default, the launch environment is never mutated, and the standalone CLI default (git-root) is untouched. Delta claims verified this round: blank/whitespace launch values are treated as unset (finding 1), the resolution is logged with source attribution, and the scope is operator-owned — project .env/settings.env cannot set it (finding 3).

The oracle is the per-runtime env.effectiveEnv observed through a real runQwenServe boot (real TCP listener, real baseEnv freeze, real workspace registry; telemetry/settings/trust/bridge mocked exactly as the repo's own test does), plus two direct buildRuntimeEnvironment cells that isolate the exclusion from the injection. The identical 12-cell probe ran on a HEAD^1 worktree and on head:

cell launch env option base effective (2 runtimes) head effective verdict
unset (unset), (unset) workspace, workspace DIFFERS — the change
env-git-root git-root git-root, git-root git-root, git-root identical
env-workspace workspace workspace, workspace workspace, workspace identical
env-empty "" "", "" workspace, workspace DIFFERS — finding 1 fixed
env-blank " " " ", " " workspace, workspace DIFFERS — new blank handling
env-upper WORKSPACE WORKSPACE, WORKSPACE WORKSPACE, WORKSPACE identical
env-unrecognized banana banana, banana banana, banana identical
option-beats-env workspace git-root git-root, git-root git-root, git-root identical
option-beats-env-reverse git-root workspace workspace, workspace workspace, workspace identical
env-file-override — (project .env tries git-root) git-root, git-root (overlay) workspace, workspace (rejected) DIFFERS — operator-owned
direct-env-file project .env tries git-root, no daemon git-root (applied) absent (excluded) DIFFERS — exclusion isolated
direct-settings-env settings.env tries git-root, no daemon git-root (applied) absent (excluded) DIFFERS — exclusion isolated

process.env after boot equals the launch value in all 10 boot records on both arms (no launch-env mutation). Witness: evidence/01-ab-probe-matrix-base-vs-head.png; raw records in logs/base-probe.jsonl / logs/head-probe.jsonl; 46 scripted assertions in assert-ab.mjs (evidence/03-scripted-assertions-ab.png).

Downstream semantics of the raw passthrough cells are core behavior, unchanged by this PR (git diff HEAD^1..HEAD -- packages/core package.json package-lock.json is empty): core's resolveWorkspaceProjectScope() trims/lowercases (WORKSPACE → workspace), warns once on unrecognized values, and returns false on unset — pinned by the unchanged core memory suite (17/17). The new daemon log line (project memory scope resolved with projectMemoryScope/projectMemoryScopeSource/projectMemoryScopeRaw) was observed live in head-arm boots and is pinned by the PR's logger test.

Mutation matrix (8 killed, 1 survivor classified)

Each mutant edits exactly one guard in a scratch worktree (tmp/mut-tree), runs the PR's targeted tests, and is reverted. Witness: evidence/02-mutation-matrix-all-mutants-killed.png; per-mutant junit archives in logs/mutant-*.junit.xml; 30 scripted assertions in assert-matrix.mjs (evidence/04-scripted-assertions-matrix.png).

mutant default-ws option-beats-env env-preserved blank whitespace unrecognized logger keys killed by
control (unmutated) GREEN GREEN GREEN GREEN GREEN GREEN GREEN GREEN
M1 revert hunk (= base) RED GREEN GREEN RED RED GREEN expected undefined to be 'workspace'
M2 precedence swap GREEN RED GREEN GREEN GREEN GREEN expected 'workspace' to be 'git-root'
M3 blank gate removed GREEN GREEN GREEN RED RED GREEN expected '' to be 'workspace'
M4 wrong default (git-root) RED GREEN GREEN RED RED GREEN expected 'git-root' to be 'workspace'
M5 writes normalized, not raw GREEN GREEN GREEN GREEN GREEN RED expected 'git-root' to be 'workspce'
M6 mutates process.env RED RED GREEN¹ RED RED GREEN 4 assertions
M7 log line removed GREEN GREEN GREEN GREEN GREEN GREEN RED toContain('project memory scope resolved')
M8 exclusion entry removed GREEN GREEN GREEN GREEN GREEN GREEN RED toContain('QWEN_CODE_MEMORY_PROJECT_SCOPE')
M9 source labels swapped GREEN GREEN GREEN GREEN GREEN GREEN GREEN² survives — coverage gap

¹ M6's env-preserved case staying green is the round-1 finding-2 blind spot, re-measured and deliberate: the mutant is still killed by the other four cases. ² M9 swaps the 'option'/'environment' values of projectMemoryScopeSource; the logger test asserts only projectMemoryScopeSource=default, so the swap survives. Every failure message above is an expected-vs-actual assertion (behavioural, not import/compile), and each mutant kills only its intended tests (no collateral failures in the 248-test suite).

Reviewer Test Plan walk-through

  1. Two dirs, no config → every runtime workspace: head probe cell unset = ["workspace","workspace"] over two registered workspaces; downstream keying pinned by the unchanged core suite.
  2. Launch env git-root, no option → legacy shared: cell env-git-root identical on both arms.
  3. Launch env workspace + --memory-project-scope git-root → option wins, env unmutated: cell option-beats-env effective git-root with processEnvAfter still workspace on both arms.
  4. Standalone CLI keeps git-root default: core + lockfile diffs empty; core returns false on unset env; core memory suite 17/17.

Findings (non-blocking)

  1. Coverage gap (M9): the option/environment source labels of the new diagnostic are unpinned. The logger test asserts projectMemoryScopeSource=default only; swapping the two non-default labels survives every test (measured). The values (workspace/git-root, raw) are fully pinned, so this is a diagnostics-only gap. Completeness reporting, not a merge condition.
  2. Round-1 finding 2 stands by construction (see matrix footnote ¹): the non-mutation assertion alone cannot see a same-value write-back; the suite as a whole pins non-mutation (M6 killed 4 ways).
  3. Raw passthrough of unrecognized/upper-case launch values remains frozen into the base env (banana, WORKSPACE) — identical on both arms, normalized downstream in core with a one-time warning, and now observable in the daemon log via projectMemoryScopeRaw vs projectMemoryScope. Residual by design; noted so a future reader knows the env value is the operator's raw string, not the resolved scope.

Not covered

  • Per-commit attribution: the checkout is depth 2 (git rev-list HEAD^1..HEAD^2 returns 1 while the metadata lists 7 commits — the shallow-boundary artifact). All evidence is for the aggregate HEAD^1..HEAD diff; the six new commits were not individually exercised.
  • Live-session memory operations: verified at the injection seam (effectiveEnv children spawn with) and via the unchanged core suite; no real ACP session drove remember/forget across two workspaces.
  • createServeApp embedded path: the default injection provably lives only in runQwenServeImpl (grep: server.ts never reads memoryProjectScope), matching the new types.ts doc; the embedded path itself was not booted.
  • Windows/macOS: container is Linux; the change is env-string semantics with no platform branch.
  • Repo-wide gates beyond the affected suites (the PR's own CI covers them).
  • Base-arm control setup: the base worktree needed the head tree's nested third-party node_modules (ajv v8 vs root v6) symlinked in and one gitignored generated file copied (src/generated/git-commit.ts); both are lockfile-pinned / inert data, and the code under test resolves from each tree's own source (vitest aliases core to the tree's own ../core), so the control stays clean.

Methodology

Environment: CI verify container (node:22-bookworm), merge-ref checkout at depth 2 (HEAD^1 = base tip bdb7e418ba, HEAD^2 = verified head c6ed41cf); npm ci + build pre-run at head. A/B used scratch git worktrees under tmp/ (removed after capture); harnesses (pr8856-ab-probe.test.ts, assert-ab.mjs, apply-mutant.mjs, run-mutation-matrix.sh, assert-matrix.mjs, renderers) live in the artifact dir and are re-runnable. Gates at head: run-qwen-serve.test.ts 248/248, serve.test.ts+shared-env-keys.test.ts+environment.test.ts 130/130 (evidence/05-gates-adjacent-suites-130-green.png), fast-path.test.ts 86/86, core store.test.ts 17/17, repo typecheck green, focused ESLint green (liveness proven by planted violation → exit 1), Prettier green. Raw logs in logs/.

Qwen Code · sandboxed verification

Evidence images

01-ab-probe-matrix-base-vs-head

02-mutation-matrix-all-mutants-killed

03-scripted-assertions-ab

04-scripted-assertions-matrix

05-gates-adjacent-suites-130-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on c6ed41cf6c5109109380024a8e39161d0d9ae25d is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 c6ed41cf6c5109109380024a8e39161d0d9ae25d 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

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

上方各阶段评论已更新为最新结果。查看工作流运行

wenshao added a commit that referenced this pull request Aug 11, 2026
@wenshao

wenshao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Local real-stack verification — PR #8856

I rebuilt this PR locally and exercised it on a real qwen serve daemon (real HTTP API, real ACP runtime child processes, real managed-memory remember agent, real filesystem) rather than unit-test doubles, and ran the merge-base as a paired control for every scenario.

Verdict: behaves exactly as the PR describes. Recommend merge. Three non-blocking notes at the bottom.


How it was verified

PR head c6ed41cf6c
Control (merge-base) 5dc98240c7
Fixture one git repo, two nested workspaces repo/alpha + repo/beta, both registered via --workspace
Isolation dedicated QWEN_HOME per scenario, so projects/<sanitizeCwd(key)>/memory is directly observable
Model local OpenAI-compatible recording server (deterministic, offline)
Platform macOS 26.6 (arm64), Node 24.18.1

Four independent observations per scenario, so no single instrument can carry the result on its own:

  1. Daemon log — the new project memory scope resolved line (projectMemoryScope, …Source, …Raw).
  2. Runtime child environment — a real run_shell_command executed inside each ACP runtime, printing its own QWEN_CODE_MEMORY_PROJECT_SCOPE. (macOS blocks ps eww env dumps, so this is the only honest way to read the child's env.)
  3. Session system prompt — the project-memory root each ordinary session is actually told about, captured from the recorded model request.
  4. Real writes on diskPOST /workspaces/:ws/memory/remember runs the real managed-memory agent; the resulting filesTouched and the files themselves show where memory landed.

1. The headline change — default scope

default scope before/after

With nothing configured, the merge-base puts both workspaces into one git-root-keyed store; the PR head gives each workspace its own. Confirmed by all four probes simultaneously.

2. Full precedence matrix

scenario matrix

# daemon configuration before after
1 nothing set shared (git-root) isolated (per workspace)
2 QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root shared shared — source=environment
3 env workspace + --memory-project-scope git-root shared shared — source=option (CLI wins)
4 env " " (blank) isolated — treated as unset, source=default
5 beta/.env sets workspace git-root + beta → split isolated (consistent)
6 beta/.env sets git-root isolated (.env ignored)
7 env workspce (typo) shared (git-root) + warning
8 standalone qwen, no daemon shared shared (unchanged)

Rows 2/3 confirm the stated precedence; row 8 confirms the standalone CLI default is untouched. In every scenario process.env of the daemon itself was never mutated — only baseEnv.

3. Upgrade impact, measured rather than assumed

migration

I seeded a project memory under the legacy git-root key before boot (marker PR8856-LEGACY-MEMORY) and counted how many model prompts actually contained it:

build prompts containing the legacy memory
merge-base, no flag 6
PR head, no flag (new default) 0
PR head, --memory-project-scope git-root 6

So the breaking change is exactly as documented: the legacy store is never deleted, only unreferenced, and the documented opt-out restores it verbatim.

4. The scope key is now operator-owned

project .env

On the merge-base, a repo-supplied beta/.env containing QWEN_CODE_MEMORY_PROJECT_SCOPE=workspace wins: the runtimes report ["<unset>", "workspace"] and the two workspaces silently disagree about their own memory scope. On the PR head both runtimes report workspace — the key is in PROJECT_ENV_HARDCODED_EXCLUSIONS and the daemon-wide value is always present, so a project .env can no longer move it. Good defence in depth.


Tests, lint, types

  • packages/cli, run from packages/cli: run-qwen-serve.test.ts + shared-env-keys.test.ts + serve.test.ts348 passed / 348 on the PR head. Merge-base with its own tests: 342 passed / 342.
  • Load-bearing check — the PR's test files applied to merge-base source: 7 failed / 348, including exactly the intended assertions:
    • keeps daemon memory scope operator-ownedexpected [...] to include 'QWEN_CODE_MEMORY_PROJECT_SCOPE'
    • defaults every runtime to workspace project-memory scopeexpected undefined to be 'workspace'
    • treats a blank / whitespace-only launch environment scope as unset
    • creates a daemon log file at boot…expected … to contain 'project memory scope resolved'
  • prettier --check and eslint on all changed files: clean. tsc --noEmit on packages/cli: clean.
  • Docs re-read against observed behaviour — 17-configuration.md, qwen-serve.md, auto-memory/memory-system.md all match, including the blank-value and unrecognized-value wording. serve --help renders the new text and yargs choices still rejects invalid flag values. No stale references to the old default remain anywhere in docs/.

Non-blocking notes

  1. A typo in the launch env silently downgrades isolation. QWEN_CODE_MEMORY_PROJECT_SCOPE=workspce resolves to git-root (verified: daemon logs projectMemoryScope=git-root projectMemoryScopeRaw=workspce source=environment, and each runtime prints the one-shot warning). That fallback predates this PR, but its meaning changed: before, an unrecognized value landed on the default; now it lands on the non-default legacy scope. Worth considering whether the unrecognized-value fallback should follow the new default, or fail fast at boot. The CLI flag is safe — yargs choices rejects it.
  2. Consider a one-time boot notice when workspace scope is active and a non-empty git-root memory store already exists for the same repo. The upgrade is silent from the user's side: memory simply looks empty, with nothing pointing at --memory-project-scope git-root.
  3. Harness quirk, not this PR: packages/cli/src/commands/serve.test.ts:838 derives the repo root as path.resolve(process.cwd(), '../..'), so serve startup import boundary only passes when vitest runs from packages/cli. It fails identically on the merge-base.

Scope of this verification

macOS only; the model was a deterministic local stub, so no real-provider behaviour is covered. Windows and Linux remain unverified, matching the PR's own claim table.

中文版

PR #8856 本地真实环境验证

我在本地重新构建了这个 PR,并在真实的 qwen serve daemon 上做了验证(真实 HTTP API、真实 ACP runtime 子进程、真实 managed-memory remember agent、真实文件系统),不是单测替身;每个场景都用 merge-base 跑了成对的对照组。

结论:行为与 PR 描述完全一致,建议合入。 文末有三条不阻塞的建议。

验证方式

PR head c6ed41cf6c
对照(merge-base) 5dc98240c7
Fixture 一个 git 仓库,两个嵌套 workspace repo/alpha + repo/beta,都通过 --workspace 注册
隔离 每个场景独立 QWEN_HOME,可以直接观察 projects/<sanitizeCwd(key)>/memory
模型 本地 OpenAI 兼容的记录型服务(确定性、离线)
平台 macOS 26.6(arm64),Node 24.18.1

每个场景取四个互相独立的判据,避免单一观测口径决定结论:

  1. daemon 日志 —— 本 PR 新增的 project memory scope resolved 行(projectMemoryScope / …Source / …Raw)。
  2. runtime 子进程环境 —— 在每个 ACP runtime 内部真实执行 run_shell_command,打印它自己的 QWEN_CODE_MEMORY_PROJECT_SCOPE。(macOS 禁止 ps eww 导出环境变量,这是唯一诚实的读法。)
  3. 会话 system prompt —— 从记录到的模型请求里取出普通会话被告知的项目记忆根目录。
  4. 磁盘上的真实写入 —— POST /workspaces/:ws/memory/remember 会跑真实的 managed-memory agent,返回的 filesTouched 和落盘文件直接证明记忆写到了哪里。

1. 核心改动:默认 scope

merge-base 在无配置时把两个 workspace 归到同一个 git-root key 的存储;PR head 让每个 workspace 拥有各自独立的存储。四个判据同时确认。

2. 完整优先级矩阵

# daemon 配置 改动前 改动后
1 什么都不设 共享(git-root) 隔离(按 workspace)
2 QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root 共享 共享 —— source=environment
3 env workspace + --memory-project-scope git-root 共享 共享 —— source=option(CLI 胜出)
4 env " "(空白) 隔离 —— 视作未设置,source=default
5 beta/.envworkspace git-root + beta → 分裂 隔离(一致)
6 beta/.envgit-root 隔离(.env 被忽略)
7 env workspce(拼写错误) 共享(git-root)+ 告警
8 standalone qwen,无 daemon 共享 共享(未变

第 2、3 行确认了声明的优先级;第 8 行确认 standalone CLI 默认值没被动。所有场景中 daemon 自身的 process.env 都没有被修改,只改了 baseEnv

3. 升级影响:实测而非推断

启动前我在旧的 git-root key 下预置了一条项目记忆(标记 PR8856-LEGACY-MEMORY),然后统计有多少条模型请求真的带上了它:

构建 含旧记忆的模型请求数
merge-base,无参数 6
PR head,无参数(新默认) 0
PR head,--memory-project-scope git-root 6

破坏性变化与文档描述完全一致:旧存储不会被删除,只是不再被引用;文档给出的回退开关能原样恢复。

4. scope 变成 operator 独占

在 merge-base 上,仓库自带的 beta/.env 写入 QWEN_CODE_MEMORY_PROJECT_SCOPE=workspace生效:两个 runtime 分别报告 ["<unset>", "workspace"],两个 workspace 对自己的记忆 scope 悄悄产生了分歧。PR head 上两个 runtime 都报告 workspace —— 该 key 既进了 PROJECT_ENV_HARDCODED_EXCLUSIONS,daemon 级取值也总是存在,项目 .env 再也动不了它。这层纵深防御是加分项。

测试、lint、类型

  • packages/cli 目录下跑 run-qwen-serve.test.ts + shared-env-keys.test.ts + serve.test.ts:PR head 348 通过 / 348;merge-base 用自己的测试 342 通过 / 342。
  • 测试是否吃紧 —— 把 PR 的测试文件套到 merge-base 源码 上:7 失败 / 348,正好是这些断言:
    • keeps daemon memory scope operator-ownedexpected [...] to include 'QWEN_CODE_MEMORY_PROJECT_SCOPE'
    • defaults every runtime to workspace project-memory scopeexpected undefined to be 'workspace'
    • treats a blank / whitespace-only launch environment scope as unset
    • creates a daemon log file at boot…expected … to contain 'project memory scope resolved'
  • 改动文件的 prettier --checkeslint 全部干净;packages/clitsc --noEmit 干净。
  • 文档逐条对照实测行为 —— 17-configuration.mdqwen-serve.mdauto-memory/memory-system.md 都对得上,包括空白值和无法识别取值的措辞。serve --help 渲染了新文案,yargs choices 仍然拒绝非法的 flag 取值。docs/ 里没有残留旧默认值的说法。

不阻塞的建议

  1. 启动环境变量拼错会静默地把隔离降级。 QWEN_CODE_MEMORY_PROJECT_SCOPE=workspce 会落到 git-root(已实测:daemon 打出 projectMemoryScope=git-root projectMemoryScopeRaw=workspce source=environment,每个 runtime 也各打一次告警)。这个回退逻辑早于本 PR,但它的含义变了:以前无法识别的值落到默认值上,现在落到默认的旧 scope 上。可以考虑让无法识别的取值跟随新默认值,或者启动时直接失败。CLI flag 是安全的 —— yargs choices 会拦住。
  2. 可以考虑加一条一次性的启动提示:当 workspace scope 生效、且同一仓库在 git-root key 下已存在非空记忆时提示一句。从用户视角看这次升级是无声的:记忆看起来就是空了,也没有任何东西指向 --memory-project-scope git-root
  3. 测试脚手架的小问题,与本 PR 无关packages/cli/src/commands/serve.test.ts:838path.resolve(process.cwd(), '../..') 推导仓库根目录,所以 serve startup import boundary 只有在 packages/cli 目录下跑 vitest 时才通过。merge-base 上同样失败。

本次验证的边界

仅覆盖 macOS;模型是确定性的本地桩,未覆盖真实 provider 行为。Windows 与 Linux 仍未验证,与 PR 自己的声明表一致。

@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.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the unit suite was not run on Windows locally (the platform relevant to the Needs Human Review env-case finding).

Not explored to full depth (tool budget reached): Context: this PR makes workspace-scoped project memory th...: did not conclusively trace whether ACP child stderr is captured into the durable daemon log (checked packages/cli/src spawn/stderr paths only, no pipe found) …; Context: reverse audit round 2 for a review of PR #8856 (...: none (finished well under the tool ceiling)..

中文说明

已审查。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the unit suite was not run on Windows locally (the platform relevant to the Needs Human Review env-case finding)。

未探索到全部深度(达到工具调用预算):Context: this PR makes workspace-scoped project memory th...:did not conclusively trace whether ACP child stderr is captured into the durable daemon log (checked packages/cli/src spawn/stderr paths only, no pipe found) …;Context: reverse audit round 2 for a review of PR #8856 (...:none (finished well under the tool ceiling).

— qwen3.8-max via Qwen Code /review (v0.21.9)

@wenshao

wenshao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge August 11, 2026 10:58
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 82 passed · 0 failed · 82 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:82 通过 · 0 失败 · 82 总计

Verification report

Sandboxed verification: ✅ passed — merge-ready (agent verdict) — follow-up round 3 at head a520f352e2e891869fcd24d1468dd9d04819a4b7

Scripted assertions: 82 passed · 0 failed · 82 total (58 A/B + 24 mutation matrix)

中文 — 判定:✅ 通过 · 可合入(agent 判定,第三轮)
  • 结论merge-ready。82/82 脚本化断言全部通过。本轮唯一增量是把上游 main 合入 PR 分支(commit a520f352);PR 自身的 10 个文件与上轮 head c6ed41cf 逐字节一致(git diff 为 0 行)。因依赖闭包随 main 合并而变化(lockfile ±60 行),所有沿用的测量均在新 head 完整重跑,未做任何旧报告 diff 推断。
  • A/B 结论:与上轮完全相同的 12-cell harness 在新 head 与 base 各重跑一遍。差异 cell 仍为 6 个:unset(中心声明)、env-empty/env-blank(空白视为 unset → workspace)、env-file-override 与两个 buildRuntimeEnvironment 直接隔离 cell(项目 .env/settings.env 无法覆写 scope);其余 6 个 cell 两臂逐字节一致,两臂均未改动启动环境。58/58 断言通过。
  • 突变矩阵:control + 9 个单点突变全部按预期——8 个被目标断言逐一杀死(每个突变只红其预期测试,293-test 普查数不变,无连带失败),M9(option/environment 标签互换)存活,归类为诊断标签覆盖缺口;M1(还原为 base)作为阳性对照以行为性消息失败(expected undefined to be 'workspace')。
  • 上轮发现复核:M9 覆盖缺口仍在(非阻塞);M6 同值写回单断言盲区按构造保留(套件整体仍 4 路杀死该突变);未识别/大写值原样冻结为设计内残留(两臂一致,下游由未改动的 core 归一化)。上轮标记“已修复”的两项(空白 env、项目 env 文件排除)重测仍为已修复。
  • 门控:run-qwen-serve 248/248、相邻三套件 130/130、fast-path 86/86、core store 17/17、typecheck/eslint/prettier 全绿(lint 以植入违规 exit 1 验证为活门)。
  • 未覆盖:逐 commit 归因(浅克隆)、真实 ACP 会话内记忆读写端到端、createServeApp 嵌入路径、Windows/macOS、上游 main 增量自身的验证(由 main 的 CI 覆盖)。

Previous-round finding status (follow-up round)

Round 2 verified head c6ed41cf; the PR has since grown exactly one commit — a520f352, a merge of upstream main into the branch. The PR's own 10 files are byte-identical between the two heads (git diff c6ed41cf..HEAD^2 over those paths = 0 lines). The merge did move the dependency closure (package-lock.json ±60 lines, packages/cli/package.json, packages/core/package.json version bumps), so the input-closure shortcut does not apply and every carried measurement was re-run at the new head — nothing was diffed off the old report.

# finding (round 2) severity status at a520f352
1 Coverage gap (M9): option/environment source labels of the diagnostic unpinned — swapping them survives every test non-blocking stands — re-measured: M9 exits green over all 10 targeted tests; only projectMemoryScopeSource=default is pinned by the logger test. The values (workspace/git-root, raw) remain fully pinned
2 Non-mutation blind spot: a same-value write-back into process.env is invisible to the single preserves… assertion note stands (by construction) — re-measured via M6's red set: preserves the launch environment scope when the option is omitted is the one scope case NOT red under M6; the mutant is still killed by the other four cases
3 Raw passthrough of unrecognized/upper-case launch values frozen into the base env (banana, WORKSPACE) informational stands (residual by design) — re-measured: cells env-upper/env-unrecognized byte-identical on both arms; downstream trim/lowercase/one-time-warning is unchanged core (git diff HEAD^1..HEAD -- packages/core empty; core store suite 17/17)
Round-2 "fixed" rows: blank/empty launch env no longer frozen (R1-F1); project .env/settings.env excluded from setting the scope (R1-F3) still fixed — re-measured: cells env-empty, env-blank, env-file-override, direct-env-file, direct-settings-env all reproduce the fixed behavior at the new head

Central claim and A/B at the new head

Central claim (unchanged): a qwen serve daemon started with neither --memory-project-scope nor QWEN_CODE_MEMORY_PROJECT_SCOPE stamps QWEN_CODE_MEMORY_PROJECT_SCOPE=workspace into the frozen daemon base env, so every workspace runtime resolves project memory by exact workspace; precedence is option > non-blank launch env > default; the launch environment is never mutated; project env files cannot set the scope; the standalone CLI default (git-root) is untouched. This round's delta question: does the upstream-main merge change any of that — answered no by re-running the identical 12-cell probe on both arms.

The oracle is the per-runtime env.effectiveEnv observed through a real runQwenServe boot (real TCP listener, real baseEnv freeze, real workspace registry; telemetry/settings/trust/bridge mocked exactly as the repo's own scope test does), plus two direct buildRuntimeEnvironment cells that isolate the exclusion from the injection. The probe (zz-pr8856-ab-probe.test.ts, copied byte-identical into both trees — sha256-matched) ran on a HEAD^1 worktree and on head:

cell launch env option base effective (2 runtimes) head effective verdict
unset (unset), (unset) workspace, workspace DIFFERS — the change
env-git-root git-root git-root, git-root git-root, git-root identical
env-workspace workspace workspace, workspace workspace, workspace identical
env-empty "" "", "" workspace, workspace DIFFERS — blank-as-unset
env-blank " " " ", " " workspace, workspace DIFFERS — blank-as-unset
env-upper WORKSPACE WORKSPACE, WORKSPACE WORKSPACE, WORKSPACE identical
env-unrecognized banana banana, banana banana, banana identical
option-beats-env workspace git-root git-root, git-root git-root, git-root identical
option-beats-env-reverse git-root workspace workspace, workspace workspace, workspace identical
env-file-override — (project .env tries git-root) git-root, git-root (overlay) workspace, workspace (rejected) DIFFERS — operator-owned
direct-env-file project .env tries git-root, no daemon git-root (applied) absent (excluded) DIFFERS — exclusion isolated
direct-settings-env settings.env tries git-root, no daemon git-root (applied) absent (excluded) DIFFERS — exclusion isolated

processEnvAfter equals the launch value in all 10 boot records on both arms — no launch-env mutation. Witness: evidence/01-ab-probe-matrix-base-vs-head.png (the full 58-assertion run); raw records in logs/base-probe.jsonl / logs/head-probe.jsonl; scripted expectations in assert-ab.mjs (58 assertions: per-cell scope tuples on both arms, post-boot launch-env equality, cross-arm identical/differs classification, record census).

Reviewer Test Plan walk-through: (1) two registered dirs, no config → head cell unset = workspace on both runtimes; (2) launch env git-root, no option → cell env-git-root identical on both arms; (3) launch env workspace + option git-root → option wins with processEnvAfter still workspace; (4) standalone CLI → git diff HEAD^1..HEAD -- packages/core package.json package-lock.json is empty and the core store suite (17/17) pins the unchanged downstream resolution. All four steps held.

Mutation matrix (8 killed, 1 survivor re-classified)

Each mutant edits exactly one guard in a scratch worktree at HEAD, runs the 10 targeted tests (the 6 scope cases, the daemon-logger test, keeps daemon memory scope operator-owned, plus the two pre-existing siblings the name filter also matches: rejects invalid memoryProjectScope=unsupported… and keeps ACP repeated-tool-failure rollout policy operator-owned), and is reverted (dirty-files=0 after every run). Census check: every run reports the same 293-test total — no collateral. Witness: evidence/02-mutation-matrix-8-killed-1-survivor.png; per-mutant junit in logs/mutant-*.junit.xml and stdout in logs/mutant-*.log; 24 scripted assertions in assert-matrix.mjs.

mutant red tests (exact) status
control (unmutated) — (10/10 green) green
M1 revert both hunks (= base) default-ws, blank, whitespace, logger test killed — positive control, behavioural messages: expected undefined to be 'workspace', to contain 'project memory scope resolved'
M2 precedence swap (env over option) applies-without-mutating killed
M3 blank gate removed blank, whitespace killed
M4 wrong default (git-root) default-ws, blank, whitespace, logger test killed
M5 writes normalized, not raw unrecognized passthrough (expected 'git-root' to be 'workspce' shape) killed
M6 mutates process.env default-ws, applies-without-mutating, blank, whitespace — not preserves… (the by-construction blind spot) killed 4 ways
M7 log line removed logger test killed
M8 exclusion entry removed keeps daemon memory scope operator-owned killed
M9 source labels swapped — (10/10 green) survives — coverage gap (diagnostics labels only; values pinned)

Findings (non-blocking, carried over — all re-measured)

  1. M9 coverage gap stands: the logger test asserts only projectMemoryScopeSource=default; swapping the 'option'/'environment' literals survives all 10 targeted tests (measured, exit 0). Scope values and raw passthrough are fully pinned, so this is diagnostics-only. Completeness reporting, not a merge condition.
  2. Round-1 blind spot stands by construction: the single preserves the launch environment scope assertion cannot see a same-value write-back; it stayed green under M6 (absent from M6's red set, re-measured), while four sibling assertions kill the mutant.
  3. Raw-value freeze remains by design: WORKSPACE/banana are frozen verbatim into the daemon base env on both arms; core normalizes downstream with a one-time warning, and the new daemon log line carries projectMemoryScopeRaw vs projectMemoryScope for observability. Noted so a future reader knows the env value is the operator's raw string, not the resolved scope.

No new findings this round: the merge commit touched none of the PR's surface, and no regressions appeared in any re-measurement.

Not covered

  • Per-commit attribution: the checkout is depth 2 — git rev-list HEAD^1..HEAD^2 returns 1 while the metadata lists 8 commits (shallow-boundary artifact; git rev-parse --is-shallow-repository = true). All evidence is for the aggregate HEAD^1..HEAD diff.
  • Live-session memory operations: verified at the injection seam (effectiveEnv children spawn with) and via the unchanged core suite; no real ACP session drove remember/forget across two workspaces (same boundary as rounds 1–2).
  • createServeApp embedded path: the default injection lives only in runQwenServeImpl (no memoryProjectScope reads in server.ts), matching the types.ts doc; the embedded path itself was not booted.
  • Windows/macOS: container is Linux; the change is env-string semantics with no platform branch.
  • The upstream-main delta itself (review toolchain adapters, InputPrompt, etc.): verified only non-intersection with this PR's surface plus re-running the affected suites on the merged tree; those areas are main's own and covered by main's CI.
  • Repo-wide gates beyond the affected suites (the PR's own CI covers them).

Methodology

Environment: CI verify container (node:22-bookworm, node v22.23.2), merge-ref checkout at depth 2 (HEAD^1 = base tip 9946d13a35, HEAD^2 = verified head a520f352); npm ci + npm run build pre-run at head. A/B and mutation work used scratch git worktrees under tmp/ (removed after capture, main tree left clean). Base/mutant trees were wired to the already-installed root node_modules via symlinks; from the base tree, readlink -f node_modules/@qwen-code/qwen-code-core resolves into the head tree — harmless here and asserted as such because (a) git diff HEAD^1..HEAD -- packages/core is empty, so core is byte-identical across arms, and (b) packages/cli's vitest config aliases @qwen-code/qwen-code-core to the tree-local ../core/index.ts. The base arm genuinely ran base code: its probe produced base semantics ((unset), ""-freeze, .env overlay) that the head source cannot produce. Each scratch tree additionally needed the nested packages/core/node_modules (ajv v8; the first matrix attempt failed collection on ajv/dist/2020.js until it was linked) and the gitignored generated src/generated/git-commit.ts copied in — both lockfile-pinned/inert. Gates at head: run-qwen-serve.test.ts 248/248, serve.test.ts+shared-env-keys.test.ts+environment.test.ts 130/130, fast-path.test.ts 86/86, core store.test.ts 17/17, repo typecheck exit 0, focused ESLint exit 0 (liveness proven: planted unused var → @typescript-eslint/no-unused-vars, exit 1, removed), Prettier exit 0 on all 10 changed files. Harnesses (zz-pr8856-ab-probe.test.ts, assert-ab.mjs, apply-mutant.mjs, run-mutation-matrix.sh, assert-matrix.mjs) live in this artifact dir and are re-runnable; raw logs in logs/.

Qwen Code · sandboxed verification

Evidence images

01-ab-probe-matrix-base-vs-head

02-mutation-matrix-8-killed-1-survivor

03-gates-all-suites-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on a520f352e2e891869fcd24d1468dd9d04819a4b7 is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 a520f352e2e891869fcd24d1468dd9d04819a4b7 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

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

上方各阶段评论已更新为最新结果。查看工作流运行

@wenshao
wenshao added this pull request to the merge queue Aug 12, 2026
Merged via the queue into QwenLM:main with commit de48637 Aug 12, 2026
39 of 40 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.11.

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

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(serve): make workspace-scoped project memory the daemon default

4 participants