Skip to content

fix(web-shell): make the workspace provider guard self-diagnosing and reload on root retry - #11421

Merged
wenshao merged 7 commits into
mainfrom
fix/web-shell-workspace-guard-diagnostics
Sep 10, 2026
Merged

wenshao merged 7 commits into
mainfrom
fix/web-shell-workspace-guard-diagnostics

Conversation

@wenshao

@wenshao wenshao commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Two changes to the standalone Web Shell's failure surface. First, the strict useDaemonWorkspace guard now reports which situation it detected, using a small diagnostic registry (keyed on globalThis, never used to share context): each copy of the provider module records a URL-bearing id and whether it rendered with a live client, so the error distinguishes "no provider has rendered" / "a provider rendered from a different module copy (duplicate module instances, both ids included)" / "this copy rendered but the consumer is outside its live subtree" / "this copy rendered without an active client (autoConnect={false})". The registry is capped so dev hot re-evaluation cannot grow it unbounded. Second, the standalone root error boundary's retry now reloads the page — but only when a reload cannot strand a credential: when no token was resolved at boot (tokenless trusted loopback) or when a token survives in the URL / per-tab storage (a new hasReloadSurvivableDaemonToken(), sharing one URL-grammar parse with getDaemonToken()); otherwise it falls back to the previous in-place boundary reset, The reload carries the live theme/language into the URL first (session switches strip those one-shot params), and the button label matches the action ("Reload page" / "重新加载" vs "Try again" / "重试").

Why it's needed

Observed occurrence (the reason this PR exists): in a dev session (vite dev server serving the standalone shell against qwen serve), the root boundary showed this guard's error three times in one morning; an in-place retry could not help and a manual page reload recovered each time. The raw guard message being visible on screen pins a DEV build, and static analysis shows the guard cannot fire in the standalone tree while a single module graph is coherent — the provider wraps the whole app and always produces a context value — which points at a duplicated DaemonWorkspaceContext (two copies of the provider module live in one page). No console component stack was captured, so the mechanism is inferred, not proven; the diagnostic branch exists precisely to confirm or refute it on the next occurrence instead of guessing again. The reload-on-retry half exists because boundary reset re-mounts the same broken module graph and throws again — reload was the only recovery that worked. Review then found the unconditional version could strand the shell unauthenticated (token only in memory, storage persist failed, URL token stripped at boot), so the reload is now gated on token survivability; the in-memory-token case keeps the old re-mount behavior, which does recover.

Reviewer Test Plan

How to verify

Run the touched unit suites from packages/web-shell: npx vitest run client/daemon/workspace/DaemonWorkspaceProvider.test.tsx client/main.test.tsx client/components/RootErrorFallback.test.tsx client/config/daemon.test.ts (62 tests, all green locally). The provider suite covers every guard branch (no provider / foreign copy / same copy but outside / rendered-without-client / duplicate takes precedence when both copies rendered / registry dedup across recomputes / registry cap); main.test.tsx covers retry-reloads-when-token-survives and retry-resets-in-place-when-not (asserting reload was not called and the tree re-mounted); daemon.test.ts covers hasReloadSurvivableDaemonToken across hash/query/persisted/empty/throwing-storage. Mutation checks performed locally: removing the reload-survivability guard, the label plumbing, the shared harness's try/catch, the URL-bearing id, the helper-local root (with an afterEach leak probe), the foreign-copy precedence, or the registry cap each turns the corresponding test red; reverting restores green. Reviewers can also confirm the guard's message prefix is unchanged, so the existing substring assertions and the build-artifact.test.ts checks still hold (transcript JS measured at 1,180,562 bytes, under the 1,300,000 ceiling; import.meta.url survives the ES lib build).

Evidence (Before & After)

N/A for visuals (no layout change; the fallback looks identical apart from the retry label when it reloads). Behavior evidence: the observed failure was "root fallback with this guard's message, recoverable only by reload", seen 3x in one dev session; after this PR the same screen's retry performs that recovery itself when safe, and the error message now names the failure class.

Tested on

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

Environment (optional)

npx vitest run under packages/web-shell; eslint/prettier clean on the changed files; vite build --config vite.lib.config.ts --mode transcript succeeds with the guard intact in the bundle. (The standalone vite build in this worktree is blocked by a pre-existing missing @tanstack/react-table install, unrelated to this diff.)

Risk & Scope

  • Main risk or tradeoff: the retry button's behavior now depends on token survivability — in the no-survivable-token case the user gets the old in-place retry, which cannot recover from module-graph-rooted crashes; accepted, because reloading there would lose authentication entirely. The breadcrumb registry lives on globalThis and is diagnostic-only (context values are never shared through it), bounded to 8 entries.
  • Not validated / out of scope: the root cause of the dev-session module duplication is not fixed here (this PR makes it diagnosable and one-click recoverable); WebShellTranscript's embedded boundary intentionally keeps its in-place retry; the chrome-extension iframe path depends on the sidepanel re-posting the token on iframe load and takes the reset path.
  • Breaking changes / migration notes: none.

Linked Issues

References #11100 (the strict-guard failure class in provider-less transcript rendering; not closed by this PR — its "no provider has rendered" branch is exactly what that issue's export-document scenario would hit).

中文说明

这个 PR 做了什么

对独立 Web Shell 的失败兜底做了两处改动。第一,严格的 useDaemonWorkspace 守卫现在会报告它命中的具体情形:通过一个小型诊断注册表(挂在 globalThis 上,绝不用于共享 context),Provider 模块的每个副本记录一个带 URL 的 id 以及它是否带着可用 client 渲染过,从而让错误信息区分「从未渲染过 Provider」/「Provider 来自另一个模块副本(模块双实例,消息带两个 id)」/「本副本渲染过但消费者在其存活子树之外」/「本副本渲染了但没有活跃 client(autoConnect={false})」。注册表有上限,dev 热重载不会让它无限增长。第二,独立版根错误边界的「重试」现在会刷新页面——但只在刷新不可能丢失凭证的前提下(启动时未解析到 token 的免认证回环场景,或 token 存在于 URL / 按标签页持久化存储中——由新的 hasReloadSurvivableDaemonToken() 判定,与 getDaemonToken() 共享同一份 URL 解析);否则回退为原来的原地边界重置。刷新前会把当前的 theme/language 写回 URL(会话切换会把这些一次性参数抹掉),且按钮文案与行为一致(「Reload page」/「重新加载」 对 「Try again」/「重试」)。

为什么需要

真实发生过的现场(本 PR 的起因):在一个 dev 会话里(vite dev server 提供独立 shell、代理到 qwen serve),根边界一个上午出现了三次这个守卫的报错;原地重试无效,每次只能手动刷新页面恢复。屏幕上能直接看到守卫原文这一点锁定了当时是 DEV 构建;而静态分析表明,在模块图一致的独立应用树里这个守卫不可能触发——Provider 包着整个应用且总是产出 context 值——因此指向 DaemonWorkspaceContext 双实例(同一页面里存在两份 Provider 模块)。当时没有抓到 console 的 component stack,所以这个机制是推断而非已证实;诊断分支的意义正在于下次发生时直接证实或证伪,而不是再猜一次。重试即刷新那一半存在的原因是:边界重置只会重新挂载同一副坏掉的模块图并再次抛错——刷新是唯一有效的恢复手段。评审随后发现无条件刷新在 token 只存在于内存(持久化失败、URL token 启动时被抹掉)时会让 shell 永久失去认证,因此刷新现在以 token 可存活为前提;内存 token 的场景保留原来的重新挂载行为,那条路径本来就能恢复。

Reviewer 测试计划

如何验证

packages/web-shell 下运行受影响的单测:npx vitest run client/daemon/workspace/DaemonWorkspaceProvider.test.tsx client/main.test.tsx client/components/RootErrorFallback.test.tsx client/config/daemon.test.ts(62 个测试,本地全绿)。Provider 套件覆盖守卫的全部分支(无 Provider / 外来副本 / 本副本但子树外 / 渲染了但无 client / 两个副本都渲染过时重复副本优先 / 重复渲染下去重 / 注册表上限);main.test.tsx 覆盖「token 可存活时重试刷新」和「不可存活时原地重置」(断言未调用 reload 且子树重新挂载);daemon.test.ts 覆盖 hasReloadSurvivableDaemonToken 的 hash/query/持久化/无 token/存储抛异常五种情形。本地做过的变异检查:分别移除刷新存活保护、标签 plumbing、共享 harness 的 try/catch、带 URL 的 id、helper 局部 root(配合 afterEach 泄漏探针)、外来副本优先分支、注册表上限,对应的测试都会变红;还原后恢复绿色。Reviewer 也可以确认守卫消息前缀未变,现有子串断言和 build-artifact.test.ts 的检查仍然成立(transcript JS 实测 1,180,562 字节,低于 1,300,000 上限;import.meta.url 在 ES lib 构建中保留)。

证据(Before & After)

视觉上 N/A(布局无变化,仅重试按钮文案在刷新时不同)。行为证据:观测到的故障是「根兜底显示该守卫报错、只能刷新恢复」,一个 dev 会话里出现 3 次;本 PR 之后同一个界面的重试在安全时会自行完成这次恢复,且报错信息会直接指明失败类别。

测试环境

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

运行环境(可选)

packages/web-shellnpx vitest run;改动文件 eslint/prettier 干净;vite build --config vite.lib.config.ts --mode transcript 构建成功且守卫完整保留在产物中。(本 worktree 的独立版 vite build 被既有的 @tanstack/react-table 未安装问题阻塞,与本 diff 无关。)

风险与范围

  • 主要风险或取舍:重试按钮的行为现在取决于 token 是否可存活——在 token 不可存活的场景用户得到的是原来的原地重试,它无法恢复模块图根因的崩溃;可以接受,因为那种场景下刷新会彻底丢失认证。面包屑注册表挂在 globalThis 上、仅用于诊断(绝不通过它共享 context 值),上限 8 条。
  • 未验证 / 不在范围内:dev 会话模块双副本的根因不在此 PR 修复(本 PR 让它可诊断、可一键恢复);WebShellTranscript 的嵌入式边界刻意保留原地重试;chrome-extension iframe 路径依赖 sidepanel 在 iframe load 时重发 token,走的是重置路径。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

引用 #11100(无 Provider 渲染 transcript 时的同类严格守卫失败;本 PR 不关闭它——该 issue 的导出文档场景命中的正是「no provider has rendered」这个分支)。

… reload on root retry

The strict useDaemonWorkspace guard fails closed with a message that
cannot distinguish an absent provider from a duplicated module copy
(two DaemonWorkspaceContext instances in one page, seen in dev when the
module graph is refreshed under a live page), so the root boundary
showed a dead-end fallback that only a manual reload could clear.

The provider now registers a per-module-copy marker on first render,
and the guard's error states which of the three cases it hit: no
provider rendered, a provider rendered from a different module copy
(with both copy ids), or a provider from this copy rendered and the
consumer is outside its subtree. The standalone root boundary's retry
now reloads the page, since re-mounting the same broken module graph
would throw again.

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

qwen-code-ci-bot commented Sep 9, 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 Sep 9, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 23b018e. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 4 render-shaping files:

  • packages/web-shell/client/App.tsx
  • packages/web-shell/client/components/RootErrorFallback.tsx
  • packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx
  • packages/web-shell/client/main.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

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

@wenshao — the code is small and the tests are tight, but I can't find the failure this fixes.

No reproduction, and the one linked observation points the other way. The PR's own Evidence section says N/A, and #11100 is referenced but explicitly not closed by this PR. #11100 is the only place in the tracker where this guard's throw was ever observed, and the verification there (against main @ 82612e35) reproduced both throws and then concluded the user-visible failure is unreachable: no producer for the mcp/tasks sentinels, dispatched status blocks never reach a ChatRecord, /export html only reads ChatRecords, zero hits across 2354 real ChatRecords. Its own closing line was that the behaviour half "is not actionable … exactly the kind of speculative work that should wait for a real caller."

The dead-end root fallback does not fire in that case either. The throw comes from a message-body component, and MessageItem.tsx:211-217 wraps every message body in its own boundary — I re-checked that on the current tree, along with RootErrorFallback's data-web-shell-error marker. So the root fallback this retry change targets is never reached for the documented failure class. The two halves also aim at different trees: main.tsx mounts DaemonWorkspaceProvider at the standalone root, so a missing-provider throw cannot happen in the standalone app at all, while the transcript/export tree — where it can — keeps its in-place retry by design here.

The duplicate-module-copy branch has nothing behind it. No issue, PR, or commit in this repo reports a duplicated DaemonWorkspaceContext in a dev session. The nearest structural case is a host importing both @qwen-code/web-shell and @qwen-code/web-shell/transcript (anticipated in vite.lib.config.ts), and there the throw is the expected provider-less transcript render from #11100, not a module-graph fault. That branch is what most of the diff buys: DaemonWorkspaceProvider.tsx's 34 added lines — the globalThis registry, the Math.random() module id, the three-way message — are 34 of this PR's 47 changed production lines, spent diagnosing a scenario nothing has observed. That is what AGENTS.md's "nothing speculative / no error handling for impossible scenarios" pushes back on.

What would change my mind — either one is enough:

  • a console log (with the component stack) or an issue link from a real occurrence: a dev session where the guard threw while two module copies were live, or a standalone page where "Try again" failed and a reload recovered;
  • or split the retry half out and argue it on its own merits. "A standalone full-page root boundary should reload rather than re-mount the same page state" is defensible without the module-copy theory, and it is a one-line change in main.tsx plus its comment; the diagnostic registry can then wait for an actual occurrence, tracked in an issue.

To be clear about what this is not: no CI signal. Static review only — nothing was built or run (the triage no-execute rule) — and the checks on this head were still in flight with no failures when I looked. The message prefix is unchanged, so the existing substring assertion in DaemonWorkspaceProvider.test.tsx holds, and the dist/transcript.js ceiling (< 1_300_000 in build-artifact.test.ts) has ample room over the ~1.19 MB measured in #11100's thread.

If a re-run still has no occurrence to point at, this goes to a maintainer for a product call rather than looping here.

中文说明

@wenshao —— 代码量不大、测试也写得扎实,但我找不到这个 PR 修的是哪一次真实故障。

没有复现,而唯一被引用的观测结论相反。 PR 自己的 Evidence 一栏写的是 N/A#11100 只是 reference,并明确说明不由本 PR 关闭。#11100 是这个守卫异常在整个 tracker 里唯一被观测到的地方,而那里的验证(针对 main @ 82612e35)复现了两次抛异常之后,结论是用户可见故障不可达:mcp/tasks 哨兵没有生产端、dispatch 的 status 块不落 ChatRecord、/export html 只读 ChatRecord、2354 份真实 ChatRecord 命中数为零。那条结论的原话是行为那一半"现在无法动手……正属于应该等真实调用方出现再做的投机工作"。

"死胡同兜底"在那种情况下也不会出现。 抛异常来自消息体组件,而 MessageItem.tsx:211-217 给每个消息体单独包了一层 boundary —— 我在当前树上重新核对过这一点,以及 RootErrorFallbackdata-web-shell-error 标记。所以 retry 改动针对的根兜底,在已记录的失败类型里根本走不到。另外两半改动瞄的其实是不同的树:main.tsx 在独立版根部就挂了 DaemonWorkspaceProvider,独立版里不可能出现"没有 provider"的抛异常;而真正可能出现的 transcript/export 树,本 PR 又刻意保留了原地 retry。

"模块双副本"这一分支没有任何依据。 仓库里的 issue、PR、commit 都没有记录过 dev 会话中出现重复的 DaemonWorkspaceContext。最接近的结构性场景是宿主同时引入 @qwen-code/web-shell@qwen-code/web-shell/transcriptvite.lib.config.ts 里确实预料到了),但那种情况下的抛异常正是 #11100 里"transcript 无 provider"的预期行为,不是模块图故障。而 diff 的主要开销恰恰花在这个分支上:DaemonWorkspaceProvider.tsx 新增的 34 行——globalThis 注册表、Math.random() 模块 id、三分支文案——占本 PR 47 行生产改动里的 34 行,用来诊断一个从未被观测到的场景。这正是 AGENTS.md "不做投机代码 / 不为不可能的场景写错误处理"要挡的东西。

能改变我判断的证据 —— 两者之一即可:

  • 一次真实发生的现场:console 日志(含 component stack)或 issue 链接——dev 会话里两个模块副本同时存活时守卫抛异常,或者独立页面点 "Try again" 无效、刷新才恢复;
  • 或者把 retry 那一半拆出来单独论证。"独立整页应用的根 boundary 应该刷新,而不是重新挂载同一份页面状态"这个理由不依赖模块副本理论也成立,而且只是 main.tsx 里一行改动加一段注释;诊断注册表可以等到真有现场再说,并用 issue 跟踪。

需要说明这不是 CI 问题:本次只做静态审查,没有构建或运行任何代码(遵循 triage 禁止执行规则),我查看时该 commit 上的检查仍在进行中,没有失败项。报错前缀未变,所以 DaemonWorkspaceProvider.test.tsx 里现有的子串断言仍然成立;build-artifact.test.tsdist/transcript.js 的上限(< 1_300_000)相对 #11100 线程里测得的约 1.19 MB 也有充足余量。

如果 re-run 时仍然拿不出真实现场,这一条会转交 maintainer 做产品判断,而不是在这里反复。

Qwen Code · qwen3.8-max-2026-09-02

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

Test Plan (not a blocker): client/main.test.tsxno such file or directory.

中文说明

Test Plan(非阻断):client/main.test.tsxno such file or directory

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

Comment thread packages/web-shell/client/main.tsx Outdated
Comment thread packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx Outdated
Comment thread packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx Outdated
Comment thread packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx Outdated
Comment thread packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx Outdated
Comment thread packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx Outdated
Comment thread packages/web-shell/client/main.tsx Outdated
…annot leak the console.error mock

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

6 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-2 packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:492 — already reported (comment 3964488531)
  • R1-3 packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:510 — already reported (comment 3964488543)
  • R1-4 packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:37 — already reported (comment 3964488545)
  • R1-5 packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:241 — already reported (comment 3964488558)
  • R1-6 packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:239 — already reported (comment 3964488562)
  • R1-7 packages/web-shell/client/main.tsx:205 — already reported (comment 3964488571)

Unresolved, please confirm:

  • [Critical] Open triage CHANGES_REQUESTED (review 5148889985) on packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:27-50 — objects that the duplicate-module-copy diagnostic branch has no observed occurrence behind it (34 of the PR'…

Test Plan (not a blocker): client/main.test.tsxno such file or directory.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/web-shell/client/main.test.tsx:84 — [probe] the retry test's only causal assertion is a post-click total count, so a mount-time reload with a dead button stays green (add a pre-click not.toHaveBeenCalled())
  • packages/web-shell/client/main.test.tsx:84 — [probe] the retry test's fixture has no reload-survivable token, so it pins the disputed half of R1-1 and its prescribed fix turns this case red

[Critical] R1-1: [fails-closed] [regression] Still standing — re-asserted from round 1 (open inline comment 3964488526 at packages/web-shell/client/main.tsx:205) and re-ruled against head c55f0492 by reading the code rather than the diff. Turning the standalone root retry into a full document reload discards the only reload-survivable copy of the daemon token, so in the storage-unavailable contexts config/daemon.ts already handles, one click on "Try again" converts a recoverable render crash into a permanently unauthenticated shell. At head, main.tsx:198 reads fallback={(error) => ( — the boundary's reset is no longer even in scope — and main.tsx:205 reloads unconditionally; no reload-survivability guard exists anywhere in packages/web-shell. persistDaemonToken (config/daemon.ts:47-55) still swallows the storage throw under its own comment "a refresh will lose it, matching the old behavior", getDaemonToken still short-circuits on the in-memory cache at config/daemon.ts:57, and waitForDaemonTokenMessage still resolves undefined when window.parent === window — so in a production (non-DEV) top-level tab whose URL token was stripped at boot and whose persist threw, the reloaded document has no token from any source and every request 401s, with nothing in the UI saying authentication was lost and recovery meaning a re-run of qwen serve --open. This round's delta is test-only (+4/-4 in main.test.tsx), so nothing in it could have closed the mechanism. Witness, measured in round 1 and re-measured this round: PROBE-K storage calls during boot: ["setItem(qwen-daemon-token)"] — persist attempted, threw, swallowed; PROBE-L getDaemonToken(): undefined; PROBE-L window.parent === window (top-level tab): true; PROBE-L waitForDaemonTokenMessage(): undefined; PROBE-L <StandaloneApp daemonToken={...}>: undefined; PROBE-L getDaemonAuthHeaders(): undefined; PROBE-O (PR arm) reload called: 1, tokens seen by provider: ["token-from-boot","token-from-boot"] — no re-mount; PROBE-O (base arm, hunk reverted) reload called: 0, tokens seen by provider: 4 x "token-from-boot" — re-mounted with the in-memory token; and this round's PROBE_TOKEN persistedTokenBeforeClick=null persistedTokenAfterClick=null sessionStorageLength=0, with persistDaemonToken module-private at config/daemon.ts:47 and reachable only from getDaemonToken's URL branch at :74. Keep the reload only where a reload can still authenticate — onRetry={() => (hasPersistedDaemonToken() ? window.location.reload() : reset())} — which also means putting reset back into the fallback signature at main.tsx:198. The fix rests on two existing facts. First, vi.spyOn(window.sessionStorage, …) silently does nothing in this package's jsdom (measured in round 1: direct call hit the spy? false, spy.calls: 0), so the guard's test must use the idiom already at packages/web-shell/client/config/daemon.test.ts:171-176 (throw new Error('storage disabled') at :173) or vi.stubGlobal, or it passes vacuously. Second, config/daemon.ts:57if (cachedDaemonToken) return cachedDaemonToken; — means the guard cannot be built on getDaemonToken(), which always reports a token after boot; it has to ask whether the token is reload-survivable, by having persistDaemonToken record success or by re-reading storage. A main.test.tsx case beside the new reload test — storage unavailable, no token in the URL, click retry, assert reload was NOT called and the tree re-mounted — must go red if this guard is removed; please drop the guard and confirm it reds.

中文说明 R1-1 仍然成立 —— 沿用第 1 轮的结论(行内评论 3964488526,位于 packages/web-shell/client/main.tsx:205),本轮按 head c55f0492 重新读代码而非读 diff 后再次判定。把独立版根边界的"重试"改成整页刷新后,daemon token 唯一能在刷新后存活的副本就被丢掉了:在 config/daemon.ts 本来已经处理过的"存储不可用"场景下,点一次 "Try again" 就把一次可恢复的渲染崩溃变成了永久未认证的 shell。当前 head 上 main.tsx:198fallback={(error) => ( —— 边界的 reset 已经不在作用域里 —— 而 main.tsx:205 无条件刷新;packages/web-shell 中不存在任何"token 能否在刷新后存活"的判据。persistDaemonTokenconfig/daemon.ts:47-55)仍然吞掉存储异常,其自带注释写着 "a refresh will lose it, matching the old behavior";getDaemonToken 仍在 config/daemon.ts:57 于内存缓存上短路;waitForDaemonTokenMessagewindow.parent === window 时仍立即返回 undefined。因此在生产(非 DEV)顶层标签页里,URL token 已在启动时被抹掉、持久化又抛过异常时,刷新后的文档从任何来源都拿不到 token,每个请求都 401,界面上没有任何地方说明认证已丢失,只能重新执行 qwen serve --open。本轮增量只有测试(main.test.tsx +4/-4),不可能修复该机制。 (Witness 为第 1 轮实测并于本轮复测的程序输出,逐条见上方英文部分,此处不重复。) 只在"刷新后仍能认证"的地方保留刷新 —— onRetry={() => (hasPersistedDaemonToken() ? window.location.reload() : reset())} —— 这也意味着要把 reset 放回 main.tsx:198 的 fallback 签名。 该修复依赖两个既有事实。其一,本包的 jsdom 中 vi.spyOn(window.sessionStorage, …) 静默无效(第 1 轮实测:direct call hit the spy? false, spy.calls: 0),所以判据的测试必须使用 packages/web-shell/client/config/daemon.test.ts:171-176 已有的写法(:173throw new Error('storage disabled'))或 vi.stubGlobal,否则会空转通过。其二,config/daemon.ts:57if (cachedDaemonToken) return cachedDaemonToken; 意味着判据不能建立在 getDaemonToken() 上(启动后它永远报告有 token),必须改问"token 是否能在刷新后存活"——让 persistDaemonToken 记录成功与否,或重新读取存储。 请在新的 reload 测试旁边补一个 main.test.tsx 用例 —— 存储不可用、URL 中无 token、点击重试,断言 reload 未被调用且子树重新挂载 —— 移除该保护后它必须变红;请去掉保护并确认它失败。

中文说明

本轮确认的 6 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

Test Plan(非阻断):client/main.test.tsxno such file or directory

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

[Critical] R1-1: [fails-closed] [regression] Still standing — re-asserted from round 1 (open inline comment 3964488526 at packages/web-shell/client/main.tsx:205) and re-ruled against head c55f0492 by reading the code rather than the diff. Turning the standalone root retry into a full document reload discards the only reload-survivable copy of the daemon token, so in the storage-unavailable contexts config/daemon.ts already handles, one click on "Try again" converts a recoverable render crash into a permanently unauthenticated shell. At head, main.tsx:198 reads fallback={(error) => ( — the boundary's reset is no longer even in scope — and main.tsx:205 reloads unconditionally; no reload-survivability guard exists anywhere in packages/web-shell. persistDaemonToken (config/daemon.ts:47-55) still swallows the storage throw under its own comment "a refresh will lose it, matching the old behavior", getDaemonToken still short-circuits on the in-memory cache at config/daemon.ts:57, and waitForDaemonTokenMessage still resolves undefined when window.parent === window — so in a production (non-DEV) top-level tab whose URL token was stripped at boot and whose persist threw, the reloaded document has no token from any source and every request 401s, with nothing in the UI saying authentication was lost and recovery meaning a re-run of qwen serve --open. This round's delta is test-only (+4/-4 in main.test.tsx), so nothing in it could have closed the mechanism. Witness, measured in round 1 and re-measured this round: PROBE-K storage calls during boot: ["setItem(qwen-daemon-token)"] — persist attempted, threw, swallowed; PROBE-L getDaemonToken(): undefined; PROBE-L window.parent === window (top-level tab): true; PROBE-L waitForDaemonTokenMessage(): undefined; PROBE-L <StandaloneApp daemonToken={...}>: undefined; PROBE-L getDaemonAuthHeaders(): undefined; PROBE-O (PR arm) reload called: 1, tokens seen by provider: ["token-from-boot","token-from-boot"] — no re-mount; PROBE-O (base arm, hunk reverted) reload called: 0, tokens seen by provider: 4 x "token-from-boot" — re-mounted with the in-memory token; and this round's PROBE_TOKEN persistedTokenBeforeClick=null persistedTokenAfterClick=null sessionStorageLength=0, with persistDaemonToken module-private at config/daemon.ts:47 and reachable only from getDaemonToken's URL branch at :74. Keep the reload only where a reload can still authenticate — onRetry={() => (hasPersistedDaemonToken() ? window.location.reload() : reset())} — which also means putting reset back into the fallback signature at main.tsx:198. The fix rests on two existing facts. First, vi.spyOn(window.sessionStorage, …) silently does nothing in this package's jsdom (measured in round 1: direct call hit the spy? false, spy.calls: 0), so the guard's test must use the idiom already at packages/web-shell/client/config/daemon.test.ts:171-176 (throw new Error('storage disabled') at :173) or vi.stubGlobal, or it passes vacuously. Second, config/daemon.ts:57if (cachedDaemonToken) return cachedDaemonToken; — means the guard cannot be built on getDaemonToken(), which always reports a token after boot; it has to ask whether the token is reload-survivable, by having persistDaemonToken record success or by re-reading storage. A main.test.tsx case beside the new reload test — storage unavailable, no token in the URL, click retry, assert reload was NOT called and the tree re-mounted — must go red if this guard is removed; please drop the guard and confirm it reds.

中文说明 R1-1 仍然成立 —— 沿用第 1 轮的结论(行内评论 3964488526,位于 packages/web-shell/client/main.tsx:205),本轮按 head c55f0492 重新读代码而非读 diff 后再次判定。把独立版根边界的"重试"改成整页刷新后,daemon token 唯一能在刷新后存活的副本就被丢掉了:在 config/daemon.ts 本来已经处理过的"存储不可用"场景下,点一次 "Try again" 就把一次可恢复的渲染崩溃变成了永久未认证的 shell。当前 head 上 main.tsx:198fallback={(error) => ( —— 边界的 reset 已经不在作用域里 —— 而 main.tsx:205 无条件刷新;packages/web-shell 中不存在任何"token 能否在刷新后存活"的判据。persistDaemonTokenconfig/daemon.ts:47-55)仍然吞掉存储异常,其自带注释写着 "a refresh will lose it, matching the old behavior";getDaemonToken 仍在 config/daemon.ts:57 于内存缓存上短路;waitForDaemonTokenMessagewindow.parent === window 时仍立即返回 undefined。因此在生产(非 DEV)顶层标签页里,URL token 已在启动时被抹掉、持久化又抛过异常时,刷新后的文档从任何来源都拿不到 token,每个请求都 401,界面上没有任何地方说明认证已丢失,只能重新执行 qwen serve --open。本轮增量只有测试(main.test.tsx +4/-4),不可能修复该机制。 (Witness 为第 1 轮实测并于本轮复测的程序输出,逐条见上方英文部分,此处不重复。) 只在"刷新后仍能认证"的地方保留刷新 —— onRetry={() => (hasPersistedDaemonToken() ? window.location.reload() : reset())} —— 这也意味着要把 reset 放回 main.tsx:198 的 fallback 签名。 该修复依赖两个既有事实。其一,本包的 jsdom 中 vi.spyOn(window.sessionStorage, …) 静默无效(第 1 轮实测:direct call hit the spy? false, spy.calls: 0),所以判据的测试必须使用 packages/web-shell/client/config/daemon.test.ts:171-176 已有的写法(:173throw new Error('storage disabled'))或 vi.stubGlobal,否则会空转通过。其二,config/daemon.ts:57if (cachedDaemonToken) return cachedDaemonToken; 意味着判据不能建立在 getDaemonToken() 上(启动后它永远报告有 token),必须改问"token 是否能在刷新后存活"——让 persistDaemonToken 记录成功与否,或重新读取存储。 请在新的 reload 测试旁边补一个 main.test.tsx 用例 —— 存储不可用、URL 中无 token、点击重试,断言 reload 未被调用且子树重新挂载 —— 移除该保护后它必须变红;请去掉保护并确认它失败。

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

…lity and sharpen copy diagnostics

Review round on ca4cdfb found that the unconditional reload could strand
the shell unauthenticated when the daemon token never reached storage,
and that the diagnostics had four misreport shapes. Retry now reloads
only when a reload can still authenticate (URL or per-tab persisted
token) and falls back to an in-place reset otherwise, with the button
label matching the action via a new retryMode prop on RootErrorFallback.
The copy registry tracks rendered-vs-provided per module copy, prefers
the duplicate-copies branch when foreign copies exist, carries the
module URL in each copy id, and is capped so dev hot re-evaluation
cannot grow it unbounded.

Mutation checks performed: removing the reload-survivability guard,
the label plumbing, the harness try/catch, the URL-bearing id, the
helper-local root, the foreign-copy precedence, or the registry cap
each turns the corresponding new test red; reverting restores green.

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

wenshao commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

On the triage review's "I can't find the failure this fixes" — the occurrence is real; it just wasn't in the PR body before (my omission, now corrected).

The occurrence. Yesterday morning, in a dev session running the vite dev server (packages/web-shell, --open) against qwen serve on 4170, the standalone shell's top-level boundary displayed this exact guard message — raw error text visible, which only happens when import.meta.env.DEV is true — three times, and each time the only recovery was a full page reload; the boundary's in-place retry could not clear it. That is the reviewer's second offered shape ("a standalone page where 'Try again' failed and a reload recovered"), observed before this PR existed. What I did not capture is the console component stack, so the duplicate-module-copy mechanism is an inference from the static shape (the standalone tree always mounts the provider and always produces a context value, so within one coherent module graph the guard cannot fire), not a proven fact. The registry branch exists to confirm or refute that inference on the next occurrence rather than to assert it — if the next report comes back "no provider has rendered", that is equally valuable, and it points at a real integration bug instead.

On "34 of 47 lines diagnose an unobserved scenario": fair pushback, and I weighed splitting. What tipped it: the occurrence did happen, repeatedly, and each occurrence cost a confused dead-end screen plus a manual reload. The diagnostics are the difference between the next occurrence being self-explaining and another investigation from scratch. The registry is diagnostic-only (context values are never shared through it — the embeddability isolation argument in the thread stands untouched), now bounded at 8 entries, and ~40 lines with tests that pin each branch.

On the retry half: your split suggestion is moot in the best way — R1-1 found the unconditional reload could strand the shell unauthenticated, so the retry now reloads only when the token survives the reload (new hasReloadSurvivableDaemonToken(), unit-tested across hash/query/persisted/empty/throwing-storage) and otherwise falls back to the previous in-place reset. The label follows the action ("Reload page" vs "Try again"). That half now stands on a correctness fix, not on the module-copy theory.

On the fallback-level correction (message-level boundaries catch #11100's class): agreed, and noted in the updated body — the observed occurrences hit a top-level boundary, consistent with a context-identity fault anywhere in the tree rather than a missing provider in a message body.

All seven inline findings from the review rounds are addressed in 0c2ee2fdec with per-thread replies (including the mutation checks requested: each guard/label/harness/precedence/cap removal turns the corresponding test red). 81 tests green across the touched and adjacent suites; eslint/prettier clean; the transcript lib build keeps the guard intact and under the size ceiling.

中文说明

关于 triage 评审「找不到这个 PR 修的是哪次故障」——故障是真实发生过的,只是之前没写进 PR 正文(我的疏漏,现已补上)。

现场。 昨天上午的一个 dev 会话(vite dev server + qwen serve 4170),独立 shell 的顶层边界三次显示了这个守卫的报错原文——原文可见这一点只有在 DEV 构建下才成立——每次都只能靠整页刷新恢复,边界的原地重试无法清除。这正是 reviewer 给出的第二种可接受形状("独立页面中 'Try again' 无效、刷新才恢复"),且发生于本 PR 存在之前。我没有抓到的是 console 里的 component stack,所以"模块双副本"机制是从静态形状推断的(独立应用树总是挂载 provider 且总是产出 context 值,单一模块图内这个守卫不可能触发),不是已证事实。注册表分支的意义正是在下次发生时证实或证伪这个推断——如果下次报出的是 "no provider has rendered",同样有价值,那指向的是真实的集成 bug。

关于「47 行生产改动里 34 行在诊断一个未观测场景」: 批评合理,我也考虑过拆分。决定性因素是:故障确实发生过、且反复发生,每次都是一次困惑的死屏加手动刷新。诊断机制决定下次发生时是自证原因还是重新排查一遍。注册表仅用于诊断(绝不通过它共享 context 值——线程里关于可嵌入隔离的论证不受影响),现有 8 条上限,约 40 行且每个分支都有测试锁定。

关于 retry 那一半: 拆分建议以一种最好的方式变得不再必要——R1-1 发现无条件刷新可能让 shell 永久失去认证,所以重试现在只在 token 可存活时刷新(新的 hasReloadSurvivableDaemonToken(),五种情形有单测),否则回退为原来的原地重置。按钮文案跟随行为("Reload page" vs "Try again")。这一半现在建立在正确性修复之上,而不是模块副本理论之上。

关于兜底层级的指正#11100 那类会被消息级边界捕获):同意,已写进更新后的正文——本次观测到的是顶层边界触达,与"树中任意位置的 context 实例不匹配"一致,而非消息体内缺 provider。

评审轮次的全部 7 条 inline 发现已在 0c2ee2fdec 中处理并逐条回复(含评审要求的变异检查:移除守卫/标签/harness/优先级/上限中的每一个都会让对应测试变红)。受影响及相邻套件共 81 个测试全绿;eslint/prettier 干净;transcript lib 构建后守卫完整且体积在上限之内。

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

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • DUP3-1 packages/web-shell/client/config/daemon.ts:64 — already reported (comment 3964488526)
  • DUP3-2 packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:311 — already reported (comment 3964488558)

Unresolved, please confirm:

  • [Critical] Open triage CHANGES_REQUESTED (review 5148889985) on packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:27-50 — objects that the duplicate-module-copy diagnostic branch has no observed occurrence behind it (34 of the PR'…

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": did not execute packages/web-templates/src/export-html/build.mjs end-to-end to observe the [empty-import-meta] warning against the real bundle — it writes b….

Test Plan (not a blocker): client/main.test.tsxno such file or directory; client/components/RootErrorFallback.test.tsxno such file or directory; client/config/daemon.test.tsno such file or directory.

中文说明

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"did not execute packages/web-templates/src/export-html/build.mjs end-to-end to observe the [empty-import-meta] warning against the real bundle — it writes b…

Test Plan(非阻断):client/main.test.tsxno such file or directory; client/components/RootErrorFallback.test.tsxno such file or directory; client/config/daemon.test.tsno such file or directory

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

Comment thread packages/web-shell/client/main.tsx Outdated
Comment thread packages/web-shell/client/config/daemon.ts
Comment thread packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx Outdated
Comment thread packages/web-shell/client/config/daemon.test.ts
Comment thread packages/web-shell/client/config/daemon.test.ts
Comment thread packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx Outdated
Comment thread packages/web-shell/client/main.tsx Outdated
Comment thread packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx Outdated
Comment thread packages/web-shell/client/components/RootErrorFallback.tsx
…en parse, bounded-map util, wording honesty

- main.tsx: the survivability gate now also passes when no token was
  resolved at boot (tokenless trusted loopback strands nothing), and the
  reload carries the live theme/language across, since session switches
  strip those one-shot URL params.
- config/daemon.ts: the URL token grammar is parsed once in
  readTokenFromLocation(), shared by getDaemonToken() and the
  survivability predicate so the spellings cannot drift.
- DaemonWorkspaceProvider.tsx: copy ids fall back to a module name when
  import.meta.url is lowered away (esbuild iife export documents); the
  registry insert goes through a new utils/bounded-map helper (the
  package's fourth copy of that loop, and the first with the correct
  exit test); both guard branches admit unmount/lost-client explicitly.
- export-html build.mjs: silences the deliberate empty-import-meta
  warning and fails the build if it resurfaces.
- Tests: boot-order replay pinning that the predicate never consults
  getDaemonToken()'s cache, a same-element re-render pinning 'provided'
  as terminal, a live-copy-eviction test that seeds foreign copies after
  the live one, the tokenless-reload and theme/language-carry cases in
  main.test.tsx, the zh-CN reload label, and bounded-map unit tests.
  Each new guard was exercised as a mutation first (removed or reverted)
  and confirmed to turn its test red.

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Blocking finding(s) follow.

Reviewed. Suggestions are inline.

Unresolved, please confirm:

  • [Critical] Open triage CHANGES_REQUESTED (review 5148889985) on packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:27-50 — objects that the duplicate-module-copy diagnostic branch has no observed occurrence behind it (34 of the PR'…

Test Plan (not a blocker): client/main.test.tsxno such file or directory; client/components/RootErrorFallback.test.tsxno such file or directory; client/config/daemon.test.tsno such file or directory.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:311 — [review] D4-1 the foreign-copy branch outranks the own-state diagnosis, so a stale registry entry left by a dev hot re-evaluation makes the guard blame module dupl…
  • packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:325 — [probe] D4-2 the empty-registry branch asserts a page-wide fact it cannot observe, so a provider mounted from an uninstrumented module copy is reported as never re…

Convergence: round 4 posted 5 inline comment(s), 5 of them reported for the first time; the previous round posted 11 (11 new). Findings keep coming back to the same files: packages/web-shell/client/main.tsx (findings in rounds 1, 3; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

Test Plan(非阻断):client/main.test.tsxno such file or directory; client/components/RootErrorFallback.test.tsxno such file or directory; client/config/daemon.test.tsno such file or directory

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 4 轮发布了 5 条行内评论,其中 5 条是首次提出;上一轮发布了 11 条(其中 11 条首次提出)。发现反复回到同一批文件:packages/web-shell/client/main.tsx(第 1、3 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread packages/web-templates/src/export-html/build.mjs Outdated
Comment thread packages/web-shell/client/main.tsx
Comment thread packages/web-shell/client/utils/bounded-map.ts
Comment thread packages/web-shell/client/components/RootErrorFallback.test.tsx
Comment thread packages/web-shell/client/main.test.tsx
wenshao and others added 2 commits September 10, 2026 04:21
…ce-guard-diagnostics

# Conflicts:
#	packages/web-shell/client/main.test.tsx
…RL reload, a firing import.meta guard, finish the bounded-map migration

- main.tsx: boot strips the one-shot theme/language/lang params once the
  initializers have read them, so the retry reload's URL carry cannot
  become a permanent override of stored preferences; the reload URL is
  built from the live location.href.
- export-html build.mjs: the import.meta guard now keeps the warnings
  (logLevel: 'error') and fails only on reads outside the tolerated
  prebuilt transcript entry — the previous logOverride: 'silent'
  discarded them and made the check unreachable.
- bounded-map: App.tsx's same-name private copy and the
  useSessionArtifacts tail migrate to the shared helper, deleting the
  drifted duplicates (their 'if (!oldest) break' exit would stop
  evicting on an empty-string key).
- tests: the zh-CN x default-reset copy cell; the reload-carry test now
  stubs location after the last navigation and pins the live session
  path and workspace in the carried URL; scripts/tests gains an
  end-to-end case that drives the export build with an injected
  import.meta read and asserts the guard's failure.
  Mutation checks: removing the boot strip, reverting the guard to
  logOverride: silent, mutating the zh-CN retry copy, or building the
  reload URL from origin each turns the corresponding new case red.

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

wenshao commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (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: 57 passed · 1 failed · 58 total

Flakiness gate: ✅ 6 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

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

脚本断言:57 通过 · 1 失败 · 58 总计

抖动门:✅ 6 changed test file(s) x 5 identical rounds, no divergence

Verification report

Verify report — PR 11421 (QwenLM/qwen-code)

Verdict: findings — 57/58 scripted assertions passed, 1 unexpected failure (G7-true-cause-named, the finding below). Verified head 444d1cbde32ab0bc2100c599b10972f4b2245d5c (= HEAD^2), base 2e212144d3d82f0eb95d2e137edf363eb9d338ef (= HEAD^1). The central claim passed its A/B: all four documented failure classes flip from one indistinguishable message at base to four distinct, correctly-named messages at head (table 1), and the reload gate flips 2/3 retry scenarios from reset to reload while correctly holding the memory-only-token control (table 2). The single failing assertion is a diagnostic-accuracy defect in the new guard message (finding F1): in any page that has ever held a second — even long-dead — copy of the provider module, the message reports "duplicate copies" for every failure class and drops the cause the registry already knows. Not blocking: no functional regression reproduced, and every guard the PR introduces is pinned by a named test (15/15 mutants killed).

中文摘要
  • 结论: findings。58 条脚本化断言中 57 条通过,1 条失败(即下面的 F1)。核心主张的 A/B 通过:守卫报错在 base 上对 4 种失败情形只有一条无法区分的消息,在 head 上变成 4 条各自正确命名的消息(表 1);重试按钮在 2/3 场景从"原地重置"翻转为"刷新页面",同时内存 token 的对照场景正确地保持不变(表 2)。
  • A/B 结论: base 臂 5 个场景全部输出同一条裸消息(对照如预期为红);head 臂 s1–s4 分别命名 no-provider / duplicate-copies / outside-subtree / no-active-client,且消息前缀不变(现有子串断言仍成立)。刷新门控:URL token 场景 reload=1、标签"重新加载";内存 token 场景 reload=0、标签"Try again";无 token 场景 reload=1、标签"Reload page";base 三个场景全部 reload=0。
  • Findings: (F1) 重复副本分支无条件优先,且丢弃注册表已知的本副本状态——任何曾经存在过第二个模块副本(哪怕早已卸载)的页面,此后所有守卫报错都会说"duplicate copies",包括纯粹的 autoConnect={false} 误用;在观测到该故障的 vite dev 环境里,HMR 恰好会留下这样一个陈旧 id,因此这个诊断无法证伪它本来要检验的重复模块假设。已给出并实测的最小修复(6/6 通过、零副作用、单测 28/28 两侧不变)。(F2) 新增的 import.meta 守卫测试只有失败臂,没有正对照:M16 证明它无法检出"拒绝一切"的守卫(那种守卫会打断所有真实导出构建);本报告的未修改构建 exit 0 补上了缺失的一半。(F3/F4) 两条注释与实测不符:logLevel 不影响 result.warnings(error/silent/info 均为 3 条),logOverride 的类型是 Record<string, LogLevel> 而非级别字符串;ESM 构建里 id 携带真实模块 URL,但导出文档(iife)构建里 import.meta 被降级,id 退化为常量字符串+随机后缀。
  • 未覆盖范围: 逐 commit 归因(浅克隆只有 merge/base/head 三个 commit);dev 会话模块双副本的真实根因(本 PR 明确推迟,我的 s2/s5 用带 query 的模块实例构造重复,复现的是"重复模块图"下的消息行为而非触发机制);chrome-extension iframe 路径与 WebShellTranscript 内嵌边界;Playwright e2e;挂载期 strip effect 与并发导航的竞态;仓库全量测试与 PR 自身 CI lane。

Central claim and its A/B

Central claim — the strict useDaemonWorkspace guard now reports which situation it detected, via a globalThis breadcrumb registry, distinguishing: no provider rendered / provider from a foreign module copy (both ids) / same copy but consumer outside its live subtree / same copy rendered without an active client.

Harness: five scenarios, each in its own vitest file (fresh module registry and globalThis per file), driving the real DaemonWorkspaceProvider / useDaemonWorkspace with a real loopback HTTP daemon (nothing on the path from provider to guard is stubbed). The duplicate-copy scenarios obtain a genuine second module instance via a query-suffixed import — the mechanism a bundler/HMR uses — and the throw itself proves two distinct DaemonWorkspaceContext objects exist. Base arm = git worktree at HEAD^1 with the harness copied in; internal deps resolve upward to the root node_modules, and the PR touches no package outside packages/web-shell/client, packages/web-templates/src/export-html, scripts/tests (asserted: readlink -f node_modules/@qwen-code/sdkpackages/sdk-typescript, unchanged by the diff; the vitest alias for @qwen-code/web-shell/daemon-react-sdk resolves into each arm's own tree). Raw per-cell JSONL: logs/ab-head.jsonl, logs/ab-base.jsonl; oracle driver harness/ab-oracle.mjs; witness 01-ab-guard-branches-and-reload-gate.png.

Table 1 — guard message per failure class (base vs head)

scenario base arm head arm
s1 no provider anywhere bare message (no class) no DaemonWorkspaceProvider has rendered in this page
s2 provider rendered from a 2nd module copy bare message (no class) a DaemonWorkspaceProvider rendered from module copy <urlA#id>, but this hook resolved module copy <urlB#id> — the page holds duplicate copies… (both ids present)
s3 same copy provided, consumer outside its (unmounted) subtree bare message (no class) …this consumer is outside its live subtree…
s4 same copy, autoConnect={false} bare message (no class) …rendered without an active client (e.g. autoConnect is false)…
s5 stale 2nd copy + autoConnect={false} (true cause = no client) bare message (no class) …duplicate copies…the no-client cause is absent (F1)

Cells: base 1 distinct message across 5 scenarios (control red as predicted, G0-control); head 4/4 distinct across s1–s4 (G5); positive control — the real daemon was reached in s2/s3 on both arms (daemonRequests=1), so neither arm's cells are vacuous (G0-control-reached-code, G0-control-reached-code-head).

Secondary claim 1 — the standalone root boundary's retry reloads iff a reload cannot strand a credential, with an honest label. Harness drives the real hasReloadSurvivableDaemonToken() over real window.location / sessionStorage (not a stubbed boolean, unlike the PR's own test): real boot (getDaemonToken() + removeDaemonTokenFromUrl()), real render, real MouseEvent('click'), and a destination-level oracle — location.href read after the click through a stub whose getters delegate to the live jsdom Location, so the reported URL is the real post-replaceState one.

Table 2 — retry behaviour (base vs head)

scenario base: label / reload / url-after-click head: label / reload / url-after-click
token survived boot in sessionStorage 重试 / 0 / ?theme=light&language=zh-CN 重新加载 / 1 / ?theme=light&language=zh-CN
token in memory only (sessionStorage throws) Try again / 0 / ?theme=dark Try again / 0 / / (reset, no URL write)
no token at boot (trusted loopback) Try again / 0 / ?theme=light&language=en Reload page / 1 / ?theme=light&language=en

2/3 cells flip reset→reload; the memory-only control correctly does not (R2, R2b). The one-shot param round trip is proven end-to-end: base keeps ?theme=…&language=… in the URL after mount, head strips them at mount (searchAfterMount="") and re-adds the live values on the reload (R4-roundtrip).

Secondary claim 2 — the setBoundedMapEntry extraction is behaviour-preserving at all six migrated call sites (T9: 5× MAX_ARTIFACT_PANEL_SESSION_STATES in App.tsx, 1× MAX_CACHED_SESSIONS in useSessionArtifacts.ts, matching the constants the removed inline loops hard-coded), and it fixes a latent bug: the eviction sentinel changed from if (!oldest) break to if (oldest === undefined) break, so an empty-string key no longer halts eviction (M7 killed by evicts an empty-string key…). The import.meta build guard is load-bearing in the negative direction (M16's injected probe kills the real build with the file named) and its allowlist is genuinely exercised (3/3 empty-import-meta warnings from ../../../web-shell/dist/transcript.js, all matched by the regex).

Corrections to the description

These are statements in the PR text that the measurements contradict; none of them asks for a code change beyond the note attached.

  1. "The standalone vite build in this worktree is blocked by a pre-existing missing @tanstack/react-table install, unrelated to this diff." Does not reproduce in a clean npm ci environment: @tanstack/react-table is present in the root node_modules and npx vite build in packages/web-shell succeeds (exit 0, 23.5 s; log logs/head-vite-build.log). Consequence worth having: the standalone production bundle was therefore verifiable here — dist/assets/index-*.js carries both Reload page and the duplicate-copy diagnostic (T8). Caveat for the reader: that build empties packages/web-shell/dist/ (it removed transcript.js, index.js, daemon-react-sdk.js, types/), so anyone reproducing must re-run the package build afterwards; I did, and the restored artifacts are byte-identical in size to the CI-built ones.
  2. "logOverride 'silent' would discard them and make that check vacuous" (comment justifying logLevel: 'error' in build.mjs). Measured false: documentBuildResult.warnings.length === 3 (all empty-import-meta) at logLevel error, silent, and infologLevel does not affect result.warnings at all. And esbuild's typings say logOverride?: Record<string, LogLevel> (node_modules/esbuild/lib/main.d.ts:85), i.e. a per-message-id map, not a level string; logOverride: 'silent' is not a valid spelling of either option. The line's real effect is therefore log suppression only; it is not load-bearing for the guard (mutant M17 survives). Suggested comment: "keeps the tolerated warnings out of the build log; result.warnings is populated at every logLevel, which is what the check below reads."
  3. "62 tests, all green locally" for the four named suites. At this head the four suites hold 70 tests (provider 28, daemon 27, main 8, fallback 7) plus 4 in the new bounded-map.test.ts = 74/74 green. The body's number predates the round-4 additions.
  4. "the id carries the module URL so the message points at the offending chunk" — true for the ES builds (T5: 3 import.meta.url reads survive in dist/transcript.js) but not for the export-document build: esbuild's iife lowering leaves the document runtime with zero import.meta occurrences and the constant fallback id web-shell/DaemonWorkspaceProvider (T6), so in exported documents the id distinguishes copies only by its random suffix. Not a defect — the fallback is exactly what prevents a crash there — but the comment overstates.

Findings

F1 (Suggestion, the round's main finding) — the duplicate-copy verdict is sticky and swallows the cause the registry already knows

useDaemonWorkspace computes detail with foreignIds.length > 0 as the first branch, and that branch reports only the duplicate-copy fact. The registry also holds ownState for the hook's own copy — 'rendered' vs 'provided' — but once any foreign id exists, ownState is never consulted, so a page whose live copy rendered with autoConnect={false} is told "the page holds duplicate copies of the DaemonWorkspaceProvider module" and nothing about the client.

Why this matters beyond tidiness: the registry entry for a dead copy is sticky. In the exact environment where the failure was observed (a Vite dev session), any hot re-evaluation of DaemonWorkspaceProvider.tsx mints a new moduleInstanceId and leaves the old one in the registry until 8 newer ids evict it. From that moment on, every guard failure in that page — whatever its true class — reports "duplicate copies". The PR states the diagnostic exists "to confirm or refute [the duplicate-module hypothesis] on the next occurrence instead of guessing again"; with this precedence it can only ever confirm it. The author's own test reports duplicate copies even when this copy also rendered a provider pins the precedence deliberately, so this is a design consequence, not an oversight — but it defeats the stated purpose in the environment that produced the bug.

Reproduce (real, no synthetic registry seeding; the stale copy is a real second module instance that rendered a provider against a real loopback daemon and then unmounted):

D=tmp/pr11421-verify-20260909-230339   # this artifact dir; the harness lives in $D/harness
cp -r $D/harness packages/web-shell/client/__verify__
cd packages/web-shell && VERIFY_ARM=head VERIFY_OUT=/tmp/s5.jsonl \
  npx vitest run --config vitest.config.ts __verify__/s5-stale-copy.test.tsx
# message: "...the page holds duplicate copies of the DaemonWorkspaceProvider module)"
#          — contains no mention of the operative autoConnect={false} cause

(harness file harness/s5-stale-copy.test.tsx; the same scenario at base emits the bare message, so this is a head-only diagnostic defect, not a regression.)

Blast radius: every consumer of useDaemonWorkspace / useDaemonWorkspaceActions / useDaemonSessionArtifacts etc. that can throw the guard in a dev page after ≥1 HMR of the provider module; the message is the only output, so the misdiagnosis is what a developer reads.

Minimal suggested fix (measured, preserves the original intent)

Keep the duplicate-copy verdict, append the own-copy state so the message can never hide a cause the registry already holds:

+    const ownDetail =
+      ownState === 'provided'
+        ? 'this copy has also rendered a provider, so this consumer is ' +
+          'outside its live subtree, that provider has unmounted, or it ' +
+          'currently has no active client (autoConnect is false)'
+        : ownState === 'rendered'
+          ? 'this copy has also rendered a provider without an active ' +
+            'client (e.g. autoConnect is false), or it has since unmounted'
+          : 'this copy has never rendered a provider';
     const detail =
       foreignIds.length > 0
         ? `a DaemonWorkspaceProvider rendered from module copy ` +
           `${foreignIds.join(', ')}, but this hook resolved module copy ` +
           `${moduleInstanceId} — the page holds duplicate copies of the ` +
-          `DaemonWorkspaceProvider module`
+          `DaemonWorkspaceProvider module; ${ownDetail}`
         : …unchanged…

Measured with harness/candidate-fix.mjs (6/6, log logs/candidate-fix.log, witness 03-s5-finding-and-candidate-fix.png): s5 now reads …duplicate copies of the DaemonWorkspaceProvider module; this copy has also rendered a provider without an active client (e.g. autoConnect is false), or it has since unmounted; s1/s3/s4 messages byte-identical after normalising the random id suffix (zero collateral); s2 still carries both ids; provider suite 28/28 green. That last result is the unpinned-axis signal: the suite is green with and without the patch, so nothing pins the own-copy cause inside the duplicate message — the fix should ship with a fixture that renders a stale foreign copy plus an autoConnect={false} live copy and asserts the message names both facts (exactly s5-stale-copy.test.tsx).

F2 (Suggestion) — the new import.meta guard test has no positive control, so it cannot detect an over-broad guard

scripts/tests/export-html-import-meta-guard.test.js only exercises the injected-failure arm: it appends import.meta.url to document-main.tsx and asserts the build fails. Mutant M16 (allowlist widened to every file) leaves that test green while making every real export-document build fail (unmodified build exit=1 under the mutant). The missing half — "the unmodified build must still succeed" — is what separates a correctly-scoped guard from one that rejects everything. I supplied it as M0-build-positive-control (exit 0 at head) and as a second probe inside the M16/M17 rows. Suggested addition to the shipped test: after the injected run, spawn the build once more on the restored entry and assert status === 0 (or assert the allowlist matched ≥1 warning, which also proves the filter is not vacuously empty).

F3 (nit) — logLevel: 'error' is justified by a claim that is measurably false

See Correction 2. result.warnings carries 3/3 empty-import-meta entries at logLevel error, silent and info; mutant M17 ('error''silent') survives the shipped test and leaves the unmodified build at exit 0. The line only suppresses the tolerated warnings from the build log — a legitimate cosmetic goal, but the comment's stated reason ("would discard them and make that check vacuous") is wrong, and the same suppression means any future non-empty-import-meta warning also disappears from the log without being checked.

F4 (nit) — the URL-bearing id's stated benefit does not hold in the export-document build

See Correction 4. In the iife document runtime every copy's id degrades to web-shell/DaemonWorkspaceProvider#<random>, so "the message points at the offending chunk" is true only for the ES builds.

Observations that are NOT findings

  • The render-phase recordProviderCopy(id, false) side effect means a discarded concurrent render records 'rendered' for a provider that never committed; the message wording hedges for exactly that ("or has since unmounted"), and M4 shows the record is pinned. Behaviour matches the documented hedge in s3/s4.
  • Both bounded caps are 20, so a swapped cap constant at any of the six migrated call sites would be behaviourally invisible today; T9 audits the constants statically but no test pins them per call site.
  • The five remaining inline bounded loops in web-shell (useQueuedPrompts.ts ×2, turn-navigation-store.ts ×2, mappers.ts:594) are not the helper's shape — Sets, byte budgets, or multi-map eviction — except mappers.ts, a near-match left unmigrated. Not a defect; not exercised.

Reviewer Test Plan, walked step by step

plan step result
run the four named suites ran five (incl. bounded-map.test.ts): 74/74 pass (provider 28, daemon 27, main 8, fallback 7, bounded-map 4); body's "62" is stale (Correction 3)
"the provider suite covers every guard branch" true for the four documented branches (M1M5 each kill a named test); the s5 class (foreign id present + own-copy cause) is not covered — M18: provider suite 28/28 green with the candidate fix applied
"main.test.tsx covers retry-reloads-when-token-survives and retry-resets-in-place-when-not" reproduced: M9 (gate forced open) and M10 (gate forced shut) each turn the named tests red
"daemon.test.ts covers hasReloadSurvivableDaemonToken across hash/query/persisted/empty/throwing-storage" reproduced: M14 (predicate consults getDaemonToken()) and M15 (storage clause dropped) each turn the named tests red
"Mutation checks performed locally … each turns the corresponding test red; reverting restores green" independently reproduced 15/15 kills (M1M15), each with the red test named from the same file as the mutant, plus an unmutated control green on all six suites
"guard's message prefix is unchanged … build-artifact.test.ts checks still hold (transcript JS 1,180,562 bytes < 1,300,000; import.meta.url survives the ES lib build)" confirmed: prefix stable (G6), build-artifact.test.ts 17/17 green, JS remainder 1,188,177 bytes here (merge-commit build; still under the ceiling), 3 import.meta.url reads preserved (T5)
"the standalone vite build … is blocked by a pre-existing missing @tanstack/react-table" does not reproduce here (Correction 1); build succeeds and the bundle carries both changes (T8)

Mutation matrix (17 mutants + controls; witness 02-mutation-matrix-17-mutants.png)

Unmutated control green on all six suites (M0-*), including the export-build positive control. Killed = the designated suite went red on a named test in the mutated file.

mutant result red test(s)
M1 guard detail dropped KILLED 6 red in the diagnostics describe
M2 foreign-copy precedence removed KILLED reports duplicate module copies with both copy ids, reports duplicate copies even when this copy also rendered a provider
M3 'provided' record dropped KILLED reports the consumer as outside the subtree…, still names the no-active-client cause…
M4 render-phase record dropped KILLED reports a provider that rendered without an active client, never evicts the live copy…
M5 registry cap removed KILLED never evicts the live copy…, bounds the registry…
M6 MRU delete-before-set dropped KILLED moves a re-set key to the newest position before evicting
M7 sentinel reverted to !oldest KILLED evicts an empty-string key rather than stopping at it
M8 eviction loop removed KILLED 3 red in bounded-map.test.ts
M9 gate forced open KILLED falls back to an in-place reset when the token cannot survive a reload
M10 gate forced shut KILLED reloads the page…, reloads even without a survivable token…, carries the live theme…
M11 theme/language not re-added KILLED carries the live theme and language across a reload retry
M12 one-shot strip effect removed KILLED carries the live theme and language across a reload retry
M13 label plumbing dropped KILLED labels the retry button as a reload…, zh-CN copy test
M14 predicate consults getDaemonToken() KILLED is false when the in-memory cache holds a token a reload would lose
M15 storage clause dropped KILLED is true when a per-tab persisted token exists
M16 allowlist widened to every file SURVIVED (expected) — shipped test green while the real build breaks (exit=1); caught only by the unmodified-build positive control → F2
M17 logLevel'silent' SURVIVED (expected)result.warnings unaffected by logLevel → F3

Survivor classification: M16 is a coverage gap in the shipped test (the behaviour it would catch — an over-broad guard — is real and harmful, nothing asserts the accept path); M17 is not dead code (the line decides whether tolerated warnings print) but is not load-bearing for the guard, contrary to its comment. Neither is a merge condition; F2 is the actionable half.

Not covered

  • Per-commit attribution. The checkout is shallow: only the merge commit, base tip and PR head are reachable (git rev-list HEAD^1..HEAD^2 returns 1 while the metadata lists 6 commits). All claims are about the aggregate HEAD^1..HEAD diff.
  • The real dev-session duplicate-module trigger. The PR defers the root cause; my s2/s5 construct duplicates via query-suffixed module instances — the mechanism a bundler/HMR produces — so they reproduce the message behaviour under a duplicated module graph, not the observed Vite dev hiccup that created it.
  • chrome-extension iframe path and WebShellTranscript's embedded boundary. Both intentionally keep the in-place reset; neither was driven.
  • Playwright e2e suites (test:e2e*) — not run; evidence is unit/jsdom + real-loopback-daemon level.
  • The mount-time strip effect racing a concurrent navigation (a replaceState landing between the effect's read and write) — not probed.
  • logOverride: { 'empty-import-meta': 'silent' } — the only spelling of the comment's claim I did not measure; the three logLevel variants were.
  • Repo-wide test suite and the PR's own CI lanes — not re-run; gates cited are the affected workspace's typecheck, eslint (with a planted-violation liveness proof), and the affected suites.
  • hasReloadSurvivableDaemonToken against a stale-but-present sessionStorage token (revoked server-side) — out of scope by design; the predicate answers "could a fresh load authenticate", not "is the token still valid".

Methodology

Environment: the CI verify container (node v22.23.2, npm 10.9.8), working tree = refs/pull/11421/merge at depth 2 (npm ci + npm run build pre-run); base arm = scratch git worktree at HEAD^1 under tmp/, removed after the cells were captured. Guard-branch cells (harness/s1..s5-*.test.tsx) and reload cells (harness/r1..r3-*.test.tsx, shared support _shared.ts, _standalone.tsx) ran under each arm's own vitest config with VERIFY_ARM/VERIFY_OUT set; the only stubs are infrastructure boundaries (react-dom/client's default export, the leaf WorkspaceSessionProvider used as the crash trigger, and location.reload, which jsdom cannot perform — every other location read delegates to the live jsdom Location). The A/B oracle (harness/ab-oracle.mjs), mutation matrix (harness/mutation-matrix.mjs), candidate-fix probe (harness/candidate-fix.mjs) and gates (harness/gates.mjs) are plain Node scripts whose every line is a scripted comparison; their raw outputs and the per-cell JSONL live in logs/, and the four PNGs in evidence/ were rendered from those runs with scripts/verify-capture.mjs. Mutations were applied to a clean tree and restored with git checkout --, with a git status --porcelain assertion after every row; packages/web-templates/src/export-html/build.mjs and document-main.tsx were verified byte-identical by sha256 after each instrumented probe, and packages/web-shell/dist was rebuilt after the standalone vite build emptied it. Final tree state: clean.

Flakiness gate log

rounds=5 files=6 skipped=0
file packages/web-shell/client/components/RootErrorFallback.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/RootErrorFallback.test.tsx
file packages/web-shell/client/config/daemon.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/config/daemon.test.ts
file packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/daemon/workspace/DaemonWorkspaceProvider.test.tsx
file packages/web-shell/client/main.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/main.test.tsx
file packages/web-shell/client/utils/bounded-map.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/utils/bounded-map.test.ts
file scripts/tests/export-html-import-meta-guard.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/export-html-import-meta-guard.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/web-shell/client/components/RootErrorFallback.test.tsx: PPPPP
  packages/web-shell/client/config/daemon.test.ts: PPPPP
  packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: PPPPP
  packages/web-shell/client/main.test.tsx: PPPPP
  packages/web-shell/client/utils/bounded-map.test.ts: PPPPP
  scripts/tests/export-html-import-meta-guard.test.js: PPPPP

verdict: pass
summary: 6 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 1 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 1 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)
round 2 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 2 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 2 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)
round 3 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 3 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 3 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)
round 4 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 4 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 4 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)
round 5 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 5 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 5 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)

Evidence images

01-ab-guard-branches-and-reload-gate

02-mutation-matrix-17-mutants

03-s5-finding-and-candidate-fix

04-gates-and-shipped-artifacts

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

Partially reviewed — gaps disclosed.

⚠️ Round 5, and the diff has grown 5.8x since this review first measured it (84 → 488 source diff lines). The findings below are anchored to the current patch, so they can only say where this approach leaks — never that a different approach would retire all of them at once. Before fixing them, a human should decide whether the shape of the change is still right. Advisory only: this does not affect the verdict, and nothing here is a blocker.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R5-4 provider-copy registry liveness at packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:310 — already reported (round 4 deferral D4-1, review 5156783174)

Unresolved, please confirm:

  • [Critical] packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:27-50 — the open triage CHANGES_REQUESTED (review 5148889985) objects that the duplicate-module-copy diagnostic branch has no observed occurrence behind it (34 of the PR…

Not reviewed: issue-fidelity (closing-issue set) — review issue-context could not resolve the PR's strong closing-issue metadata because it requires gh >= 2.72.0 and this runner has gh 2.45.0, so whether this PR formally closes an issue is UNKNOWN rather than empty. Agent 0 covered the rest of the dimension: it fetched the referenced #11100 explicitly from its own repository, read the author's occurrence comment, replayed the narrated incident against the post-change workflow (it differs at three steps, so no Critical), and ruled on root-cause ownership..

Not explored to full depth (tool budget reached): "agent 5": did not open packages/web-shell/client/hooks/useSessionArtifacts.test.tsx to confirm whether its MAX_CACHED_SESSIONS = 20 eviction is pinned there..

Test Plan (not a blocker): client/main.test.tsxno such file or directory; client/components/RootErrorFallback.test.tsxno such file or directory; client/config/daemon.test.tsno such file or directory.

Convergence: round 5 posted 3 inline comment(s), 3 of them reported for the first time; the previous round posted 5 (5 new). Findings keep coming back to the same files: packages/web-templates/src/export-html/build.mjs (findings in round 4; 2 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

⚠️ 第 5 轮,且自本审查首次测量以来 diff 已增长 5.8 倍(源码 diff 行数 84 → 488)。下方的发现都锚定在当前这版补丁上,因此它们只能指出这个方案在哪里漏了,而无法说明换一个方案就能一次性消除全部问题。在动手修复之前,应由人来判断这次改动的整体形态是否仍然正确。仅供参考:本段不影响判定结论,其中也没有任何阻断项。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

未审查(原文为英文):issue-fidelity (closing-issue set) — review issue-context could not resolve the PR's strong closing-issue metadata because it requires gh >= 2.72.0 and this runner has gh 2.45.0, so whether this PR formally closes an issue is UNKNOWN rather than empty. Agent 0 covered the rest of the dimension: it fetched the referenced #11100 explicitly from its own repository, read the author's occurrence comment, replayed the narrated incident against the post-change workflow (it differs at three steps, so no Critical), and ruled on root-cause ownership..

未探索到全部深度(达到工具调用预算):"agent 5"did not open packages/web-shell/client/hooks/useSessionArtifacts.test.tsx to confirm whether its MAX_CACHED_SESSIONS = 20 eviction is pinned there.

Test Plan(非阻断):client/main.test.tsxno such file or directory; client/components/RootErrorFallback.test.tsxno such file or directory; client/config/daemon.test.tsno such file or directory

收敛情况:第 5 轮发布了 3 条行内评论,其中 3 条是首次提出;上一轮发布了 5 条(其中 5 条首次提出)。发现反复回到同一批文件:packages/web-templates/src/export-html/build.mjs(第 4 轮已出过发现,本轮又有 2 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

Comment thread scripts/tests/export-html-import-meta-guard.test.js Outdated
Comment thread packages/web-templates/src/export-html/build.mjs Outdated
Comment thread packages/web-templates/src/export-html/build.mjs Outdated
…cate and stop silencing the build

Round 5 found three problems with the round-4 guard wiring: the e2e test
drove the real build with an injected probe and left src/export-html/dist
wiped on every green run (the build rm -rf's it up front and only
repopulates on success), its existsSync bail-out reported a vacuous pass
instead of a skip on trees without the prebuilt transcript, the
file-path allowlist could not see a fourth import.meta read arriving
through that same prebuilt bundle, and logLevel: 'error' suppressed
every other warning class the build emits.

The guard is now a pure predicate in import-meta-guard.mjs — file
allowlist plus a count ratchet (the deliberate guarded ternary accounts
for exactly three empty-import-meta warnings; a fourth fails) — called
from build.mjs, unit-tested with fabricated warnings. The e2e half keeps
driving the real build but snapshots and restores the dist directory,
restores the probed file in finally, and uses describe.skipIf so a
missing prebuilt transcript reports skipped, not passed. logLevel is
gone: warnings print at the default level again while
documentBuildResult.warnings stays populated for the guard.

Mutation checks: removing the ratchet reds the fourth-read unit case and
the transcript-probe e2e; disconnecting the guard call reds both e2e
cases; reverting to logOverride: 'silent' reds the document-entry e2e.

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Blocking finding(s) follow.

Partially reviewed — gaps disclosed.

⚠️ Round 6, and the diff has grown 6.4x since this review first measured it (84 → 541 source diff lines); the reverse audit also stopped at its round cap without converging. The findings below are anchored to the current patch, so they can only say where this approach leaks — never that a different approach would retire all of them at once. Before fixing them, a human should decide whether the shape of the change is still right. Advisory only: this does not affect the verdict, and nothing here is a blocker.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • DUP6-1 provider-copy registry staleness outranking the own-state diagnosis at packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:311 — already reported (round-4 deferral D4-1, review 5156783174; re-confirmed and dropped …

Unresolved, please confirm:

  • [Critical] packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:27-50 — the open triage CHANGES_REQUESTED (review 5148889985) objects that the duplicate-module-copy diagnostic branch has no observed occurrence behind it (34 of the PR…

Not reviewed: issue-fidelity (closing-issue set) — review issue-context requires gh >= 2.72.0 and this runner has gh 2.45.0, so whether this PR formally closes an issue is UNKNOWN rather than empty; Agent 0 covered the rest of the dimension (it fetched the referenced #11100 from its own repository, read its body and all 5 comments, replayed the narrated incident against the post-change workflow — three steps differ, so no replay Critical — and ruled root-cause ownership client-side).

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Test Plan (not a blocker): client/main.test.tsxno such file or directory; client/components/RootErrorFallback.test.tsxno such file or directory; client/config/daemon.test.tsno such file or directory.

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:

  • packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx:644 — [probe] 'never evicts the live copy when foreign copies accumulate past the cap' accumulates the eight foreign copies with a raw registry.set() that never run…
  • packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.tsx:42 — [probe] The fallback branch of the new moduleUrl derivation — the one that exists for the iife export-document build, where esbuild lowers import.meta to {} —…
  • packages/web-shell/client/main.test.tsx:185 — [probe] The assertion that pins 'the reload must land on the live session URL, not a stale snapshot' cannot fail for a stale-snapshot implementation, because…
  • packages/web-shell/client/main.tsx:164 — [probe] The new mount effect's third key (lang) is exercised by no test anywhere in the repository, so deleting that line ships green while the exact preferen…
  • packages/web-shell/client/main.tsx:165 — [probe] The before/url.search comparison added to keep the new mount-time strip from writing history when it stripped nothing is dead for any query carrying u…
  • packages/web-templates/src/export-html/build.mjs:226 — [probe] The new import.meta throw is the first of the build's gates, so it preempts the purpose-written FORBIDDEN_DOCUMENT_INPUTS diagnostic on exactly the #1…
  • packages/web-templates/src/export-html/build.mjs:229 — [review] The PR's stated scope ('Two changes to the standalone Web Shell's failure surface') and its Test Plan omit the rest of the diff: a new release-gating…
  • scripts/tests/export-html-import-meta-guard.test.js:119 — [probe] The e2e helper appends a probe line to tracked source (src/export-html/src/document-main.tsx) and restores it only in a finally, so an abnormal termin…
  • scripts/tests/export-html-import-meta-guard.test.js:131 — [probe] The e2e helper snapshots and restores only src/export-html/dist, but the build it drives writes a second, hash-coupled product — src/generated/exportT…
  • scripts/tests/export-html-import-meta-guard.test.js:143 — [probe] The artifact assertion runs after runBuildWithProbe's finally has already restored the snapshot, so it measures the test's own restoreDist() rather th…
  • scripts/tests/export-html-import-meta-guard.test.js:154 — [probe] The only e2e case pinning the over-count ratchet asserts a bare non-zero exit and never captures stdout/stderr, so any unrelated build failure — inclu…

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

⚠️ 第 6 轮,且自本审查首次测量以来 diff 已增长 6.4 倍(源码 diff 行数 84 → 541);反向审计也在轮数上限处停止且未收敛。下方的发现都锚定在当前这版补丁上,因此它们只能指出这个方案在哪里漏了,而无法说明换一个方案就能一次性消除全部问题。在动手修复之前,应由人来判断这次改动的整体形态是否仍然正确。仅供参考:本段不影响判定结论,其中也没有任何阻断项。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

未审查(原文为英文):issue-fidelity (closing-issue set) — review issue-context requires gh >= 2.72.0 and this runner has gh 2.45.0, so whether this PR formally closes an issue is UNKNOWN rather than empty; Agent 0 covered the rest of the dimension (it fetched the referenced #11100 from its own repository, read its body and all 5 comments, replayed the narrated incident against the post-change workflow — three steps differ, so no replay Critical — and ruled root-cause ownership client-side).

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

Test Plan(非阻断):client/main.test.tsxno such file or directory; client/components/RootErrorFallback.test.tsxno such file or directory; client/config/daemon.test.tsno such file or directory

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 11 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (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: 61 passed · 2 failed · 63 total

Flakiness gate: ✅ 6 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

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

脚本断言:61 通过 · 2 失败 · 63 总计

抖动门:✅ 6 changed test file(s) x 5 identical rounds, no divergence

Verification report

Verify report — PR 11421 (QwenLM/qwen-code) — follow-up round

Verdict: findings — 61/63 scripted assertions passed, 2 unexpected failures, both of them findings below (G7-head-s5-true-cause = F1, GM20-ratchet-down-killed = F5). Verified head 23b018e8d81dab55e2eb9d3b1fb032dfbda8d049 (= HEAD^2, matches the snapshot's headRefOid), base 33a4062591bcbe3c382174546729f0df79243a2b (= HEAD^1). Note the snapshot's baseRefOid (2e212144…, the base the previous round measured against) has drifted: the merge ref was recomputed against a newer main, so this round's A/B is against a different base tip and every carried-forward measurement was re-run rather than diffed.

The central claim passed its A/B again at the new head and new base: base emits one indistinguishable bare message across all five failure scenarios, head names four distinct classes correctly (table 1, witness 01-ab-guard-branches-base-vs-head.png). The reload gate also re-measured clean: 2 of 3 scenarios flip reset→reload and the memory-only-token control correctly does not (table 2, witness 02-ab-reload-gate-cells.png). The round-5 delta fixed the previous round's F2 for the allowlist (the same mutant is now killed) and removed the logLevel line F3 objected to — but it left the ratchet constant with the identical gap F2 described (F5), and the comment that replaced the logLevel justification states a mechanism that is measurably false (F6).

中文摘要
  • 结论: findings。63 条脚本化断言中 61 条通过,2 条失败,即下面的 F1 与 F5。已验证 head 23b018e8(= HEAD^2),base 33a40625(= HEAD^1)。快照里的 baseRefOid2e212144,上一轮的 base)已经漂移——merge ref 是相对更新的 main 重新计算的,所以本轮所有沿用的测量都在新 head/新 base 上重跑,没有对比旧报告。
  • A/B 结论(见表 1、表 2,不在此复述数字): 核心主张在新 head/新 base 上再次通过 A/B——base 臂 5 个失败场景只有一条无法区分的裸消息,head 臂正确命名 4 个类别;重试门控 3 个场景中 2 个从"原地重置"翻转为"刷新页面",内存 token 的对照场景正确保持不变,一次性 URL 参数的"挂载时抹掉 + 刷新时带回"往返在两个臂上都可见。
  • Findings: F1(上一轮的主要发现,仍然成立)重复副本分支无条件优先,任何曾出现过第二个模块副本的页面此后所有守卫报错都说 "duplicate copies",丢掉注册表已知的本副本原因;已实测的最小修复仍然适用(s5 变干净、s1/s3/s4 字节一致、单测两侧同为 31 通过 0 失败——即该轴仍未被钉住)。F5(新)计数棘轮常量没有构建级正对照:把它调低到真实值以下会让每一次真实导出构建失败,而出厂测试套件仍 7/7 全绿——因为单测用符号 {length: TOLERATED…} 构造用例,会随常量一起缩放,永远抓不到错误的值;缓解事实是向上留余量已被钉住,且 npm run build 会响亮地失败。F6(新,属更正)替换 logLevel 的新注释断言 logOverride 'silent' 会清空 result.warnings;实测 esbuild 直接抛 "logOverride" must be an object,根本到不了 warnings;真正会清空的是合法的 Record 写法(3 → 0 条),而出厂套件确实能抓到它。
  • 未覆盖范围: 逐 commit 归因(浅克隆,本地只有 1 个 commit 而元数据列了 7 个;上一轮的 head 444d1cbd 与 base 2e212144 都不可达,所以无法直接 diff 出增量);dev 会话模块双副本的真实根因;chrome-extension iframe 路径与 WebShellTranscript 内嵌边界;Playwright e2e;独立版 vite build 的重跑(只复验了依赖存在);build-artifact.test.ts;仓库全量测试与 PR 自身 CI lane;整个 scripts/tests 套件的并发实跑。

Previous-finding status at the new head

The previous round measured head 444d1cbde32ab0bc2100c599b10972f4b2245d5c against base 2e212144…. Neither that head nor that base is reachable in this depth-2 checkout (git cat-file -t fails for both), so the delta could not be diffed directly; it was scoped from the round-5 commit message and then everything below was re-measured from scratch at 23b018e8 / 33a40625.

# previous finding severity status at 23b018e8
F1 duplicate-copy verdict is sticky and swallows the cause the registry already knows Suggestion STANDS — re-measured (G7 fails; s5 message shape unchanged). Candidate fix re-measured, still applies (11/11).
F2 the import.meta guard test has no positive control, so it cannot detect an over-broad guard (M16 survived) Suggestion FIXED for the allowlist — the same mutant (M19) is now killed by flags reads from any other file; the accept path has 3 unit cases. Re-opened for the ratchet constant as F5.
F3 logLevel: 'error' justified by a measurably false claim nit FIXED (superseded)logLevel is gone and warnings print at the default level. The replacement comment introduces a new false claim → F6.
F4 the URL-bearing id's stated benefit does not hold in the export-document build nit STANDS — re-measured: the iife bundle has 0 import.meta occurrences and 1 of the constant fallback web-shell/DaemonWorkspaceProvider (S-iife-no-import-meta).
Corr. 1 "vite build blocked by a missing @tanstack/react-table" does not reproduce correction STANDS (partially re-measured)@tanstack/react-table@8.21.3 is present in the clean npm ci tree (S-react-table-present). The 23 s standalone build was not re-run this round.
Corr. 2 logLevel does not affect result.warnings; logOverride is a Record correction STANDS and extended — re-measured all four spellings; see F6 for the new half.
Corr. 3 "62 tests" is stale correction STANDS, number moved again — the five suites now hold 77 (provider 31, daemon 27, main 8, fallback 7, bounded-map 4). The provider suite grew 28→31 with main; the body's "62" predates round 4.
Corr. 4 iife id degrades to a constant correction STANDS — same measurement as F4.
"Not findings" render-phase record, both caps = 20, five unmigrated inline bounded loops observation Not re-measured — listed under Not covered.

No previously declined or deferred row exists, so there is no worsened to report.

Central claim and its A/B

Central claim — the strict useDaemonWorkspace guard now reports which situation it detected via a globalThis breadcrumb registry: no provider rendered / provider from a foreign module copy (both ids) / same copy but consumer outside its live subtree / same copy rendered without an active client.

Harness harness/guard-ab.test.tsx, copied byte-identically into both arms (sha256 5037228d… on both) and run under each arm's own vitest config. Nothing on the path from provider to guard is stubbed: the real DaemonWorkspaceProvider, the real DaemonClient from @qwen-code/sdk/daemon, and a real loopback HTTP daemon that counts requests. The duplicate-copy cells obtain a genuine second module instance via a query-suffixed import (…DaemonWorkspaceProvider.js?copy=b) — the mechanism a bundler/HMR uses — and the harness asserts copyA !== copyB before any scenario runs (G0-distinct-copies), so the registry is never hand-seeded.

Control integrity: the base arm is a scratch git worktree at HEAD^1 (removed after the cells were captured). It has no node_modules of its own, so internal deps resolve upward to the root; readlink -f node_modules/@qwen-code/sdk/__w/qwen-code/qwen-code/packages/sdk-typescript, and git diff --name-only HEAD^1..HEAD -- packages/sdk-typescript is empty, so the one cross-tree dependency is untouched by the PR. The code under test is arm-local and demonstrably different per arm: DaemonWorkspaceProvider.tsx sha256 b8cc09dc… (base) vs 8a5a08a0… (head). Raw cells logs/ab-head.jsonl, logs/ab-base.jsonl; oracle harness/ab-oracle.mjs.

Table 1 — guard message per failure class (witness 01-ab-guard-branches-base-vs-head.png)

scenario base arm (33a40625) head arm (23b018e8)
s1 no provider anywhere bare message, no class no DaemonWorkspaceProvider has rendered in this page
s2 provider rendered from a 2nd module copy bare message, no class …the page holds duplicate copies of the DaemonWorkspaceProvider module with both ids (G3)
s3 same copy provided, consumer outside its unmounted subtree bare message, no class …this consumer is outside its live subtree…
s4 same copy, autoConnect={false} bare message, no class …rendered without an active client (e.g. autoConnect is false)…
s5 stale 2nd copy + autoConnect={false} (true cause = no client) bare message, no class …duplicate copies…the no-client cause is absent (F1)

Cells: base 1 distinct message across 5 scenarios (the control is red exactly as predicted, so G1-base-control passes); head 4/4 distinct across s1–s4 (G6). Positive control run symmetrically on both arms: the real loopback daemon was reached in s2/s3/s5 on head and base (daemonRequests=1 in all six cells, G9-*), so neither arm's cells are vacuous. Prefix stability (G8): all ten messages start with the pre-existing useDaemonWorkspace must be used within DaemonWorkspaceProvider, so existing substring assertions still hold.

Secondary claim 1 — the retry reloads iff a reload cannot strand a credential, with an honest label. Harness harness/reload-ab.test.tsx drives the real StandaloneApp, the real ErrorBoundary and the real RootErrorFallback; the only stubs are infrastructure boundaries (the leaf WorkspaceSessionProvider as the crash trigger, the SDK provider as a passthrough, config/daemon so both arms see the same token world, and location.reload, which jsdom cannot perform). Every other location read goes through a Proxy that delegates to the live jsdom Location, so urlAfter is the real post-replaceState URL rather than a snapshot frozen at stub time — a first attempt with {...window.location, reload} silently reported urlChanged:false on a cell that had in fact written the URL, and was replaced.

Table 2 — retry behaviour (witness 02-ab-reload-gate-cells.png)

scenario base: label / reload / remount / ?search after mount head: label / reload / remount / ?search after mount
r1 token survived boot in per-tab storage Try again / 0 / true / ?theme=light&language=en Reload page / 1 / false / ""
r2 token in memory only (cannot survive) Try again / 0 / true / ?theme=light&language=en Try again / 0 / true / ""
r3 no token at boot (trusted loopback) Try again / 0 / true / ?theme=light&language=en Reload page / 1 / false / ""

2/3 cells flip reset→reload; the memory-only control correctly does not (R-flip-count). The one-shot param round trip is visible end-to-end on both arms (R-onestrip-roundtrip): base keeps ?theme=…&language=… in the URL after mount, head strips them at mount (searchAfterMount="") and re-adds the live values on the reload URL (…/?theme=light&language=en).

Secondary claim 2 — the round-5 delta: the import.meta build guard is a tested predicate. This is the only code the delta touches, so it got the round's second mutation matrix, and every mutant is paired with two oracles: the shipped suite and the real unmodified export-document build. That pairing is what separates a correctly-scoped guard from one that rejects everything (harness/guard-matrix.mjs, witness 03-guard-mutation-matrix-with-build-control.png).

mutant shipped suite real build verdict
M0 control 7/7 green exit 0 control green — the guard accepts a correct tree
M18 ratchet clause removed 2 red (flags a fourth read…, fails the build on a fourth…) exit 0 KILLED — matches the commit's claim
M19 allowlist widened to every file (= previous round's M16) 1 red (flags reads from any other file) exit 0 KILLED — F2 fixed
M20 ratchet constant 3 → 0 7/7 green exit 1, names the guard SURVIVED → F5
M21 ratchet constant 3 → 9 1 red (fails the build on a fourth…) exit 0 KILLED — upward slack is pinned
M22 guard call disconnected 2 red (both e2e cases) exit 0 KILLED — matches the commit's claim
M23 logOverride: 'silent' re-added 1 red (fails the build when an import.meta read lands in the document entry) exit 1, not the guard KILLED, but for the wrong reason → F6
M24 logOverride: { 'empty-import-meta': 'silent' } 2 red (both e2e cases) KILLED — the suite does catch the spelling that really vacates the check

The ratchet's arithmetic checks out against reality, not just against its own comment: the real build emits exactly 3 empty-import-meta warnings, all from ../web-shell/dist/transcript.js at 13513:35, 13513:66, 13513:84 (G-ratchet-matches-reality), and the prebuilt bundle contains exactly 3 import.meta.url reads in the deliberate ternary typeof import.meta.url == "string" && import.meta.url ? import.meta.url : "web-shell/DaemonWorkspaceProvider" (S-es-transcript-keeps-reads) — three occurrences, matching TOLERATED_TRANSCRIPT_IMPORT_META_READS = 3.

The round-5 restore claim was also tested by repetition rather than by one green run: 3 consecutive runs of the suite left all seven watched artifacts byte-identical, including web-shell/dist/transcript.js (which the second e2e case appends a probe line to) and src/generated/exportTranscriptDocumentTemplate.ts (H-repeated-runs-clean), and the e2e half really executed here rather than reporting a vacuous skip (7/7 each run, H-e2e-not-skipped).

Corrections to the description

Statements in the PR text or in comments the measurements contradict. None asks for a code change beyond the note attached.

  1. "logOverride 'silent' would empty result.warnings and vacate this check"packages/web-templates/src/export-html/build.mjs:223-225. Measured false in mechanism (harness/logoverride-probe.mjs, log logs/logoverride-probe.log): esbuild rejects that value outright with "logOverride" must be an object, so the build dies before any warning is collected and result.warnings is never reached. The typings agree (node_modules/esbuild/lib/main.d.ts: logOverride?: Record<string, LogLevel>). The spelling that does what the comment fears is the valid Record form — logOverride: { 'empty-import-meta': 'silent' } takes result.warnings from 3 to 0 — and the shipped suite does catch it (M24, both e2e cases red). So the guard is correctly defended; only the comment's explanation names an impossible spelling. Suggested wording: "No log suppression here: logOverride: {'empty-import-meta': 'silent'} would empty result.warnings and vacate this check, and logLevel would hide every other warning class this build emits from the log."
  2. The round-5 commit's mutation check "reverting to logOverride: 'silent' reds the document-entry e2e" is true but vacuous. The mutant is killed because esbuild crashes on an invalid option, not because the check was vacated — and any edit that makes the build crash reds that same test. It therefore provides no evidence about the guard. M24 is the mutation that does.
  3. "logLevel does not affect result.warnings" (previous round's Correction 2) re-measured at this head and confirmed: logLevel 'silent' and 'error' both still yield 3 warnings. Removing the line was cosmetic, as the previous round said.
  4. "62 tests, all green locally" for the four named suites — at this head the five suites hold 77 (provider 31, daemon 27, main 8, fallback 7, bounded-map 4), all green. The body's number predates round 4; the previous round's 74 predates the provider suite growing 28→31 with main.
  5. "the standalone vite build in this worktree is blocked by a pre-existing missing @tanstack/react-table install" — still does not reproduce in a clean npm ci tree: the package is present at 8.21.3. (Only the dependency's presence was re-checked this round; the build itself was not re-run.)
  6. "the id carries the module URL so the message points at the offending chunk" — true for the ES builds, false for the export-document build, where the iife lowering leaves zero import.meta occurrences and every copy's id degrades to web-shell/DaemonWorkspaceProvider#<random>. The fallback is exactly what prevents a crash there; the comment overstates the benefit.

Findings

F1 (Suggestion, carried forward — STANDS) — the duplicate-copy verdict is sticky and swallows the cause the registry already knows

useDaemonWorkspace computes detail with foreignIds.length > 0 as the first branch, and that branch reports only the duplicate-copy fact. The registry also holds ownState for the hook's own copy ('rendered' vs 'provided'), but once any foreign id exists ownState is never consulted — so a page whose live copy rendered with autoConnect={false} is told "the page holds duplicate copies of the DaemonWorkspaceProvider module" and nothing about the client.

The registry entry for a dead copy is sticky: in the exact environment where the failure was observed (a Vite dev session), any hot re-evaluation of the provider module mints a new moduleInstanceId and leaves the old one in the registry until 8 newer ids evict it. From that moment every guard failure in that page — whatever its true class — reports "duplicate copies". The PR states the diagnostic exists "to confirm or refute [the duplicate-module hypothesis] on the next occurrence instead of guessing again"; with this precedence it can only ever confirm it. The author's own test pins the precedence deliberately, so this is a design consequence, not an oversight — but it defeats the stated purpose in the environment that produced the bug.

Re-measured at 23b018e8 with no synthetic registry seeding — the stale copy is a real second module instance that rendered a provider against a real loopback daemon (daemonRequests=1) and then unmounted:

D=tmp/pr11421-verify-20260910-062120
cp $D/harness/guard-ab.test.tsx packages/web-shell/client/__verify__/
cd packages/web-shell && VERIFY_ARM=head VERIFY_OUT=/tmp/s.jsonl \
  npx vitest run --config vitest.config.ts client/__verify__/guard-ab.test.tsx
# s5 -> "...the page holds duplicate copies of the DaemonWorkspaceProvider module)"
#       no mention of the operative autoConnect={false} cause   (assertion G7-head-s5-true-cause FAILS)

Blast radius: every consumer of useDaemonWorkspace / useDaemonWorkspaceActions / useDaemonSessionArtifacts that can throw the guard in a dev page after ≥1 HMR of the provider module. The message is the only output, so the misdiagnosis is what a developer reads. Not blocking: no functional regression, and base emits the bare message for the same scenario, so this is a head-only diagnostic-accuracy defect rather than a regression.

Minimal suggested fix — re-measured at this head (11/11), preserves the original intent

Keep the duplicate-copy verdict, append the own-copy state so the message can never hide a cause the registry already holds (harness/fix-anchor.txt, harness/fix-replacement.txt):

+    const ownDetail =
+      ownState === 'provided'
+        ? 'this copy has also rendered a provider, so this consumer is ' +
+          'outside its live subtree, that provider has unmounted, or it ' +
+          'currently has no active client (autoConnect is false)'
+        : ownState === 'rendered'
+          ? 'this copy has also rendered a provider without an active ' +
+            'client (e.g. autoConnect is false), or it has since unmounted'
+          : 'this copy has never rendered a provider';
     const detail =
       foreignIds.length > 0
         ? `a DaemonWorkspaceProvider rendered from module copy ` +
           `${foreignIds.join(', ')}, but this hook resolved module copy ` +
           `${moduleInstanceId} — the page holds duplicate copies of the ` +
-          `DaemonWorkspaceProvider module`
+          `DaemonWorkspaceProvider module; ${ownDetail}`
         : …unchanged…

Measured with harness/candidate-fix.mjs (log logs/candidate-fix.log, witness 05-f1-candidate-fix-measured.png): s5 becomes …duplicate copies of the DaemonWorkspaceProvider module; this copy has also rendered a provider without an active client (e.g. autoConnect is false), or it has since unmounted (C5), and still reports the duplicate fact (C6); s1/s3/s4 are byte-identical after normalising the random id suffix (C8-*, zero collateral); s2 still carries both ids (C7); the harness stays 6/6 green (C4); the provider source restores byte-identical (C3).

The suite result is the unpinned-axis signal, and it reproduces at this head: the shipped provider suite is 31 passed / 0 failed both with and without the patch (C9). Nothing pins the own-copy cause inside the duplicate message, so the fix should ship with a fixture that renders a stale foreign copy plus an autoConnect={false} live copy and asserts the message names both facts — exactly harness/guard-ab.test.tsx's s5.

F5 (Suggestion, new) — the ratchet constant has no build-level positive control, so a wrong value in the safe-looking direction is invisible to its own suite

TOLERATED_TRANSCRIPT_IMPORT_META_READS = 3 (import-meta-guard.mjs:22) is the guard's only defence against a fourth import.meta read arriving through the tolerated bundle. Lower it below the real count and every real export-document build fails, while the shipped suite stays 7/7 green (mutant M20, constant → 0: suite green, real build exit 1 naming unexpected import.meta use).

The reason is structural, not an oversight in one test: both ratchet unit cases build their fixtures from the constant itselfArray.from({ length: TOLERATED_TRANSCRIPT_IMPORT_META_READS }) for the accept case and { length: TOLERATED_… + 1 } for the reject case (test file lines 52 and 60). They therefore scale with whatever value the constant holds and can never assert that the value is right. The two e2e cases only ever drive builds that are expected to fail, so neither one observes the accept path of a correct tree.

This is the same shape as the previous round's F2, one constant over: F2 was fixed for the allowlist (M19 is now killed) and the fix added unit coverage for the predicate, but the missing half — "the unmodified build must still succeed" — is still supplied only by my M0-build-positive-control, not by the shipped suite.

Bounded, so the severity is a Suggestion rather than a blocker:

  • Upward slack is pinned — M21 (3 → 9) is killed by fails the build on a fourth import.meta read inside the prebuilt transcript bundle.
  • A too-low constant fails loudly in any build lane: packages/web-templates's build is node build.mjs && tsc --build --clean && tsc, and packages/web-templates/build.mjs:20 invokes src/export-html/build.mjs, so npm run build breaks immediately. The gap is that this guard's own suite cannot tell a correct ratchet from a broken one — not that the breakage would ship silently.

Suggested addition (one e2e case, no new mechanism): after the two injected-probe runs, spawn the build once more on the restored entry and assert status === 0. That single case kills M20 and is the positive control F2 asked for. A tighter variant also pins the constant to reality: assert documentBuildResult.warnings.filter(w => w.id === 'empty-import-meta').length === TOLERATED_TRANSCRIPT_IMPORT_META_READS, which additionally catches the silent-weakening direction (if the deliberate ternary is ever refactored to two reads, a stray third read in the same bundle would slip through).

F6 (nit, new) — the comment that replaced the logLevel justification states a mechanism that cannot occur

See Correction 1. build.mjs:223-225 justifies the absence of log suppression by claiming logOverride 'silent' "would empty result.warnings and vacate this check". esbuild rejects that value with "logOverride" must be an object; it never reaches the warnings array. The hazard the comment is reaching for is real but has a different spelling (logOverride: { 'empty-import-meta': 'silent' }, measured 3 → 0 warnings), and the suite already catches that one (M24). Worth fixing because this comment is the only documentation of why the build prints its warnings: the previous round already flagged this exact spelling as invalid (its Correction 2) and listed measuring it under Not covered, and the round-5 commit then promoted that unmeasured spelling into the comment as the justification — so the next reader who trusts it will reason incorrectly about esbuild's options.

F4 (nit, carried forward — STANDS) — the URL-bearing id's benefit does not hold in the export-document build

See Correction 6. Re-measured: the iife export bundle contains 0 import.meta occurrences and 1 occurrence of the constant fallback, so in exported documents copy ids differ only by their random suffix. Not a defect — the fallback is what prevents a crash there — but the comment overstates what the id buys.

Consequences checked that do NOT hold

Reported so they are not assumed from the findings above:

  • The new e2e test does not poison the worktree. Three consecutive runs left all seven watched artifacts byte-identical, including the two the real build wipes and the one it appends a probe to (H-repeated-runs-clean). The round-5 restore claim holds.
  • It does not race the parallel scripts/tests files. scripts/tests/vitest.config.ts runs files in parallel, and this test is a new writer into two shared artifacts, so I enumerated every other file that references those paths: package-assets.test.js, install-script.test.js and core-subpath-exports-resolution.test.js. All three operate on a temporary fixture root (createFixtureRoot() / distPath), never the real tree, so there is no collision. (Enumerated by reading, not by a concurrent full-suite run — see Not covered.)
  • The e2e half is not vacuously skipped here. describe.skipIf(!hasPrebuiltTranscript) really runs in this container: 7/7 tests, 0 skipped, on every run (H-e2e-not-skipped). A tree without the prebuilt transcript would report skipped rather than passed, which is the behaviour the round-5 commit asked for.
  • The build does not delete the generated document template on a failing run. build.mjs:16 removes the legacy exportHtmlTemplate.ts, not exportTranscriptDocumentTemplate.ts; the current template is only overwritten on success. I initially suspected the same wipe-the-tree bug the round fixed for dist/, measured it, and it does not hold.

Reviewer Test Plan, walked step by step

plan step result
run the four named suites ran five (incl. bounded-map.test.ts): 77/77 pass (provider 31, daemon 27, main 8, fallback 7, bounded-map 4). Body's "62" is stale (Correction 4).
"the provider suite covers every guard branch" true for the four documented branches; the s5 class (foreign id present + own-copy cause) is still not covered — the suite is 31/0 both with and without the F1 fix (C9).
"main.test.tsx covers retry-reloads-when-token-survives and retry-resets-in-place-when-not" reproduced at this head: M25 (gate forced open) and M26 (gate forced shut) each turn the named tests red; control 8/8 green (witness 04-reload-gate-mutation-proof-5-of-5.png).
"daemon.test.ts covers hasReloadSurvivableDaemonToken across hash/query/persisted/empty/throwing-storage" 27/27 green; the predicate's effect on behaviour is pinned end-to-end by table 2 rather than re-mutated this round.
"Mutation checks performed locally … each turns the corresponding test red" independently reproduced for the delta: M18/M22 (claimed) killed; M23 (claimed) killed but for the reason in F6, not the claimed one; M19/M21/M24 (not claimed) killed; M20 (not claimed) survived → F5. For the reload half: M25–M29 5/5 killed, each on a named test in the mutated file.
"the guard's message prefix is unchanged … build-artifact.test.ts checks still hold (transcript JS 1,180,562 bytes < 1,300,000; import.meta.url survives the ES lib build)" prefix confirmed stable across all ten cells on both arms (G8); 3 import.meta.url reads survive in dist/transcript.js (S-es-transcript-keeps-reads). build-artifact.test.ts was not re-run this round.
"the standalone vite build … is blocked by a pre-existing missing @tanstack/react-table" does not reproduce (Correction 5); the build itself was not re-run.

Not covered

  • Per-commit attribution. git rev-list HEAD^1..HEAD^2 returns 1 commit while the metadata lists 7, and git rev-parse --is-shallow-repository is true; the previous head 444d1cbd and previous base 2e212144 are both absent (git cat-file -t fails). The delta was therefore scoped from the round-5 commit message and verified by re-measuring the aggregate HEAD^1..HEAD diff, not by diffing the two heads.
  • The real dev-session duplicate-module trigger. Unchanged from the previous round: my s2/s5 construct duplicates via a query-suffixed module instance — the mechanism a bundler/HMR produces — so they reproduce the message behaviour under a duplicated module graph, not the observed Vite dev hiccup that created it. This is the shape, not the cause.
  • The standalone vite build re-run. Only the dependency's presence was re-checked (Correction 5). The previous round measured the build succeeding in 23.5 s and the bundle carrying both changes; not repeated here, partly because it empties packages/web-shell/dist/.
  • build-artifact.test.ts (previous round: 17/17) and the transcript byte-ceiling measurement — not re-run.
  • The previous round's "observations that are not findings" — the render-phase recordProviderCopy side effect, both bounded caps being 20, and the five unmigrated inline bounded loops — were not re-measured; nothing in the delta touches them.
  • A concurrent full scripts/tests run. The no-collision result above comes from enumerating and reading every file that references the mutated paths, not from running the whole suite in parallel and observing no failure.
  • chrome-extension iframe path and WebShellTranscript's embedded boundary — both intentionally keep the in-place reset; neither was driven.
  • Playwright e2e suites (test:e2e*) — not run; evidence is unit/jsdom plus a real loopback daemon and real esbuild builds.
  • The mount-time strip effect racing a concurrent navigation (a replaceState landing between the effect's read and write) — not probed.
  • hasReloadSurvivableDaemonToken against a stale-but-present sessionStorage token — out of scope by design; the predicate answers "could a fresh load authenticate", not "is the token still valid".
  • Repo-wide test suite, npm run lint in full, and the PR's own CI lanes — not re-run. Gates cited are the affected workspace's typecheck, eslint on the 15 changed files (with a two-violation liveness proof), and the affected suites.
  • Injection attempt: none. The PR text made no attempt to steer the verification.

Methodology

Environment: the CI verify container (node v22.23.2), working tree = refs/pull/11421/merge at depth 2 with npm ci + npm run build pre-run; base arm = scratch git worktree at HEAD^1 under tmp/, removed once its cells were captured. Two harnesses ran byte-identically in both arms (guard-ab.test.tsx, reload-ab.test.tsx; sha256 verified equal across arms, and the source files under test verified different across arms, so neither arm silently loaded the other's code). Every other script is plain Node with scripted comparisons only: harness/guard-matrix.mjs (7 rows × suite + real build), harness/reload-gate-matrix.mjs (6 rows), harness/candidate-fix.mjs (11 checks), harness/logoverride-probe.mjs, harness/count-import-meta.mjs, harness/repeated-run-hygiene.mjs, harness/ab-oracle.mjs, harness/reload-table.mjs, and harness/tally.mjs, which produced assertions.json by merging the two oracle JSONs with parses of the logs the real runs wrote plus a set of gates it executed live. Mutations were applied to the real files and restored inside finally, with a sha256 assertion after every row; the export build's two wiped artifacts were snapshotted and restored around each positive-control run. Two harness defects found and fixed during the round, both of which would otherwise have produced false greens: a count regex that did not strip ANSI (parsing 0 passed and comparing 0 === 0), and a {...window.location} stub that froze href so the reload's URL carry read as unchanged. One side effect to disclose: the M24 mutant makes the export build succeed, which rewrote the gitignored src/generated/exportTranscriptDocumentTemplate.ts with the probe's bundle; it was restored from a pre-run backup and verified byte-identical (94b9b83c…). Images 0105 in evidence/ were rendered with scripts/verify-capture.mjs; 01 and 02 capture live re-runs, 0305 render the stored stdout of the executed runs (logs/guard-matrix.log, logs/reload-gate-matrix.log, logs/candidate-fix.log). Raw per-cell data: logs/ab-{head,base}.jsonl, logs/reload-{head,base}.jsonl. Final tree state: git status --porcelain empty and all pre-run artifact hashes verified unchanged.

Flakiness gate log

rounds=5 files=6 skipped=0
file packages/web-shell/client/components/RootErrorFallback.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/RootErrorFallback.test.tsx
file packages/web-shell/client/config/daemon.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/config/daemon.test.ts
file packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/daemon/workspace/DaemonWorkspaceProvider.test.tsx
file packages/web-shell/client/main.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/main.test.tsx
file packages/web-shell/client/utils/bounded-map.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/utils/bounded-map.test.ts
file scripts/tests/export-html-import-meta-guard.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/export-html-import-meta-guard.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/web-shell/client/components/RootErrorFallback.test.tsx: PPPPP
  packages/web-shell/client/config/daemon.test.ts: PPPPP
  packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: PPPPP
  packages/web-shell/client/main.test.tsx: PPPPP
  packages/web-shell/client/utils/bounded-map.test.ts: PPPPP
  scripts/tests/export-html-import-meta-guard.test.js: PPPPP

verdict: pass
summary: 6 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 1 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 1 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)
round 2 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 2 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 2 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)
round 3 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 3 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 3 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)
round 4 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 4 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 4 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)
round 5 · packages/web-shell/client/components/RootErrorFallback.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/config/daemon.test.ts: P (exit 0)
round 5 · packages/web-shell/client/daemon/workspace/DaemonWorkspaceProvider.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/main.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/utils/bounded-map.test.ts: P (exit 0)
round 5 · scripts/tests/export-html-import-meta-guard.test.js: P (exit 0)

Evidence images

01-ab-guard-branches-base-vs-head

02-ab-reload-gate-cells

03-guard-mutation-matrix-with-build-control

04-reload-gate-mutation-proof-5-of-5

05-f1-candidate-fix-measured

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

Qwen Code · sandboxed verification

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

Critical-only review at head 23b018e8. Not approving. The code-side Critical from the previous round is fixed and I found no new one, but the open triage request-changes on this PR is a direction objection that the newest round re-states as its only unresolved Critical, and that is not something I can settle by reading code.

The standing blocker

Review 5148889985 (triage stage 1b, 2026-09-09T02:04:31Z) is still an open CHANGES_REQUESTED at this head, and the round-6 review posted here 12 minutes ago — which returned zero findings of its own under a Critical posting floor — carries exactly one item under "Unresolved, please confirm": that objection. Its substance is unchanged by the six commits since:

  • There is no observed occurrence behind the duplicate-module-copy diagnostic. The PR's own Evidence section reads N/A, #11100 is referenced but explicitly not closed, and the verification recorded there concluded the user-visible failure is unreachable on main.
  • The retry half targets a root fallback the documented failure class never reaches: message-body throws are caught by MessageItem's own boundary, and the standalone root mounts DaemonWorkspaceProvider, so a missing-provider throw cannot occur there.
  • The diagnostic branch is what most of the production diff buys — the globalThis registry, the random module id and the three-way message were 34 of 47 changed production lines when the objection was written, and the diff has since grown a bounded-map utility to cap the registry — which is what AGENTS.md's "nothing speculative / no error handling for impossible scenarios" pushes back on.

The objection also names its own exit: produce a real occurrence (a console log with the component stack, or an issue link), or split the retry half out and argue it on its own merits, deferring the registry to an issue. Neither has happened, so this stays a maintainer/author decision rather than a code question, and I am not able to approve over it.

R5-1 (round 5's code Critical) — verified fixed at this head

The test no longer drives the real, destructive build.mjs. The predicate is extracted into packages/web-templates/src/export-html/import-meta-guard.mjs (findUnexpectedImportMeta, with the tolerated-bundle pattern and the reasoning for the single deliberate guarded read), build.mjs:219-234 imports it and throws on anything unexpected — deliberately without logLevel/logOverride, since silencing the warning class would empty result.warnings and vacate the check — and scripts/tests/export-html-import-meta-guard.test.js (+160) exercises the pure module.

Critical-only scan of what I read

No blocking defect. The reload path cannot strand a credential: canReload = !daemonToken || hasReloadSurvivableDaemonToken() reads the boot prop rather than getDaemonToken(), whose in-memory cache always reports a token after boot, and hasReloadSurvivableDaemonToken() accepts only a token still in the URL or in per-tab storage — falling back to the pre-existing in-place reset() otherwise, with retryMode driving a button label that matches the action. The URL token grammar is now parsed in one place (readTokenFromLocation) shared by both readers, so the accepted spellings cannot drift. The reload re-adds the live theme/language before navigating, because session switches strip those one-shot params, and the new mount effect that strips them is guarded by a url.search !== before comparison so it does not write history when it stripped nothing.

Not read in this pass, and the reason I would not certify the whole diff even setting the triage objection aside: DaemonWorkspaceProvider.tsx's registry (+73/-1), bounded-map.ts (+28), App.tsx, RootErrorFallback.tsx and useSessionArtifacts.ts. All 26 inline threads are resolved and the remaining round-6 items are deferred probes at Suggestion level.

CI at this head

Every substantive check passed — Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox), both Desktop Shell jobs, the web-shell visual capture, web-shell E2E Smoke and review-pr. The rollup reads FAILURE only because two route checks were cancelled. Round 6's Test Plan lines reporting no such file or directory for three test files are a harness path problem on the reviewer's side, not a defect here.

Next step

Answer the triage objection rather than the code: attach a real occurrence for the duplicate-module-copy guard, or split the reload-on-root-retry change out (defensible on its own, and a small diff in main.tsx) and move the diagnostic registry into a tracked issue. Once that is settled the code side needs nothing further from this round.

@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Answering the standing triage objection directly, as the person who filed the occurrence: it is real, and this PR is the fix for it.

On the morning of Sep 9, in a dev session running the vite dev server for the standalone shell against qwen serve on :4170, the top-level boundary rendered exactly this guard message — useDaemonWorkspace must be used within DaemonWorkspaceProvider — three times. The raw error text was visible on screen, which pins a DEV build (the production fallback hides it), the in-place retry could not clear it, and a full page reload recovered each time. That is the "standalone page where 'Try again' failed and a reload recovered" exit the objection named, and it is what the PR was written from.

What I did not capture was the console component stack, so the duplicate-module-copy mechanism remains an inference from the static shape of the tree — the standalone entry always mounts DaemonWorkspaceProvider and always produces a context value, so within one coherent module graph the guard cannot fire — rather than a proven fact. The registry branch exists so the next occurrence identifies itself from the error message alone instead of requiring that investigation again: if it reports "no provider has rendered", that points at a genuine integration bug (the #11100 class), and if it names two copy ids, the bundling fault is confirmed with the offending chunks printed. Either outcome settles the question the objection and I both want answered.

On the offered alternative of splitting the retry half out: after the review rounds, the two halves are load-bearing on the same incident — the retry performs the recovery that worked, and the diagnostics exist because the incident's mechanism is not yet proven. I'd like to land them together. The occurrence account above is also now in the PR body under "Why it's needed".

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for coming back with the occurrence instead of letting this sit — that's what unblocks it.

Template ✓ — every required heading present, with a full Chinese mirror.

Problem. Observed, not theoretical. The standalone root boundary showed this guard's message three times in one dev session, the in-place retry could not clear it, and a manual reload recovered every time. That is exactly the second shape the original Stage 1b objection asked for ("a standalone page where 'Try again' failed and a reload recovered"), so I'm treating that objection as answered and withdrawing it. One honest limit, which the body states itself: no component stack was captured, so duplicate module copies is the residual inference — not a proven mechanism. That doesn't undermine the retry half, but it does cap how much weight the registry half can carry. See Approach.

Direction. Aligned. "A standalone full-page root boundary should reload rather than re-mount the same broken page state" stands on its own, and it now also stands on a correctness fix: an unconditional reload would strand the shell unauthenticated when the token lives only in module memory. The CHANGELOG has no direct reference to a web-shell error boundary; the nearest signal is the embedding-side recovery fixes (blank sidebar after a window reload), so the area is relevant rather than directly precedented.

Size. 1052 changed lines = 316 production + 736 test. It spans web-shell and web-templates, so Stage 0's core-path check fires — but you're a maintainer on a same-repo branch, which is the documented exemption, and 316 production lines is under both the 500 escalation and the 1000 advisory regardless. Not blocked, not escalated.

Approach. Two things I'd push on. Neither blocks.

The diff is three changes, not two. The body describes the guard diagnostics and the reload-on-retry, but there's a third: a new release-gating check in the export-document build (import-meta-guard.mjs, the build.mjs throw, and a 160-line test that drives real builds). That's 72 production + 160 test lines which can hard-fail a release build step, and it appears in neither the body summary nor the Test Plan's verify command — which also omits client/utils/bounded-map.test.ts. Worth a sentence in the body, so nobody is surprised to find a build gate in a PR about an error boundary.

That third change is the entire cost of one expression. The gate exists because the diagnostic id reads import.meta.url, which esbuild lowers to {} under iife. Your ternary guards it correctly (typeof ({}).url === 'string' is false → fallback), so nothing is broken today; the gate is forward-looking. But if the copy id were a plain random id or a build-time constant, the whole web-templates half disappears. The tradeoff is genuine — the module URL is what points at the offending chunk in a dev session, and your test pins that deliberately. So the question is: while the mechanism is still an inference, is "which chunk" worth a release-path gate plus TOLERATED_TRANSCRIPT_IMPORT_META_READS = 3, a constant coupled to esbuild's warning granularity that the code itself admits must be re-measured by hand? Your call, and a reasonable one either way — I'd just rather you made it explicitly.

The bounded-map extraction is a clear win regardless: three call sites of the same logic collapsed into one, and oldest === undefined fixes a latent bug where !oldest would have stopped eviction forever on an empty-string key.

Risk. No high-risk-path match. Two notes for whoever merges: the unit matrix ran on ubuntu only (the windows/macos Test jobs are skipped by config), and the new export-html test mutates tracked source (document-main.tsx) plus another package's dist/transcript.js, restoring both in a finally — contained in CI, but a SIGKILL mid-run leaves a local tree dirty.

Moving on to code review. 🔍

中文说明

感谢你补上了真实现场,而不是让这个 PR 挂着——这正是解锁它的关键。

模板 ✓ —— 所有必需标题齐全,并带完整中文镜像。

问题。 是已观测的,不是理论性的。独立版根边界在一个 dev 会话里三次显示了这个守卫的报错,原地重试无法清除,每次只能靠手动刷新恢复。这正是最初 Stage 1b 异议所要的第二种形状("独立页面中 'Try again' 无效、刷新才恢复"),因此我认定该异议已被回答并撤回。有一个诚实的限度(正文自己也写了):当时没抓到 component stack,所以"模块双副本"是排除法剩下的推断,而非已证实的机制。这不影响重试那一半,但确实限制了注册表那一半能承载的分量,见「方案」。

方向。 对齐。"独立整页的根边界应当刷新,而不是重新挂载同一副坏掉的页面状态"本身就站得住;而且它现在还多了一层正确性修复:无条件刷新会在 token 仅存于模块内存时让 shell 永久失去认证。CHANGELOG 里没有直接对应 web-shell 错误边界的条目,最接近的信号是嵌入端的恢复类修复(窗口 reload 后侧边栏空白),所以这个领域是相关而非有直接先例。

规模。 1052 行改动 = 316 行生产代码 + 736 行测试。跨 web-shellweb-templates,因此触发 Stage 0 的核心路径检查——但你是维护者、且是同仓分支,属于文档化的豁免情形;而且 316 行生产代码本就低于 500 行升级线和 1000 行大 PR 建议线。不阻断,不升级。

方案。 两点想推一下,都不阻断。

这个 diff 是三处改动,不是两处。 正文写了守卫诊断和重试即刷新,但还有第三处:导出文档构建里新增的发布门禁(import-meta-guard.mjsbuild.mjs 里的 throw、以及一个驱动真实构建的 160 行测试)。那是 72 行生产 + 160 行测试,能让一个发布构建步骤硬失败,却既不在正文摘要里、也不在测试计划的验证命令里——该命令还漏了 client/utils/bounded-map.test.ts。建议在正文补一句,免得别人在一个讲错误边界的 PR 里突然发现一道构建门禁。

而这第三处改动,代价完全来自一个表达式。 这道门禁之所以存在,是因为诊断 id 读了 import.meta.url,而 esbuild 在 iife 下会把它降级为 {}。你的三元判断是正确的(typeof ({}).url === 'string' 为假 → 走兜底),所以今天没有任何东西是坏的;这道门禁是前瞻性的。但如果副本 id 用纯随机 id 或构建期常量,整个 web-templates 那一半就不需要存在。取舍是真实的——模块 URL 正是 dev 会话里指向出问题 chunk 的信息,你的测试也刻意锁定了它。所以问题是:在机制仍是推断的当下,"哪个 chunk"这条信息,是否值得一道发布路径门禁,外加 TOLERATED_TRANSCRIPT_IMPORT_META_READS = 3 这个与 esbuild 警告粒度耦合、且代码自己承认必须手工重新测量的常量?由你判断,两种选择都合理——我只是希望你显式地做这个判断。

无论如何,bounded-map 的提取是明确的收益:三处相同逻辑收敛为一处,而 oldest === undefined 修掉了一个潜在 bug——原来的 !oldest 会在空字符串 key 上永远停止淘汰。

风险。 无高风险路径命中。给合并者的两点提示:单测矩阵只跑了 ubuntu(windows/macos 的 Test job 按配置跳过);新增的 export-html 测试会改写受版本管理的源码(document-main.tsx)以及另一个包的 dist/transcript.js,两者都在 finally 里恢复——在 CI 中是可控的,但本地若中途被 SIGKILL,工作树会残留脏改动。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

I read the diff against the head commit and wrote down my own approach first. Mine was thinner — reload-on-retry gated on token survivability, plus one module-scoped boolean to say whether this copy ever rendered a provider — so I looked hard for where the fuller version earns its size. It mostly does: the cross-copy branch genuinely needs shared state, and there is no way to detect a foreign copy without globalThis. What I'd have skipped is the URL-bearing id, and that one expression is what pulls in the whole web-templates half (Stage 1).

No Critical findings. Three Suggestions, all deferrable.

1. An assertion in the export-html e2e test measures the test, not the build. scripts/tests/export-html-import-meta-guard.test.js checks existsSync(exportDist/export-transcript-document.js) after runBuildWithProbe returns — and its finally has already run restoreDist(), which puts the pre-test snapshot back. So the check passes whenever the artifact existed before the test, whatever the build did to it. The comment above it claims "the failing build must not leave the release-gated artifact wiped", and that invariant is in fact false by the test's own account two blocks up: build.mjs rm -rf's dist/ up front and only recreates it on success. The assertion can only fail for the wrong reason (artifact absent before the test). Either move it inside runBuildWithProbe before the restore, or drop it and let the comment say plainly that the test restores dist/.

2. Foreign-copy precedence masks the own-state diagnosis. In the guard, foreignIds.length > 0 short-circuits, so once any stale foreign entry exists — and hot re-evaluation mints a fresh id per save, up to the cap of 8 — a genuine "consumer is outside its live subtree" reports as "the page holds duplicate copies". That's the same finding /review has carried as DUP6-1 / R5-4 since round 4. It's diagnostic-only, the throw is still correct, and reports duplicate copies even when this copy also rendered a provider pins the precedence deliberately — so this is a wording choice, not a defect. Noting it for the record rather than opening another round.

3. The Test Plan's verify command covers 4 of the 6 suites this diff touches — it omits client/utils/bounded-map.test.ts and scripts/tests/export-html-import-meta-guard.test.js.

What I verified so you don't have to re-check it:

  • hasReloadSurvivableDaemonToken() is genuinely fail-closed. I read getDaemonToken() at the head commit: when the URL is clean it falls back to readStoredDaemonToken(), so the predicate's two sources are exactly the two a fresh load would consult, minus the in-memory cache — which is the whole point. The waitForDaemonTokenMessage() path sets only cachedDaemonToken and never persists, so the chrome-extension iframe case correctly lands on the in-place reset, as the body claims.
  • The longer message never reaches end users. RootErrorFallback renders error.message only under import.meta.env.DEV, statically false in the published lib build — so up to 8 module URLs is a dev-only surface, not a leak into host pages or screenshots.
  • The registry stays diagnostic-only. I checked every read of providerCopyRegistry(): written in recordProviderCopy, read only on the throw path. No context value crosses module copies through it, so the embeddability isolation argument from the earlier thread holds untouched.
  • The message prefix is unchanged (useDaemonWorkspace must be used within DaemonWorkspaceProvider + a parenthetical), so existing substring assertions still bind — confirmed by green CI, not by reading alone.

One thing worth saying out loud: /review's round-5 and round-6 "[Critical]" is not an independent code defect. Both rounds describe it as the open triage CHANGES_REQUESTED objecting that the duplicate-copy branch has no observed occurrence. That objection was mine, the author answered it with the occurrence, and Stage 1 has now withdrawn it — so the Critical dissolves rather than needing a code change. Likewise the rounds' Test Plan: no such file or directory lines are the reviewer resolving client/main.test.tsx from the repo root instead of packages/web-shell; the files exist and CI ran them.

Test evidence

From the PR's own CI on the reviewed commit — nothing was built or executed here (triage no-execute rule). Per-suite counts are quoted from the Test (ubuntu-latest, Node 22.x) job log, so these are real runs rather than "tests pass":

Suite Result
main.test.tsx 8 ✅ (81ms)
components/RootErrorFallback.test.tsx 7 ✅ (63ms)
config/daemon.test.ts 27 ✅ (35ms)
daemon/workspace/DaemonWorkspaceProvider.test.tsx 28 ✅ (82ms)
utils/bounded-map.test.ts 4 ✅ (3ms)
scripts/tests/export-html-import-meta-guard.test.js 7 ✅ (797ms)

The last row matters: 7 tests means the describe.skipIf(!hasPrebuiltTranscript) end-to-end half ran rather than skipped, so both real builds executed and the gate was exercised against actual esbuild output.

Check Conclusion
Qwen Code CI success
Test (ubuntu-latest, Node 22.x) success
Lint & Static (ubuntu-latest, Node 22.x) success
Integration Tests (no-AK, No Sandbox) success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) success
Capture web-shell visuals (ubuntu-latest, Node 22.x) success
Desktop Shell (ubuntu-22.04) success
Desktop Shell (windows-2022) success
Test (macos-latest, Node 22.x) skipped
Test (windows-latest, Node 22.x) skipped
Integration Tests (CLI, No Sandbox) skipped

132 check-runs on the commit, no failures; the skips above are the repo's normal matrix and orchestration shape, not something this PR caused. No pull_request-event workflow run is still in flight.

Not verified: that a real browser reload actually recovers the observed dev-session crash. No lane can settle that, because the trigger isn't reproducible on demand — the diagnostic registry is the instrument that will answer it on the next occurrence, which is a fair reason to ship it.

Sandboxed verification would settle the narrower claim: @qwen-code /verify — that the retry performs a real navigation in a browser (not just a stubbed location.reload in jsdom), and that it genuinely declines to when the token is in-memory only. main.test.tsx pins both against a stub, so the jsdom suite passes with the real navigation semantics untested. The visuals job independently flags the same gap: it rendered base vs head 23b018e, saw no screenshot change, and named RootErrorFallback.tsx as a render-shaping file no scenario reaches — so the new "Reload page" / "重新加载" label has no visual coverage anywhere. Adding a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds the root-boundary state would close that half permanently.

中文说明

代码审查

我先在不看 diff 的情况下写下了自己的方案,再对照阅读。我的方案更薄——重试即刷新(以 token 可存活为前提),加上一个模块级布尔量表示"本副本是否渲染过 provider"——所以我重点看了更完整的这一版是否对得起它的体量。大部分是对的得起的:跨副本分支确实需要共享状态,没有 globalThis 就无法发现外来副本。我会省掉的是带 URL 的 id,而这一个表达式正是把整个 web-templates 那一半拉进来的原因(见 Stage 1)。

无 Critical。三条 Suggestion,均可延后。

1. export-html 端到端测试里有一条断言测的是测试自己,不是构建。 scripts/tests/export-html-import-meta-guard.test.jsrunBuildWithProbe 返回之后才检查 existsSync(exportDist/export-transcript-document.js)——而它的 finally 已经执行了 restoreDist(),把测试前的快照放了回去。所以只要该产物在测试前存在,这条断言就通过,与构建对它做了什么无关。它上方的注释声称"失败的构建不得把受发布门禁的产物清空",而这个不变量按测试自己上面两段的说法其实是不成立的:build.mjs 会先 rm -rf dist/,只在成功时重建。这条断言只可能因为错误的原因失败(测试前产物就不存在)。建议要么把它移进 runBuildWithProbe 的恢复之前,要么删掉,并让注释直说是测试自己恢复了 dist/

2. 外来副本优先会盖掉本副本状态的诊断。 守卫里 foreignIds.length > 0 直接短路,因此一旦存在任何陈旧的外来条目——而热重载每次保存都会生成新 id,最多到上限 8 条——真正属于"消费者在存活子树之外"的情形会被报告成"页面存在双副本"。这与 /review 自第 4 轮起一直带着的 DUP6-1 / R5-4 是同一条。它只影响诊断信息,抛异常本身仍然正确,且 reports duplicate copies even when this copy also rendered a provider 是刻意锁定这个优先级的——所以这是措辞取舍,不是缺陷。记录在案,不再开新一轮。

3. 测试计划的验证命令覆盖了 6 个受影响套件中的 4 个,漏了 client/utils/bounded-map.test.tsscripts/tests/export-html-import-meta-guard.test.js

我已核实、你不必再查的部分:

  • hasReloadSurvivableDaemonToken() 确实是失败即关闭的。我在 head 提交上读了 getDaemonToken():URL 干净时它会回退到 readStoredDaemonToken(),所以该判定式的两个来源恰好就是一次全新加载会查的两个来源,减去内存缓存——而这正是关键。waitForDaemonTokenMessage() 路径只设置 cachedDaemonToken、从不持久化,因此 chrome-extension iframe 场景正确地落到原地重置,与正文所述一致。
  • 变长的报错信息不会到达最终用户。RootErrorFallback 只在 import.meta.env.DEV 下渲染 error.message,而在发布的 lib 构建中它静态为假——所以最多 8 条模块 URL 只是 dev 可见面,不会泄漏进宿主页面或截图。
  • 注册表确实只用于诊断。我检查了 providerCopyRegistry() 的每一处读取:只在 recordProviderCopy 中写入,只在抛异常路径上读取。没有任何 context 值经由它跨模块副本传递,因此此前讨论中的可嵌入隔离结论完全不受影响。
  • 报错前缀未变(useDaemonWorkspace must be used within DaemonWorkspaceProvider 加一个括号补充),所以既有子串断言仍然有效——这一点由 CI 全绿确认,而非仅靠阅读。

有一点值得明说:/review 第 5、6 轮的"[Critical]"并不是独立的代码缺陷。两轮都把它描述为未关闭的 triage CHANGES_REQUESTED,异议内容是双副本分支缺少真实发生现场。那条异议是我的,作者已用现场回答,Stage 1 也已撤回——所以这条 Critical 是消解掉的,不需要改代码。同样,那两轮里的 Test Plan: no such file or directory 是审查者从仓库根目录而非 packages/web-shell 解析 client/main.test.tsx 导致的;文件存在,CI 也确实跑了它们。

测试证据

证据来自本 PR 自己在被审提交上的 CI——这里没有构建或执行任何代码(triage 不执行规则)。逐套件数字引自 Test (ubuntu-latest, Node 22.x) 的 job 日志,所以是真实运行结果,不是一句"测试通过"。上表 7 个测试意味着 describe.skipIf(!hasPrebuiltTranscript) 的端到端那一半确实运行而非跳过,两次真实构建都执行了,门禁是在真实 esbuild 产物上被验证的。

该提交上共 132 条 check-run,无失败;表中的 skipped 是仓库正常的矩阵与编排形态,不是本 PR 造成的。没有 pull_request 事件的 workflow run 仍在运行。

未验证:真实浏览器里的刷新是否确实能从那次 dev 会话的崩溃中恢复。没有任何验证通道能确定这一点,因为触发条件无法按需复现——诊断注册表本身就是下次发生时用来回答它的仪器,这也是它值得随 PR 一起合入的合理理由。

沙箱验证可以确定更窄的那个主张:@qwen-code /verify —— 即重试在真实浏览器中确实发生了一次导航(而不只是 jsdom 里被 stub 的 location.reload),以及在 token 仅存于内存时确实拒绝导航。main.test.tsx 两条都是对着 stub 断言的,所以 jsdom 套件通过时,真实导航语义并未被测试。visuals job 独立地指出了同一个缺口:它对 base 与 head 23b018e 渲染对比后没有任何截图变化,并点名 RootErrorFallback.tsx 是一个没有场景能到达的渲染相关文件——所以新的「Reload page」/「重新加载」文案目前没有任何视觉覆盖。在 packages/web-shell/client/e2e/visuals/screenshots.spec.ts 里加一个播种根边界状态的场景,就能永久补上这一半。

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid, safe where it matters, and the one thing I'd cut is named rather than blocking.

My independent proposal was thinner than this diff: reload-on-retry gated on token survivability, plus a single module-scoped boolean for "did this copy ever provide". Comparing the two, the extra machinery mostly earns its place — you cannot detect a foreign module copy without state shared across copies, so globalThis is not over-engineering, it's the only mechanism available. The part that doesn't earn its place is the URL-bearing copy id, because that one expression is what drags in a release-gating build check, a hand-maintained warning-count constant, and a 160-line test that rewrites tracked source to exercise it. 232 lines to make a dev-only diagnostic message name a chunk. Defensible while you're actively hunting this; expensive if the mechanism turns out to be something else and the registry has to be re-cut. That's the reservation, and it's a scope reservation, not a correctness one.

The half I have no doubt about is the retry. A dead-end error screen whose only recovery is a manual reload is a real paper cut, and the fix is now better than the one-line version I'd have written: the review rounds caught that an unconditional reload strands the shell unauthenticated when the token lives only in module memory, and the gate that came out of it is fail-closed in every path I could trace — including the postMessage case, which persists nothing and therefore correctly takes the in-place reset. Turning "reload is obviously right" into "reload is right except where it would lose your credential" is the substantive improvement in this PR, and it landed because the review pushed, not despite it.

On the objection I raised in round 1: I'm withdrawing it, and I want to be explicit about why, because the reviewDecision is about to flip. I asked for one of two things — a real occurrence, or a split. You supplied the occurrence, in the exact shape I said would change my mind, and you were straight that the component stack is missing and the duplicate-copy mechanism is therefore inferred rather than proven. A gate that keeps its objection after being handed what it asked for is not being rigorous, it's just being stubborn. The residual uncertainty is real but it is not a reason to hold the PR: the registry exists precisely to convert that uncertainty into an answer next time, and the retry half stands without it.

Six rounds in, the diff is 6.4× what it started as, and AGENTS.md says land only Critical fixes past round five and defer the rest. There is no standing Critical that is a code defect — rounds 5 and 6 both label their Critical as my own unresolved Stage 1b objection, which is now resolved. So the three Suggestions in Stage 2 are deferred, recorded there rather than dropped: the vacuous artifact assertion in the export-html e2e test, the foreign-copy precedence masking the own-state diagnosis (carried as DUP6-1 since round 4, deliberately pinned by a test), and the Test Plan command missing two of the six suites touched. None of them would change my mind about merging; the first is worth a follow-up because an assertion that cannot fail for the reason it states is worse than no assertion.

Would I thank or curse whoever wrote this in six months? Thank them for the token-survivability gate and the bounded-map extraction — that one quietly fixes a latent !oldest bug where an empty-string key would have stopped eviction forever. Mild curse at TOLERATED_TRANSCRIPT_IMPORT_META_READS = 3 the first time esbuild changes warning granularity, though the comment does warn me it's coming.

CI is green on the reviewed commit with nothing in flight, so this approval is pinned to 23b018e8 rather than deferred. It supersedes my earlier request-changes; main still wants a second human approval, and given the scope question above I'd rather a maintainer made that call than have it ride on mine alone.

中文说明

信心:4/5 —— 扎实、在关键处是安全的,唯一我想砍掉的部分已点名,但不构成阻断。

我自己的方案比这个 diff 更薄:重试即刷新(以 token 可存活为前提),再加一个模块级布尔量表示"本副本是否 provide 过"。两者对照,多出来的机制大部分对得起它的体积——不借助跨副本共享的状态就无法发现外来模块副本,所以 globalThis 不是过度设计,而是唯一可用的机制。真正对不起体积的是带 URL 的副本 id:就这一个表达式,把一道发布门禁构建检查、一个需手工维护的警告计数常量、以及一个为验证它而改写受管源码的 160 行测试全都拉了进来。为了让一条只在 dev 可见的诊断信息说出 chunk 名字,代价是 232 行。在你正主动追查这个问题时它是说得通的;但如果机制最终被证明是别的原因、注册表需要重做,它就变贵了。这是我的保留意见,属于范围层面,不是正确性层面。

我毫不怀疑的是重试那一半。一个只能靠手动刷新才能恢复的死胡同报错界面是真实的体验刺点,而这个修复比我原本会写的一行版本更好:评审轮次发现无条件刷新会在 token 仅存于模块内存时让 shell 永久失去认证,由此得到的门禁在我能追踪到的每条路径上都是失败即关闭的——包括 postMessage 那条:它不做任何持久化,因此正确地走了原地重置。把"刷新显然对"变成"刷新是对的,除非它会丢掉你的凭证",是这个 PR 里实质性的改进;而且它是评审推动出来的结果,不是绕开评审得到的。

关于我在第 1 轮提出的异议:我撤回它,并且想明确说明原因,因为 reviewDecision 即将翻转。我当时要求两件事之一——一个真实发生现场,或者拆分。你提供了现场,且正好是我说过会改变我判断的那种形状;同时你也坦白说明 component stack 缺失,因此双副本机制是推断而非已证实。一个在拿到自己所要的东西之后仍保留异议的门禁,不是严谨,只是固执。残留的不确定性是真实的,但它不是扣住这个 PR 的理由:注册表存在的意义正是在下次发生时把这份不确定性变成答案,而重试那一半不依赖它也能成立。

到第 6 轮,diff 已是初始的 6.4 倍,而 AGENTS.md 说超过 5 轮只合入 Critical 修复、其余延后。目前没有仍然成立的、属于代码缺陷的 Critical——第 5、6 轮都把它们的 Critical 标注为我自己那条未解决的 Stage 1b 异议,而它现已解决。所以 Stage 2 里的三条 Suggestion 予以延后,记录在案而非丢弃:export-html 端到端测试里那条空转的产物断言、外来副本优先盖掉本副本状态诊断(自第 4 轮起以 DUP6-1 携带,且被测试刻意锁定)、以及测试计划命令漏掉 6 个受影响套件中的 2 个。这三条都不会改变我对合并的判断;第一条值得开一个后续,因为一条无法因其声称的原因而失败的断言,比没有断言更糟。

六个月后我会感谢还是抱怨写这段代码的人?会感谢 token 可存活门禁和 bounded-map 提取——后者悄悄修掉了一个潜在的 !oldest bug:空字符串 key 会让淘汰永远停止。会在 esbuild 第一次改变警告粒度时,对 TOLERATED_TRANSCRIPT_IMPORT_META_READS = 3 轻微抱怨一句,不过注释确实提前提醒了我它会来。

被审提交上的 CI 全绿且无进行中的任务,因此本次批准锚定在 23b018e8,而非延后。它取代我此前的 request-changes;main 仍需要第二个真人批准,而鉴于上面的范围问题,我更希望由维护者来做那个判断,而不是只压在我这一票上。

Qwen Code · qwen3.8-max-2026-09-02

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅ The retry/token-survivability half is fail-closed on every path I could trace, and CI is green on this commit. My earlier request-changes (no observed occurrence) is withdrawn — see the Stage 3 note. Three Suggestions deferred per the round-5+ posture, recorded in Stage 2.

@wenshao
wenshao added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit f6540d1 Sep 10, 2026
187 of 189 checks passed

@doudouOUC doudouOUC 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 at 23b018e8 (head). C=0 — no new HEAD-verified Critical. All 26 review threads on this PR are resolved, and round 6 posted 0 fresh findings, so this round is a verification pass rather than a new sweep. Nothing already reported is repeated below.

What I verified, and how

setBoundedMapEntry extraction is behaviour-preserving. Every one of the five App.tsx call sites (:4348, :4387, :4404, :4417, :5824) and the useSessionArtifacts.ts one passes its original cap — MAX_ARTIFACT_PANEL_SESSION_STATES and MAX_CACHED_SESSIONS, both still 20. No call site silently lost its budget. The loop-exit change from if (!oldest) to if (oldest === undefined) can only differ for an empty-string key, and every key in the family is built as <cwd>\0<sessionId> or import.meta.url#<rand>, so it is a strict correctness improvement with no reachable behaviour delta.

readTokenFromLocation() is a faithful extraction. The old code produced '' for "no token" and gated on if (fromUrl); the new one produces undefined and gates the same way. #token= with an empty value still falls through to ?token= and then to undefined, exactly as before, so hasReloadSurvivableDaemonToken()'s !== undefined test is equivalent to a truthiness test and cannot report a token that isn't there.

canReload cannot strand the shell unauthenticated. I traced all four boot paths against persistDaemonToken's call sites: URL-borne token (persisted → reload survivable), waitForDaemonTokenMessage / extension iframe (memory only → canReload false → in-place reset, matching the PR's stated scope note), tokenless trusted loopback (!daemonToken → reload, nothing to strand), and sessionStorage-throwing private mode (memory only → reset). The reload URL is built from window.location.href, so a surviving #token= fragment is carried through searchParams.set untouched.

retryMode is a live switch, not a dead one. Written at main.tsx:244, read at RootErrorFallback.tsx:110, and deliberately left unset by the two embeddable call sites (index.tsx:69, WebShellTranscript.tsx:315) — with RootErrorFallback.test.tsx:127 pinning that default in a comment. hasReloadSurvivableDaemonToken likewise has a real read site.

The guard message prefix really is safe. must be used within DaemonWorkspaceProvider appears in exactly two places repo-wide: the throw itself and a substring toThrow at DaemonWorkspaceProvider.test.tsx:494. No exact-match or anchored-regex assertion exists anywhere that the added (${detail}) suffix could break.

replaceState(null, ...) discards nothing. No consumer in the repository reads history.state, so both the new mount-time strip and the reload path are safe on that axis.

Test placement is right, not a style miss. import-meta-guard.mjs lives in packages/web-templates, which has no test script, no vitest config, and no test files at all. scripts/tests/ is the only location wired into npm run test:ci, so that is where the test has to live for it to run.

One finding I raised against myself and then withdrew

I want to record this so it does not get re-litigated in a round 7. A reviewer looking at scripts/tests/export-html-import-meta-guard.test.js will notice that the module-scope mkdtempSync snapshot root is deleted by the file-level afterEach after every test — including the five pure unit tests — and conclude that the second e2e case's snapshotDist() must fail ENOENT, leaving the release-gated dist/ wiped. That is wrong, on two independent counts. cpSync(src, dest, { recursive: true }) creates missing parent directories, so the copy into the deleted root succeeds and the snapshot is rebuilt per case; I confirmed this with a direct probe against a removed mkdtempSync root. And the CI Test lane on this exact head settles it empirically:

✓ scripts/tests/export-html-import-meta-guard.test.js (7 tests) 797ms
  ✓ export-html import.meta guard (end-to-end) > fails the build when an import.meta read lands in the document entry  381ms
  ✓ export-html import.meta guard (end-to-end) > fails the build on a fourth import.meta read inside the prebuilt transcript bundle  406ms

Both e2e cases ran — not skipped — and passed. The same measurement retires the spawnSync timeout: 120_000 vs. the suite's testTimeout: 90_000 mismatch as a live flake risk: the observed runtimes are 381 ms and 406 ms, three orders of magnitude inside the ceiling. The unsnapshotted second product is a non-issue too — packages/web-templates/src/generated/ is gitignored, so the e2e half cannot dirty a worktree.

Ruling on the open blocking review

Per the project's own rule that an approval is a claim rather than a default, the one unresolved Critical on this PR is the round-1 triage CHANGES_REQUESTED (review 5148889985), which objects that the duplicate-module-copy diagnostic has no observed occurrence behind it. My ruling is partially answered, remainder is a maintainer product call — not a code defect, and not something further review rounds can settle:

  • Its "no reproduction" limb is now answered in kind: the PR body carries a narrated occurrence (root boundary showing this guard three times in one dev morning, in-place retry ineffective, manual reload recovering each time). It is an author narrative rather than the console component stack the triage asked for, and the PR says so plainly — the mechanism is labelled inferred, not proven.
  • Its structural limb ("a missing-provider throw cannot happen in the standalone app at all") is consistent with, not contradicted by, that occurrence: both sides agree the throw is unreachable while a single module graph is coherent, which is precisely why the duplicate-copy hypothesis exists and why the diagnostic branch is written to confirm or refute it on the next occurrence.
  • Its alternative remedy — split the retry half out and let the registry wait for a real occurrence — was not taken; the diff instead grew to include the export-build import.meta guard, which the PR description's stated scope does not mention.

Whether roughly 250 production lines of diagnostics for an inferred mechanism clears "nothing speculative" is a judgment about product intent, and the triage itself routed it to a maintainer rather than looping. I am approving on the engineering axis only: this approval does not dismiss that objection, and should not be read as resolving it. A maintainer still owns the shape call.

Convergence note

This is round 6+, with 11 items already recorded as deferred. Per the project's convergence rule, only Critical fixes should land from here — and I have none to add. The deferred set is worth a follow-up issue so it is not silently dropped, in particular the two that are genuinely about diagnostic quality rather than test strength: the stale-id precedence that lets foreignIds report "duplicate copies" for a hot-re-evaluation ghost (already reported as D4-1/DUP6-1), and the new import.meta throw preempting the purpose-written FORBIDDEN_DOCUMENT_INPUTS message on exactly the regression that guard was written for. Neither blocks.

中文说明

23b018e8(head)上完成审查。C=0 — 未发现新的、经 HEAD 验证的 Critical。 PR 上 26 条 review thread 全部已解决,第 6 轮也已经是 0 条新发现,因此本轮是验证性复核,不重复任何已报告项。

已验证的内容: setBoundedMapEntry 抽取是行为保持的(App.tsx 五处调用与 useSessionArtifacts.ts 均沿用原有上限 20,!oldestoldest === undefined 仅在空字符串 key 下有差异,而所有调用点的 key 都不可能为空);readTokenFromLocation() 的抽取与旧逻辑等价(空值 token 的短路行为一致);canReload 在四条 boot 路径下都不会让 shell 在刷新后失去认证,且刷新 URL 基于 location.href 构造,会原样保留可能存活的 #token= 片段;retryModehasReloadSurvivableDaemonToken 都有真实读取点,不是无人设置的死开关;守卫消息前缀在全仓库只有一处子串断言,新增的 (${detail}) 后缀不会破坏任何精确匹配;仓库内无任何 history.state 读取方,两处 replaceState(null, ...) 是安全的;packages/web-templates 没有任何测试设施,把测试放在 scripts/tests/ 是唯一能被 npm run test:ci 执行的位置,不算风格问题。

一条我自己提出又撤回的发现(记录在此以免第 7 轮重复): 有人会认为 export-html-import-meta-guard.test.js 中文件级 afterEach 在每个测试后删除 mkdtempSync 快照根目录,会导致第二个 e2e 用例 snapshotDist()ENOENT 并留下被清空的发布产物。这是错的,有两重独立证据:cpSync(src, dest, { recursive: true }) 会创建缺失的父目录(我对已删除的 mkdtempSync 根目录直接做了探针验证);且本 head 的 CI Test lane 显示该文件 7 个测试全绿,两个 e2e 用例确实执行(未被 skip)、分别耗时 381ms 与 406ms。同一组测量也消除了 spawnSync 120s 超时与套件 90s testTimeout 不一致的实际 flake 风险。此外 packages/web-templates/src/generated/ 已被 gitignore,e2e 部分不会污染工作树。

对未决阻断性审查的裁定: 唯一未决的 Critical 是第 1 轮 triage 的 CHANGES_REQUESTED(review 5148889985),质疑重复模块副本诊断分支背后没有实际观测到的现场。我的裁定是部分已回应,剩余部分属于 maintainer 的产品判断——不是代码缺陷,也不是继续增加审查轮次能解决的问题:「无复现」这一条已由 PR 描述中的现场叙述回应(一个上午三次、原地重试无效、手动刷新恢复),但那是作者叙述而非 triage 要求的 component stack,PR 自己也明确标注机制是推断而非证实;「独立应用树里不可能触发」这一条与该现场并不矛盾,双方都认为模块图一致时不可能触发,这正是重复副本假设存在的理由;而 triage 给出的替代方案(拆出刷新那一半、诊断注册表等真实现场)未被采纳,diff 反而扩大到包含导出构建的 import.meta 守卫,且 PR 描述的范围并未提及这一半。约 250 行生产代码用于诊断一个推断中的机制是否符合「不做投机代码」,属于产品意图判断,triage 本身也已将其转交 maintainer。我仅在工程层面批准:本次批准不构成对该异议的撤销,也不应被理解为已解决它,改动形态的最终判断仍归 maintainer。

收敛提示: 已是第 6 轮以上、11 条延后项在册。按项目收敛规则,从这里开始只应落地 Critical 修复,而我没有新的 Critical。建议把延后项开一个 follow-up issue 以免被静默丢弃,尤其是两条真正关于诊断质量而非测试强度的:foreignIds 中的过期 id 会让热重载残留被误报为「重复模块副本」(已作为 D4-1/DUP6-1 报告),以及新的 import.meta 抛错会在该守卫本要防的那个回归上抢先于专门写的 FORBIDDEN_DOCUMENT_INPUTS 诊断。两者都不阻断。

yiliang114 added a commit that referenced this pull request Sep 10, 2026
…-css

One both-side change: packages/web-templates/src/export-html/build.mjs. main's
#11421 added the import.meta guard (a new import plus the post-build
findUnexpectedImportMeta throw) while this branch added the transcript CSS
entry filter import at the same spot. Both imports are kept and the guard block
is untouched.

The budget constants stay at this branch's JS-only values (1,870,000 warning /
1,930,000 max against a measured 1,833,894 bytes of renderer JS), and the
comment now also records main's pre-split combined measurement of 4,133,282
bytes that #11372 raised them for, so that history is not lost by the split.

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

Copy link
Copy Markdown
Collaborator

Released in v0.23.3.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants