Skip to content

feat(web-shell): add async submit preparation - #9802

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
dreamWB:codex/webshell-prepare-submit-20260820
Aug 24, 2026
Merged

feat(web-shell): add async submit preparation#9802
wenshao merged 4 commits into
QwenLM:mainfrom
dreamWB:codex/webshell-prepare-submit-20260820

Conversation

@dreamWB

@dreamWB dreamWB commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds an optional asynchronous prepareSubmit customization for embedded Web Shell hosts. The callback can replace the outgoing prompt and input annotations after Web Shell has handled local commands, but before submission gates, session allocation, queue admission, and transport. Direct submissions and queued submissions use the prepared snapshot, while retries reuse the already prepared payload instead of resolving mutable host context again.

Why it's needed

Embedded hosts sometimes need to resolve context only when a user actually submits, such as the editor or node active at that moment. Doing this in the host before rendering either makes the context stale or requires UI event listeners, while the existing pre-submit gate is intentionally a wait-only contract. A dedicated preparation stage lets hosts attach late-bound context without changing local slash/shell command behavior or weakening Web Shell's session ownership and queue guards.

Reviewer Test Plan

How to verify

  1. Render WebShell with a prepareSubmit callback that appends text and replaces inputAnnotations, then submit a normal prompt. Confirm preparation runs before onSubmitBefore, and the prepared payload reaches both the daemon transport and the prompt-submitted event.
  2. Handle /help, /theme dark, and !echo hello locally. Confirm prepareSubmit is not called.
  3. Queue a prompt while another turn is active, mutate the host-side value after queue admission, and confirm the queued turn sends the snapshot captured during preparation.
  4. Retry a failed prepared prompt and confirm the prepared payload is reused without invoking prepareSubmit again.
  5. Return an empty prepared prompt with no attachments and confirm submission is cancelled while the draft remains available.

Local verification:

  • npm run test --workspace=packages/web-shell -- App.test.tsx — 525 passed.
  • npm run typecheck --workspace=packages/web-shell — passed.
  • npm run lint --workspace=packages/web-shell — passed.
  • npm run build --workspace=packages/web-shell — passed.
  • npm run pre-commit — passed with the staged patch unchanged.
  • Root npm run preflight completed formatting, lint, builds, typechecks, and the full Web Shell suite, but returned 1 because unrelated CLI baseline tests failed under the aggregate run. The two server files passed when rerun serially (1077 and 15 tests); AuthDialog.test.tsx remains a baseline-only keyboard-navigation failure, and this branch has no CLI diff.

Evidence (Before & After)

N/A — this is a host integration API with no standalone Web Shell visual change.

Tested on

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

Environment (optional)

Node.js 22-compatible monorepo environment; embedded host behavior was also exercised against a locally built Web Shell artifact.

Risk & Scope

  • Main risk or tradeoff: asynchronous host preparation can delay submission; ownership and source-version checks are repeated after the callback so a stale result cannot cross into another session or draft.
  • Not validated / out of scope: Windows and Linux local runs; defining host-specific context formats or timeout policy.
  • Breaking changes / migration notes: none; prepareSubmit is optional and omitted hosts preserve existing behavior.

Linked Issues

N/A

中文说明

本 PR 做了什么

为嵌入 Web Shell 的宿主新增可选的异步 prepareSubmit 定制能力。该回调会在 Web Shell 完成本地命令处理后、提交门禁、会话分配、队列准入和网络发送前执行,并可替换最终发送的 prompt 与 input annotations。直接发送和排队发送都会使用准备后的快照;重试则复用已经准备好的 payload,不会再次解析可能已变化的宿主上下文。

为什么需要

嵌入式宿主有时需要只在用户真正发送时解析上下文,例如那一刻处于激活状态的编辑器或节点。如果宿主在渲染阶段提前处理,上下文可能过期,或者需要额外监听 UI 事件;现有的发送前门禁则明确只是等待型契约。独立的准备阶段允许宿主附加发送时才确定的上下文,同时不改变本地 slash/shell 命令行为,也不削弱 Web Shell 的会话归属和排队保护。

Reviewer Test Plan

如何验证

  1. WebShell 传入一个会追加文本并替换 inputAnnotationsprepareSubmit 回调,然后发送普通 prompt。确认准备阶段先于 onSubmitBefore,并且准备后的 payload 同时进入 daemon transport 和 prompt-submitted 事件。
  2. 在本地处理 /help/theme dark!echo hello,确认不会调用 prepareSubmit
  3. 在已有 turn 运行时排队一条 prompt,准入后修改宿主侧值,确认排队 turn 发送的是准备阶段捕获的快照。
  4. 重试一条失败的已准备 prompt,确认复用准备后的 payload,且不会再次调用 prepareSubmit
  5. 返回空的准备后 prompt 且没有附件,确认发送被取消,同时草稿仍然保留。

本地验证:

  • npm run test --workspace=packages/web-shell -- App.test.tsx — 525 个用例通过。
  • npm run typecheck --workspace=packages/web-shell — 通过。
  • npm run lint --workspace=packages/web-shell — 通过。
  • npm run build --workspace=packages/web-shell — 通过。
  • npm run pre-commit — 通过,暂存 patch 未发生变化。
  • 根级 npm run preflight 已完成格式化、lint、构建、类型检查和 Web Shell 全量测试,但因为聚合运行中的无关 CLI 基线测试失败而返回 1。两个服务端测试文件串行复跑均通过(1077 和 15 个用例);AuthDialog.test.tsx 仍是基线已有的键盘导航失败,本分支没有任何 CLI diff。

证据(修改前后)

N/A — 这是宿主集成 API,独立 Web Shell 没有可视变化。

已测试平台

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

环境(可选)

兼容 Node.js 22 的 monorepo 环境;同时使用本地构建的 Web Shell 产物验证了嵌入式宿主行为。

风险与范围

  • 主要风险或权衡:异步宿主准备可能延迟发送;回调完成后会重新检查会话归属和来源版本,避免过期结果进入其他会话或草稿。
  • 未验证 / 不在范围内:Windows 和 Linux 本地运行;宿主特定上下文格式和超时策略。
  • 破坏性变更 / 迁移说明:无;prepareSubmit 是可选能力,未传入的宿主保持现有行为。

关联 Issue

N/A

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR — re-run after three follow-up commits (head is now 2f17b33).

  • Template: complete ✓ — all sections present, bilingual body included.
  • Problem: a feature, not a bug fix, so there is no reproduction to ask for — and the gap is verifiable in code: onSubmitBefore is intentionally a wait-only contract (Promise<void>), so a host can hold or veto a submission but cannot change the prompt or annotations on its way out. For an embeddable shell whose hosts resolve late-bound context (the editor or node active at the moment of submission), a transformation stage before the gates is a real gap. Still no linked issue or named downstream consumer — justified by the package's role (@qwen-code/web-shell is published for external hosts; no in-repo consumer embeds it).
  • Direction: aligned. The package exists to be embedded and customized — its customization surface already carries ~30 host hooks — and a submit-time preparation stage fits that surface. It stays inside the client submit pipeline; the daemon protocol is untouched. No CHANGELOG reference, but the area is actively developed in this repo.
  • Size: not a core module — packages/web-shell/client/** only, single package: 341 production lines (App.tsx +233/−95, customization.tsx +11, index.tsx +2) plus 767 test lines (App.test.tsx +734/−33). Well under any escalation threshold.
  • Approach: scope unchanged and still right — prepare once per logical submission, snapshot the payload, replay the snapshot at queue admission, skip preparation on retries. The three follow-up commits refine retry/disarm semantics in response to the round-1 code review (the failed-prompt retry no longer carries daemon retry semantics, and slash-prepared submits disarm stale retry state) without changing the API's shape. No drive-by changes spotted.
  • Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

感谢贡献——这是在三个后续提交之后的重新运行(当前 head 为 2f17b33)。

  • 模板:完整 ✓ —— 各部分齐全,含双语正文。
  • 问题:这是功能而非 bug 修复,所以没有复现可要求——而且缺口可以在代码里验证:onSubmitBefore 刻意只是等待型契约(Promise<void>),宿主只能暂缓或否决提交,无法在发送途中修改 prompt 或 annotations。对宿主需要解析延迟绑定上下文(提交那一刻激活的编辑器或节点)的嵌入式终端来说,门禁前的转换阶段是真实缺口。仍没有关联 issue,也没有点名的下游消费方——其合理性来自包的定位(@qwen-code/web-shell 发布给外部宿主使用;仓库内没有消费方直接嵌入它)。
  • 方向:对齐。这个包就是为嵌入和定制而生的——定制面已有约 30 个宿主钩子——提交时准备阶段与这个面契合。改动只停留在客户端提交管线内,不触碰 daemon 协议。CHANGELOG 无直接引用,但该领域在本仓库持续开发中。
  • 规模:不触及核心模块——仅 packages/web-shell/client/**,单包:341 行生产代码(App.tsx +233/−95、customization.tsx +11、index.tsx +2)加 767 行测试(App.test.tsx +734/−33)。远低于任何升级阈值。
  • 方案:范围未变且依然合理——每次逻辑提交准备一次、快照 payload、排队准入时重放快照、重试跳过准备。三个后续提交是根据第一轮代码审查细化重试/解除语义(失败 prompt 重试不再携带 daemon 重试语义,slash 准备后的提交会解除过期的重试状态),API 形态未变。未发现顺手改动。
  • 风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review (re-run on 2f17b33)

The three follow-up commits landed since the first pass; this review covers the current head end-to-end, with focus on what changed. No Critical or blocking findings.

What I verified by reading the diff against the current submit pipeline:

  • The round-1 Critical is fixed, and minimally. The failed-prompt retry now passes skipPrepareSubmit: true instead of retry: true. I checked the base: that call site previously carried only optimisticUserMessage: false + inputAnnotations, so daemon-visible behavior is now byte-identical to main — the new option only skips the new callback. The transcript-loss scenario from round 1 (daemon retry semantics skipping a user-message record that was never created) is closed, and the test does not resend a failed-prompt retry with daemon retry semantics pins it with a retry: undefined assertion. The Ctrl+Y turn-error path keeps its pre-existing retry: true untouched.
  • Slash-prepared disarm (new since first pass). A submit whose prepared prompt starts with / is never retried, so the new branch clears all eight retry-related refs (last prompt/images/files/annotations plus the four turn-error refs). I checked the guards: it does not fire for retries (!opts?.retry) or the failed-prompt retry (optimisticUserMessage !== false), so a retry cannot disarm itself. Covered by three tests — base case, attachment variant, and the stashed turn-error variant.
  • Staleness. The direct path re-checks admissionSourceIsCurrent() three times — after the host prepare, after the whole preflight, and after session allocation; the queued path re-checks submissionOwnerIsCurrent() after each host await before enqueue. The abort-after-prepare path — the gap flagged in my previous review — now has tests on both the direct and queued paths, asserting nothing downstream runs and the draft/preparing state are restored.
  • Contract tests pin the five claims. Ordering (prepare → gate → transport), queue snapshot frozen before admission, retry reusing the first prepared payload without re-preparing, empty-prepared cancel with draft retention, and local slash/shell commands bypassing preparation — each has a test whose assertions cannot pass on the base branch (the prop does not exist there). resolvePreparedSubmit's fallback to the original payload on a void/malformed host return is a sensible boundary default.
  • Remaining items are the S-level probes already tracked in the /review thread (e.g. internal daemon syncs like /language ui pass through the host's prepareSubmit and can be observed/rewritten by it; queued-path gate assertion on the prepared prompt). Those are recorded follow-ups, not merge blockers — the language-sync send otherwise behaves exactly as on main.

Submit pipeline with prepareSubmit

sequenceDiagram
    participant P1 as Host prepareSubmit
    participant P2 as Web Shell submit path
    participant P3 as onSubmitBefore gate
    participant P4 as Session allocation and queue
    participant P5 as Daemon transport
    P2->>P1: prepare prompt and annotations
    P1-->>P2: prepared snapshot
    P2->>P2: re-check session owner and source version
    P2->>P3: gate with prepared prompt
    P3-->>P2: approved
    P2->>P4: send directly or enqueue snapshot
    P4->>P5: send prepared prompt once
Loading

Queued prompts keep the snapshot until admission; both retry entry points skip preparation — the failed-prompt banner via skipPrepareSubmit, Ctrl+Y via retry — and resend the stored payload.

Testing — what this comment carries

This is an unattended CI run, so the PR's code was not built or executed here (no tmux real-scenario section on this path); the evidence is the PR's own CI on the reviewed commit, fetched via API. The unit-test additions remain the load-bearing signal: they cannot pass without the diff, so the green Test (ubuntu-latest) run below substantiates the five contract claims. A maintainer additionally ran a local verification pass on this exact commit and reported it green in the thread — that is the maintainer's own result, cited as context, not as this review's evidence. One layer stays out of reach of every in-repo lane by construction: behavior inside a real embedding host — no downstream host exists in this repo. A sponsored sandboxed verification job is already in flight as part of this triage re-run and will post its report separately.

Final CI results for 2f17b33:

Check Conclusion
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
route ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

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

All substantive checks are green on the reviewed commit; the macOS/Windows test legs report skipped as their check conclusion (the fork-PR CI pattern, not failures).

中文说明

代码审查(在 2f17b33 上重新运行)

自上一轮之后又落了三个后续提交;本轮对当前 head 做了端到端审查,重点看变化部分。无 Critical 或阻塞项。

对照现有提交管线逐条核对:

  • 第一轮 Critical 已修复,且修法最小。 失败 prompt 重试现在传 skipPrepareSubmit: true 而非 retry: true。核对过 base:该调用点此前只有 optimisticUserMessage: false + inputAnnotations,因此对 daemon 可见的行为与 main 完全一致——新选项只用于跳过新回调。第一轮提出的丢失 transcript 场景(daemon 重试语义跳过从未创建的用户消息记录)已关闭,测试 does not resend a failed-prompt retry with daemon retry semanticsretry: undefined 断言钉住了这一点。Ctrl+Y turn-error 路径保持原有的 retry: true 不变。
  • slash 准备后的解除(本轮新增)。 准备后以 / 开头的提交永不重试,因此新分支清空全部 8 个重试相关 ref(最近 prompt/图片/文件/annotations 以及 4 个 turn-error ref)。核对过守卫:对重试(!opts?.retry)和失败重试(optimisticUserMessage !== false)均不触发,重试不会解除自身。共 3 个测试覆盖——基础场景、附件变体、暂存 turn-error 变体。
  • 过期防护。 直接路径三次复查 admissionSourceIsCurrent()——宿主准备之后、整个 preflight 之后、会话分配之后;排队路径在每次宿主 await 之后、入队之前复查 submissionOwnerIsCurrent()。prepare 之后的中断路径——上一轮我标记的缺口——现在直接和排队路径都有测试,断言下游均不执行且草稿/准备状态被恢复。
  • 契约测试钉住五项声明。 顺序(prepare → 门禁 → 传输)、入队前冻结的排队快照、重试复用首次准备后的 payload 且不再重新准备、空准备结果取消且保留草稿、本地 slash/shell 命令绕过准备——每项都有在 base 分支上不可能通过的测试(base 上该 prop 不存在)。resolvePreparedSubmit 在宿主返回 void/畸形值时回退到原 payload,是合理的边界默认。
  • 剩余事项即 /review 线程中已跟踪的 S 级探针(例如 /language ui 这类内部 daemon 同步会经过宿主的 prepareSubmit,宿主可以观察甚至改写它;排队路径门禁对准备后 prompt 的断言)。这些是已记录的后续项,不是合入阻塞——language 同步发送的其他行为与 main 完全一致。

测试证据说明

本次为无人值守 CI 运行,未构建或执行 PR 代码(此路径没有 tmux 真实场景部分);证据来自 PR 自身 CI 在受审提交上的结果(经 API 获取)。新增单测仍是承重证据:没有此 diff 不可能通过,因此下面变绿的 Test (ubuntu-latest) 运行证实了五项契约声明。一位维护者还在同一提交上做了本地验证并在线程中报告全绿——那是维护者自己的结果,此处仅作背景引用,不作为本审查的证据。有一层按构造就超出仓库内所有通道:真实嵌入宿主中的行为——仓库内不存在下游宿主。一个受赞助的沙箱验证任务已作为本次 triage 重跑的一部分在运行,报告会另行发布。

CI 表格(英文部分):受审提交上所有实质检查均为绿色;macOS/Windows 测试项的检查结论为 skipped(fork PR 的 CI 模式,非失败)。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the follow-up commits resolved the only Critical cleanly and pinned every fix with real oracles; the remaining reservations are recorded non-blocking follow-ups, not doubts about the code.

Reflecting on the whole picture: this re-run is mostly about whether the autofix loop's three commits fixed what round 1 found without growing the PR past its goal — and they did. The skipPrepareSubmit split is exactly the minimal repair: I checked the base, and the failed-prompt retry's daemon-visible behavior is now identical to main, with the new flag doing only one thing. The slash-prepared disarm closes a genuinely reachable stale-retry path (a turn error on an internal /language sync resurrecting an already-succeeded prompt) and clears the state completely. The test growth is legitimate — 18 new tests, each asserting behavior that cannot exist on the base branch, including the staleness-abort coverage my previous review asked for. If I had to maintain this in six months, the prepare/gate/admission ordering and the three retry-state invariants are all documented by tests, which is more than most of this file can claim.

Reservations, plainly, none blocking: no in-repo consumer of prepareSubmit is named yet — the investment stands on the package's published-host role, and a real downstream use would validate the ergonomics. The S-level probes from the /review thread stay open as follow-ups (the one with the most teeth: internal daemon syncs like /language ui flow through the host's prepareSubmit, so a host can observe or rewrite them — inherent to hooking the submit path, worth a doc note). And the real-embedding-host layer remains beyond what any in-repo lane can exercise; a maintainer has already verified this commit locally and approved, and the sponsored sandboxed verification from this re-run will report separately — if it surfaces anything, the approval here is commit-pinned and easy to revisit.

This approval supersedes the round-1 changes-requested review on the earlier commit — the finding it cited is fixed and test-pinned on the current head.

中文说明

置信度:4/5 —— 后续提交干净地解决了唯一的 Critical,并为每个修复钉上了真实的验证用例;剩余保留意见是已记录的非阻塞后续项,而非对代码的疑虑。

整体反思:这次重跑主要是检验 autofix 循环的三个提交是否在不让 PR 超出目标的前提下修复了第一轮发现的问题——答案是肯定的。skipPrepareSubmit 的拆分正是最小修复:我核对过 base,失败 prompt 重试对 daemon 可见的行为现在与 main 完全一致,新标志只做一件事。slash 准备后的解除关闭了一条真实可达的过期重试路径(内部 /language 同步上的 turn 错误复活一个早已成功的 prompt),且状态清理是完整的。测试增长是合理的——18 个新测试,每个都断言 base 分支上不可能存在的行为,包括我上一轮要求的过期中断覆盖。如果六个月后由我维护,prepare/门禁/准入的顺序和三条重试状态不变量都有测试背书,这比这个文件的大多数部分都强。

保留意见,直说,均不阻塞:prepareSubmit 尚无仓库内的消费方——这项投入立足于该包发布的宿主角色,一个真实的下游使用场景将验证其人体工学。/review 线程中的 S 级探针作为后续项保留(其中最要紧的一条:/language ui 这类内部 daemon 同步会流经宿主的 prepareSubmit,宿主可以观察甚至改写它们——这是挂接提交路径的固有代价,值得在文档中说明)。真实嵌入宿主这一层仍然超出仓库内任何通道的能力;一位维护者已在本地验证此提交并批准,本次重跑的受赞助沙箱验证会另行报告——如果它发现问题,此处的批准锚定在提交上,容易重新审视。

本次批准取代第一轮在早期提交上的 changes-requested 审查——该审查引用的问题已在当前 head 上修复并有测试钉住。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 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 2f17b33. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

⚠️ No preview: one or more scenarios failed to render on this head — see the workflow run. This is not "no visual change" — a scenario that times out or throws produces no image. Fix the failing scenario (or a genuine regression it caught) and the preview returns on the next push.

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.

LGTM, looks ready to ship — CI landed green after the review. ✅

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

Not explored to full depth (tool budget reached): chunk 2: none — but I did not run the web-shell test suite or a live daemon to dynamically confirm the transcript-loss scenario in the Critical finding; it rests on the ….

Test Plan (not a blocker): 525 passed — this review observed 4164 passed.

中文说明

未探索到全部深度(达到工具调用预算):chunk 2:none — but I did not run the web-shell test suite or a live daemon to dynamically confirm the transcript-loss scenario in the Critical finding; it rests on the …

Test Plan(非阻断):525 passed — this review observed 4164 passed

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

Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx
@wenshao

wenshao commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code takeover

@wenshao

wenshao commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 23, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 1 finishedview run. See this round's report below.

中文说明

AutoFix 第 1 轮已完成 —— 查看运行。本轮报告见下方。

The failed-prompt retry reused `retry: true` only to skip prepareSubmit,
but the flag is also forwarded to the daemon, whose retry branch skips
recording the user message — the retried turn ran and was answered, yet
its user prompt never reached the transcript. Skip preparation with an
internal skipPrepareSubmit flag instead; the Ctrl+Y turn-error retry
keeps `retry: true`, where the daemon semantics are correct. Also
classify the retry payload from the prepared prompt so a host rewrite
that changes slash-ness still arms the turn-error retry.

Adds focused coverage for the resolvePreparedSubmit fallbacks,
preparation-time staleness cancellation on both paths, prepareSubmit
rejection, and the queued empty-prepared guard.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round — PR #9802

Round outcome: changed — 1 Critical fixed, 5 Suggestions addressed (2 of them with code fixes; every point witnessed by new tests). One additive commit: 97a782e3 on codex/webshell-prepare-submit-20260820.

Feedback points and dispositions

  1. [Critical] rc:3838710427 (R1-1) — failed-prompt retry forwards retry: true to the daemon → transcript loss. → Fixed.

    • Reproduced first: a new regression test asserts the retry transport call carries retry: undefined; on the pre-fix code it failed with received retry: true.
    • Root cause verified in code: packages/webui/src/daemon/session/actions.ts forwards options.retry into the daemon prompt request; the daemon Session.ts treats it as isRetry, strips orphaned user entries and skips recordUserMessage. That is correct for the Ctrl+Y turn-error retry (the user message was already recorded), but the failed-prompt path only fires when the prompt was never admitted (definite 413/501 rejection or pre-dispatch error), so no record exists and the retried turn's user message never reached the transcript.
    • Fix: the failed-prompt retry now passes an internal-only skipPrepareSubmit: true (reuses the already-prepared payload without re-running prepareSubmit) instead of retry: true; nothing is forwarded to the daemon, so the user message is recorded on the retried turn. The pre-existing retry: true on the Ctrl+Y handleRetry path is unchanged — its daemon semantics are correct there.
  2. [Suggestion] rc:3838710437 (R1-7) — retry payload classified from the pre-preparation text. → Fixed.

    • Reproduced first: a regression test submits /fix-ci with a host rewrite into a plain prompt; pre-fix, no turn-error retry affordance appeared after a model_stream_interrupted error because the payload was never recorded for retry.
    • Fix: the record block now classifies from preparedPrompt (the post-rewrite payload) instead of the raw text, so a prepareSubmit rewrite that changes slash-ness records what was actually sent and the turn-error retry arms with it.
  3. [Suggestion] rc:3838710435 (R1-2) — resolvePreparedSubmit fallback branches untested. → Tests added.

    • Preparation resolving undefined keeps the original prompt and annotations; resolving { prompt: 'rewritten' } without inputAnnotations inherits the original annotations. A mutation probe (?? inputAnnotations?? []) turns the second test red.
  4. [Suggestion] rc:3838710442 (R1-3) — post-prepare staleness cancellation untested. → Tests added.

    • Two tests with a manually-resolvable prepareSubmit: switch the session owner while preparation is pending, then resolve. Direct path: cancels before the host gate and before any session allocation (no-session start, asserts createSession never called). Queue path: A→B→A owner cycle, nothing enqueued. Draft preserved in both.
  5. [Suggestion] rc:3838710443 (R1-4) — prepareSubmit rejection untested. → Tests added.

    • Direct and queue variants with prepareSubmit = vi.fn().mockRejectedValue(new Error('host failed')): no sendPrompt/rawEnqueuePrompt, draft kept, promise chain settles without crashing.
  6. [Suggestion] rc:3838710446 (R1-5) — queue-path empty-prepared guard untested. → Test added.

    • Mirror of the direct-path test with streamingState = 'responding': preparation returns { prompt: '', inputAnnotations: [] }; asserts rawEnqueuePrompt, editorClear, editorCommit are not called and the draft is kept. A mutation probe deleting the guard turns it red.

Review body and issue-level comments

  • [rv:5002574309] (CHANGES_REQUESTED): its actionable content was the inline findings above — all addressed. The depth note and the Test Plan count (525 passed vs 4164 passed) were informational; the suite now runs 4173 web-shell tests green.
  • [ic:5386028539] (web-shell visual preview failed to render): not reproducible on this runner — the visuals harness needs a Playwright browser install and the failing workflow's logs, neither available here. Evidence against a rendering regression from this PR: the visuals harness installs neither prepareSubmit nor onSubmitBefore, so none of this PR's new async submit paths can execute in it, and all 4173 component/render tests pass. The preview re-renders on every push.
  • [ic:5387688022] (@qwen-code takeover): handled by the workflow; it seeded this round.

Mutation probes (every round-added guard/branch witnessed)

Probe Mutation Witness test Result
A drop skipPrepareSubmit from the prepare gate R1-1 test red, then restored green
B failed-prompt retry sends retry: true again R1-1 test red, then restored green
C record gate classifies from raw text R1-7 test red, then restored green
D resolvePreparedSubmit drops inherited annotations R1-2 second test red, then restored green
E drop the direct-path in-try staleness check R1-3 direct test red, then restored green
F drop the queue-path staleness check R1-3 queue test red, then restored green
G drop the queue-path empty-prepared guard R1-5 test red, then restored green

The source was verified byte-identical to the fixed state after the probe run, and the full suite re-ran green afterwards.

Conflict notes

None — invoked with --conflict false; no merge performed.

Verification

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (all workspaces, exit 0)
  • npm run lint — passed (exit 0)
  • npx prettier --check packages/web-shell/client/App.tsx packages/web-shell/client/App.test.tsx — passed
  • npx vitest run client/App.test.tsx in packages/web-shell — 534 passed (525 baseline + 9 new)
  • npx vitest run for the full packages/web-shell suite — 197 files, 4173 tests passed
  • Regression-before-fix evidence: the Critical and R1-7 tests both failed on pre-fix code with the exact reported symptoms (transport received retry: true; retry affordance absent)
  • Mutation probes A–G above — each turned its witness red, then restored green
  • Not run: integration tests (the touched behavior is web-shell client UI exercised by its own mocked-transport vitest suite, not the bundled CLI); the web-shell visuals Playwright harness (requires browsers unavailable on this runner — see the visuals note above)
中文说明

Autofix 审查轮次 — PR #9802

本轮结果:有改动 — 修复 1 个 Critical,处理 5 个 Suggestion(其中 2 个涉及代码修复;每个问题均有新增测试作为见证)。一个增量提交:97a782e3,位于 codex/webshell-prepare-submit-20260820 分支。

反馈点与处置

  1. [Critical] rc:3838710427 (R1-1) — 失败 prompt 重试向 daemon 转发了 retry: true → transcript 丢失。→ 已修复。

    • 先复现:新增回归测试断言重试的传输层调用应携带 retry: undefined;在修复前代码上,该测试以实际收到 retry: true 而失败。
    • 已在代码中确认根因:packages/webui/src/daemon/session/actions.ts 会把 options.retry 转发进 daemon 的 prompt 请求;daemon 的 Session.ts 将其视为 isRetry,剥离孤立的 user 条目并跳过 recordUserMessage。这对 Ctrl+Y 的 turn-error 重试是正确的(user message 已被记录过),但失败 prompt 路径只在 prompt 从未被准入时触发(明确的 413/501 拒绝或派发前错误),此前不存在任何记录,因此重试 turn 的 user message 永远不会写入 transcript。
    • 修复:失败 prompt 重试现在传递内部专用的 skipPrepareSubmit: true(复用已准备好的 payload,不重跑 prepareSubmit),而不是 retry: true;不会向 daemon 转发任何 retry 语义,因此重试 turn 的 user message 会被正常记录。Ctrl+Y handleRetry 路径上已有的 retry: true 保持不变——那里的 daemon 语义本来就是正确的。
  2. [Suggestion] rc:3838710437 (R1-7) — 重试 payload 使用准备阶段之前的文本分类。→ 已修复。

    • 先复现:回归测试提交 /fix-ci 并由宿主改写为普通 prompt;修复前,出现 model_stream_interrupted 错误后不会显示 turn-error 重试入口,因为该 payload 从未被记录到重试状态中。
    • 修复:记录块现在基于 preparedPrompt(改写后的 payload)而非原始 text 分类,因此改变斜杠开头性质的 prepareSubmit 改写会记录实际发送的内容,并让 turn-error 重试携带正确的 payload。
  3. [Suggestion] rc:3838710435 (R1-2) — resolvePreparedSubmit 兜底分支无测试。→ 已补测试。

    • 准备回调 resolve 为 undefined 时保留原始 prompt 与 annotations;resolve 为 { prompt: 'rewritten' }(不含 inputAnnotations)时继承原始 annotations。变异探针(把 ?? inputAnnotations 改成 ?? [])会让第二个测试变红。
  4. [Suggestion] rc:3838710442 (R1-3) — prepare 之后的过期取消无测试。→ 已补测试。

    • 两个使用可手动 resolve 的 prepareSubmit 的测试:在准备 pending 期间切换会话 owner,然后 resolve。直接路径:在宿主门禁之前、会话分配之前取消(无会话起步,断言 createSession 从未被调用)。排队路径:A→B→A 的 owner 循环,不入队。两条路径草稿均保留。
  5. [Suggestion] rc:3838710443 (R1-4) — prepareSubmit 拒绝无测试。→ 已补测试。

    • 直接路径与排队路径两个变体,使用 prepareSubmit = vi.fn().mockRejectedValue(new Error('host failed')):不调用 sendPrompt/rawEnqueuePrompt,草稿保留,promise 链正常结束且不崩溃。
  6. [Suggestion] rc:3838710446 (R1-5) — 排队路径的空内容守卫无测试。→ 已补测试。

    • streamingState = 'responding' 复刻直接路径的测试:准备回调返回 { prompt: '', inputAnnotations: [] };断言 rawEnqueuePrompteditorCleareditorCommit 均未被调用且草稿保留。删除该守卫的变异探针会让测试变红。

审查主体与 issue 级评论

  • [rv:5002574309]CHANGES_REQUESTED):其中可执行的内容就是上面的内联发现——均已处理。深度说明与 Test Plan 计数(525 passed 对比 4164 passed)属于信息性内容;当前测试套件已有 4173 个 web-shell 测试全绿。
  • [ic:5386028539](web-shell 视觉预览渲染失败):无法在本 runner 上复现——视觉测试框架需要安装 Playwright 浏览器并读取失败 workflow 的日志,两者在此均不可用。认为这不是本 PR 引入的渲染回归的证据:视觉测试框架既不安装 prepareSubmit 也不安装 onSubmitBefore,因此本 PR 新增的任何异步提交流程都不会在其中执行,且全部 4173 个组件/渲染测试通过。预览会在每次 push 后重新渲染。
  • [ic:5387688022]@qwen-code takeover):由 workflow 处理;它触发了本轮。

变异探针(本轮新增的每个守卫/分支均有见证)

探针 变异 见证测试 结果
A 从 prepare 门禁中去掉 skipPrepareSubmit R1-1 测试 变红,恢复后全绿
B 失败 prompt 重试重新发送 retry: true R1-1 测试 变红,恢复后全绿
C 记录块改为基于原始 text 分类 R1-7 测试 变红,恢复后全绿
D resolvePreparedSubmit 丢弃继承的 annotations R1-2 第二个测试 变红,恢复后全绿
E 删除直接路径 try 内的过期检查 R1-3 直接路径测试 变红,恢复后全绿
F 删除排队路径的过期检查 R1-3 排队路径测试 变红,恢复后全绿
G 删除排队路径的空内容守卫 R1-5 测试 变红,恢复后全绿

探针运行结束后已验证源码与修复状态逐字节一致,且全量测试重新跑绿。

冲突说明

无——调用参数为 --conflict false;未执行任何合并。

验证

  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(全部 workspace,exit 0)
  • npm run lint — 通过(exit 0)
  • npx prettier --check packages/web-shell/client/App.tsx packages/web-shell/client/App.test.tsx — 通过
  • packages/web-shell 中运行 npx vitest run client/App.test.tsx — 534 通过(525 基线 + 9 新增)
  • 运行完整 packages/web-shell 测试套件 npx vitest run — 197 个文件、4173 个测试全部通过
  • 修复前的回归证据:Critical 与 R1-7 两个测试在修复前代码上均以报告的症状失败(传输层收到 retry: true;重试入口缺失)
  • 上表变异探针 A–G — 每个都让对应见证测试变红,恢复后全绿
  • 未运行:集成测试(本次触碰的行为是 web-shell 客户端 UI,由其自带的 mock 传输层 vitest 套件覆盖,而非打包后的 CLI);web-shell 视觉 Playwright 框架(需要本 runner 上不可用的浏览器——见上文视觉预览说明)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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. Suggestions are inline.

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

Test Plan (not a blocker): 525 passed — this review observed 4173 passed.

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

  • packages/web-shell/client/App.tsx:6698 — [probe] The queued admission gate receiving the prepared prompt is untested with both prepareSubmit and onSubmitBefore present
  • packages/web-shell/client/App.test.tsx:12149 — [probe] The ordering test never records the 'submit' session-change event, so its position relative to the gate is unpinned
中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

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

Test Plan(非阻断):525 passed — this review observed 4173 passed

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

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

Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.test.tsx
Comment thread packages/web-shell/client/App.test.tsx
Comment thread packages/web-shell/client/App.test.tsx
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Autofix round summary — PR #9802

Round: address-review on codex/webshell-prepare-submit-20260820 (head was 97a782e32a, --conflict false, base main).
Commit this round: fix(web-shell): disarm stale retry state after slash-prepared submits.

Feedback triage

ID Finding Disposition
R2-1 (rc:3839711729) A submit that prepareSubmit rewrites into a slash command skips the retry-recording gate but leaves the previous submit's lastSubmitted* refs armed, so a retryable turn error resends the already-succeeded earlier prompt Fixed in code — reproduced red, fixed green, mutation-probed
R2-2 (rc:3839711731) No test exercises a turn-error retry with non-empty prepared annotations, so a regression in annotation recording survives the suite Fixed — test extended, mutation-probed
R2-3 (rc:3839711733) Nothing pins that a retry skips host preparation (prepareSubmit call count) Fixed — assertion added, mutation-probed
R2-4 (rc:3839711744) No queued-path test pins the snapshot arguments passed to prepareSubmit Fixed — assertion added, mutation-probed
R2-5 (rc:3839711748) The queued path's pre-gate staleness check between prepareSubmit and onSubmitBefore is implemented but unpinned Fixed — test extended, mutation-probed

The two review-body items deferred under the convergence posture (the queued admission gate receiving the prepared prompt with both callbacks present, and the ordering test not recording the submit session-change event) were recorded by the reviewer as not requested this round; no work was done on them. The R2-5 extension now passes both callbacks to the queued staleness test, which incidentally covers part of the first deferred probe's surface.

Changes

R2-1 — disarm stale retry state on slash submits (App.tsx)

Reproduced first: the new test records a first prompt ("hello"), then submits a second prompt that prepareSubmit resolves to /compact foo, then fails the slash turn with a retryable turn_error. On the unmodified code the retry affordance appears armed with the first prompt (test red — the defect); after the fix no affordance appears (test green).

The recording gate now has a scoped else-branch: when a non-retry submit resolves to a slash command (raw or prepared), it disarms lastSubmittedPrompt/Images/Files/InputAnnotations, retryableTurnErrorId/Identity, retriedTurnErrorId, and failedTurnErrorRetry. failedTurnErrorRetryRef is included because rearmFailedTurnErrorRetry would otherwise resurrect the stashed prompt on the next turn error and re-arm the same stale state.

Scoping note: the suggestion's else if (!opts?.retry) was deliberately narrowed to also exclude optimisticUserMessage === false — that shape is the failed-prompt admission retry, whose payload was recorded on first send and must stay armed so a turn error after a successful admission retry still offers the correct prompt. Disarming there would silently remove that affordance and no existing test would have caught it. The implemented condition closes both entrances named in the finding: the new prepared-slash rewrite and the pre-existing raw-slash one.

R2-2 / R2-3 — retry-classification test (App.test.tsx)

classifies the retry payload from the prepared prompt, not the raw text now resolves non-empty prepared annotations and asserts the retry sendPrompt call carries them, and pins prepareSubmit to exactly one call so a re-preparing mutant cannot hide in identical payloads.

R2-4 — queued snapshot pin (App.test.tsx)

freezes a normalized prepared submission before it enters the queue now asserts prepareSubmit received { sessionId, prompt, inputAnnotations } from the composer draft, mirroring the direct-path argument pins.

R2-5 — queued pre-gate staleness pin (App.test.tsx)

cancels a queued submission when the owner changes while preparing now passes onSubmitBefore to the initial render and both rerenders and asserts it is never called after an owner change during preparation, mirroring the direct-path analogue.

Mutation probes (witness verification)

Each new guard was probed by temporarily mutating the source, confirming the witness test FAILS, then restoring and re-running to green:

Probe Mutant Witness result
R2-1 fix disarm branch removed (valid syntax kept) disarms retry state… FAILED; restored → green
R2-2 recording stores opts?.inputAnnotations instead of prepared annotations classifies the retry payload… FAILED; restored → green
R2-3 opts?.retry dropped from the prepare gate classifies the retry payload… FAILED with "expected 'spy' to be called once, but got 2 times"; restored → green
R2-4 queued call passes prompt: '', inputAnnotations: [] freezes a normalized… FAILED; restored → green
R2-5 pre-gate submissionOwnerIsCurrent() check dropped cancels a queued submission… FAILED; restored → green

Additionally, the R2-1 test fails on the pre-round code (defect reproduction), satisfying the pre-round red requirement for a code-level defect fix.

Conflict resolution

None — --conflict false; no merge performed.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --experimental-cli --check packages/web-shell/client/App.tsx packages/web-shell/client/App.test.tsx — passed (App.tsx was reformatted once mid-round; whitespace-only, re-verified)
  • npx vitest run client/App.test.tsx --config vitest.config.ts (packages/web-shell) — 535 passed (535), including the new R2-1 test; also re-run green on the committed state
  • npx vitest run --config vitest.config.ts (full packages/web-shell unit suite) — 197 files, 4174 passed (4174)
  • Integration tests after npm run bundle — not applicable: the changed behavior is web-shell UI retry state, fully exercised by the package's unit suite, not by the bundled CLI or integration harness
  • npm run generate:settings-schema — not applicable: no settings source changed
中文说明

Autofix 轮次总结 — PR #9802

轮次:在 codex/webshell-prepare-submit-20260820 上执行 address-review(轮次开始时 head 为 97a782e32a--conflict false,base 为 main)。
本轮提交:fix(web-shell): disarm stale retry state after slash-prepared submits

反馈分类

编号 发现 处置
R2-1(rc:3839711729 prepareSubmit 把提交改写为斜杠命令时,会跳过重试记录门禁,但上一次提交遗留的 lastSubmitted* 引用仍处于 armed 状态,于是可重试的 turn error 会重发那条早已成功的旧 prompt 已在代码中修复——先复现(红),修复后转绿,并完成变异探针验证
R2-2(rc:3839711731 没有任何测试用非空 prepared annotations 演练过 turn-error 重试,注解记录上的回归可以让整个测试套件全绿 已修复——扩展测试,并完成变异探针验证
R2-3(rc:3839711733 没有任何断言钉住"重试会跳过宿主准备"(prepareSubmit 调用次数) 已修复——补充断言,并完成变异探针验证
R2-4(rc:3839711744 没有任何排队路径测试钉住传给 prepareSubmit 的快照参数 已修复——补充断言,并完成变异探针验证
R2-5(rc:3839711748 排队路径在 prepareSubmitonSubmitBefore 之间的门禁前过期检查已实现但没有测试钉住 已修复——扩展测试,并完成变异探针验证

审查正文中按收敛姿态延后的两条(同时传入两个回调时排队准入门禁接收 prepared prompt 的场景、以及排序测试未记录 submit 会话变更事件)被审查者明确记录为本轮不要求修改,本轮未对它们做任何工作。R2-5 的扩展测试现在同时向排队过期测试传入两个回调,顺带覆盖了第一条延后探针的部分场景。

变更内容

R2-1 — 斜杠提交时解除旧重试状态的 armed 状态(App.tsx)

先复现:新测试先记录第一条 prompt("hello"),再提交一条被 prepareSubmit resolve 为 /compact foo 的提交,然后让该斜杠 turn 以可重试的 turn_error 失败。在未修改的代码上,重试入口带着第一条 prompt 出现(测试红——即缺陷本身);修复后不再出现重试入口(测试绿)。

记录门禁现在新增了一个限定作用域的 else 分支:当一个非重试提交 resolve 为斜杠命令(无论是原始输入还是准备阶段改写)时,解除 lastSubmittedPrompt/Images/Files/InputAnnotationsretryableTurnErrorId/IdentityretriedTurnErrorIdfailedTurnErrorRetry 的 armed 状态。之所以包含 failedTurnErrorRetryRef,是因为 rearmFailedTurnErrorRetry 否则会在下一个 turn error 时把暂存的 prompt 重新 arm 起来,让同样的旧状态复活。

作用范围说明:建议中的 else if (!opts?.retry) 被有意收窄,额外排除了 optimisticUserMessage === false 这一形态——那是失败 prompt 的准入重试,其 payload 在首次发送时已被记录,必须保持 armed,这样准入重试成功后的 turn error 才能继续针对正确的 prompt 提供重试入口。在那里解除武装会静默移除该入口,而且现有测试无法捕获这一回归。实际实现的条件关闭了该发现点名的两个入口:新的"准备阶段改写为斜杠"入口,以及本就已存在的原始斜杠入口。

R2-2 / R2-3 — 重试分类测试(App.test.tsx)

classifies the retry payload from the prepared prompt, not the raw text 现在 resolve 出非空的 prepared annotations,并断言重试的 sendPrompt 调用携带这些注解;同时钉住 prepareSubmit 恰好被调用一次,防止"重新准备"的变异体藏在相同 payload 后面溜过去。

R2-4 — 排队快照钉住(App.test.tsx)

freezes a normalized prepared submission before it enters the queue 现在断言 prepareSubmit 收到的是来自草稿的 { sessionId, prompt, inputAnnotations } 快照,与直接路径的参数钉住保持一致。

R2-5 — 排队门禁前过期检查钉住(App.test.tsx)

cancels a queued submission when the owner changes while preparing 现在向初始渲染与两次 rerender 都传入 onSubmitBefore,并断言在准备期间 owner 变化后它从未被调用,与直接路径的对应测试保持一致。

变异探针(见证验证)

每个新增守卫都通过临时变异源码做了探针验证:确认见证测试失败,然后恢复并重跑至绿:

探针 变异体 见证结果
R2-1 修复 移除解除武装分支(保持语法合法) disarms retry state… 失败;恢复后转绿
R2-2 记录块改存 opts?.inputAnnotations 而非 prepared annotations classifies the retry payload… 失败;恢复后转绿
R2-3 从 prepare 门禁中移除 opts?.retry classifies the retry payload… 失败,报错 "expected 'spy' to be called once, but got 2 times";恢复后转绿
R2-4 排队调用传入 prompt: ''inputAnnotations: [] freezes a normalized… 失败;恢复后转绿
R2-5 删除门禁前的 submissionOwnerIsCurrent() 检查 cancels a queued submission… 失败;恢复后转绿

此外,R2-1 的测试在轮次前的代码上失败(缺陷复现),满足代码级缺陷修复对"轮前必须红"的要求。

冲突处理

无——--conflict false,未执行任何合并。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --experimental-cli --check packages/web-shell/client/App.tsx packages/web-shell/client/App.test.tsx — 通过(App.tsx 在轮次中做过一次格式化,纯空白改动,已重新验证)
  • npx vitest run client/App.test.tsx --config vitest.config.ts(packages/web-shell)— 535 passed (535),包含新增的 R2-1 测试;在已提交状态上重跑同样为绿
  • npx vitest run --config vitest.config.ts(packages/web-shell 完整单元测试套件)— 197 个文件,4174 passed (4174)
  • npm run bundle 之后的集成测试 — 不适用:本次变更的行为是 web-shell UI 的重试状态,完全由该包的单元测试套件覆盖,不经过打包 CLI 或集成测试框架
  • npm run generate:settings-schema — 不适用:未改动任何 settings 源

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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. Suggestions are inline.

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

Test Plan (not a blocker): 525 passed — this review observed 4174 passed.

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

  • packages/web-shell/client/App.tsx:6064 — [probe] Empty-prepared bail-out runs after submitBefore, so the host gate observes a submission that is then discarded
  • packages/web-shell/client/App.tsx:9311 — [probe] Deferred composer commit is unpinned when prepareSubmit is the only async preflight; mutant keeps the suite green
  • packages/web-shell/client/App.tsx:6714 — [probe] No test verifies the queued path's onSubmitBefore gate receives the prepared prompt (recorded in round 2 as a deferred finding at App.tsx:6698)
  • packages/web-shell/client/App.tsx:9363 — [probe] Uncertain-admission recovery payload is unpinned to the prepared prompt/annotations
  • packages/web-shell/client/App.tsx:571 — [probe] Explicit inputAnnotations: [] stripping non-empty originals is unpinned in resolvePreparedSubmit
  • packages/web-shell/client/App.tsx:5980 — [probe] App-internal control sends (/language ui, /model --fast) are routed through the host's prepareSubmit and can be rewritten
  • packages/web-shell/client/App.tsx:6680 — [probe] Queued-path async preflight has no re-entrancy guard; a repeated Enter while prepareSubmit is pending enqueues the draft twice

Convergence: round 3 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 5 (5 new). Findings keep coming back to the same files: packages/web-shell/client/App.test.tsx (findings in round 2; 1 more now); packages/web-shell/client/App.tsx (findings in round 2; 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.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:reverse audit — did not converge within the reverse-audit round cap of 5。

Test Plan(非阻断):525 passed — this review observed 4174 passed

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

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

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

Comment thread packages/web-shell/client/App.test.tsx
Comment thread packages/web-shell/client/App.tsx
… stash variants

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

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Address-review summary — PR #9802 (round 3)

Feedback dispositions

  • [rc:3840151717] [Suggestion] Attachment-path disarm clears unpinned — IMPLEMENTED. The claim was verified against the code first: canRetry (App.tsx ~7690) is a disjunction over the prompt/images/files refs, so with attachments in play, deleting only the two lastSubmittedImagesRef / lastSubmittedFilesRef clears from the slash-prepared disarm branch leaves stale attachment refs that re-arm the retry affordance for an unrelated slash turn. Added keeps attachment retry state disarmed after a slash-prepared submit: submits hello with an image and a file via latestChatEditorProps.onSubmit('hello', images, files, editorCommit) (existing attachment-test pattern), performs the slash-prepared submit, renders a turn-error block, and asserts [data-testid="retry"] stays null. Both the images and files clears are now pinned individually.
  • [rc:3840151723] [Suggestion] Deferred turn-error retry stash clear unpinned — IMPLEMENTED. Verified against the code: handleRetry's catch stashes the payload in failedTurnErrorRetryRef (~10667) when the daemon rejects the retry attempt itself with a turn error, and rearmFailedTurnErrorRetry (~4406) resurrects that payload for a later turn error unless the stash is cleared. Added keeps a stashed turn-error retry disarmed after a slash-prepared submit: submits A, renders turn-error E1, clicks retry with sendPrompt mocked to admit then reject with a daemon turn error (populating the stash), performs the slash-prepared submit, then renders a new turn error E2 and asserts [data-testid="retry"] stays null.
  • [rv:5003939653] Review body — no action required. The seven probe items it lists are explicitly recorded as "Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round". The Test Plan observation (525 passed vs the review's 4174 passed count) was flagged as not a blocker; the authoritative current count after this round is 537 passed in App.test.tsx.

Changes

  • packages/web-shell/client/App.test.tsx (+136): the two witness tests above, inserted next to the existing disarms retry state when preparation turns the submit into a slash command test. No source changes.

Mutation probes (witness verification)

Each new guard line was proven load-bearing by temporarily deleting it and re-running the focused tests:

  • Probe A — removed both attachment clears from the disarm branch: attachment test FAILED (expected <button data-testid="retry"> to be null), stash test passed. Restored.
  • Probe A2 — removed only the lastSubmittedFilesRef clear: attachment test FAILED, stash test passed. Restored.
  • Probe B — removed failedTurnErrorRetryRef.current = null from the disarm branch: stash test FAILED, attachment test passed. Restored.
  • After restoring all probes the full suite is green and git diff shows only the test file changed.

Conflict notes

--conflict false; no merge performed.

Verification

  • npm run build — passed (exit 0, no errors)
  • npm run typecheck — passed (0 TypeScript errors)
  • npm run lint — passed (0 errors, 0 warnings)
  • npx vitest run client/App.test.tsx in packages/web-shell — 537 passed (535 pre-existing + 2 new)
  • npx prettier --check packages/web-shell/client/App.test.tsx — passed
  • Integration tests after npm run bundle — not run: this round is a test-only change inside the web-shell package, exercised by its unit-test harness, not by the bundled CLI or integration harness.
中文说明

处理评审摘要 — PR #9802(第 3 轮)

反馈处理结论

  • [rc:3840151717] [Suggestion] 附件路径的解除(disarm)清理未被测试钉住 — 已实现。 先对照代码核实了该说法:canRetry(App.tsx ~7690)是 prompt/images/files 三个引用的析取,因此在带附件的场景下,仅删除斜杠准备解除分支中对 lastSubmittedImagesRef / lastSubmittedFilesRef 的两处清理,遗留的附件引用就足以为一个无关斜杠 turn 的 turn error 重新武装重试入口。新增测试 keeps attachment retry state disarmed after a slash-prepared submit:通过 latestChatEditorProps.onSubmit('hello', images, files, editorCommit)(与本文件其他附件测试相同的写法)带一张图片和一个文件提交 hello,执行斜杠准备提交,渲染 turn-error block,并断言 [data-testid="retry"] 保持为 null。images 与 files 两处清理现在各自都被单独钉住。
  • [rc:3840151723] [Suggestion] 延迟 turn-error 重试暂存的清理未被测试钉住 — 已实现。 对照代码核实:当 daemon 以 turn error 拒绝重试尝试本身时,handleRetry 的 catch 会把 payload 暂存到 failedTurnErrorRetryRef(~10667);若该暂存未被清除,rearmFailedTurnErrorRetry(~4406)会为之后的 turn error 复活该 payload。新增测试 keeps a stashed turn-error retry disarmed after a slash-prepared submit:提交 A、渲染 turn-error E1、把 sendPrompt mock 为先 admit 再以 daemon turn error 拒绝(填充暂存)后点击重试,执行斜杠准备提交,再渲染新的 turn error E2 并断言 [data-testid="retry"] 保持为 null。
  • [rv:5003939653] 评审正文 — 无需处理。 其中列出的 7 条探针项明确标注为“在收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改”。Test Plan 观察项(525 passed 与评审观察到的 4174 passed 计数差异)已标注为非阻断;本轮之后 App.test.tsx 的权威当前计数为 537 passed。

变更内容

  • packages/web-shell/client/App.test.tsx(+136):上述两个见证测试,插入在既有的 disarms retry state when preparation turns the submit into a slash command 测试旁。无源码变更。

变异探针(见证验证)

通过临时删除每条新增守卫对应行并重跑聚焦测试,逐条证明其必要性:

  • 探针 A — 删除解除分支中的两处附件清理:附件测试失败(expected <button data-testid="retry"> to be null),暂存测试通过。已恢复。
  • 探针 A2 — 仅删除 lastSubmittedFilesRef 的清理:附件测试失败,暂存测试通过。已恢复。
  • 探针 B — 删除解除分支中的 failedTurnErrorRetryRef.current = null:暂存测试失败,附件测试通过。已恢复。
  • 全部恢复后整套测试全绿,git diff 仅显示测试文件变更。

冲突说明

--conflict false;未执行任何合并。

验证

  • npm run build — 通过(exit 0,无错误)
  • npm run typecheck — 通过(0 个 TypeScript 错误)
  • npm run lint — 通过(0 错误、0 警告)
  • packages/web-shell 中运行 npx vitest run client/App.test.tsx — 537 通过(既有 535 + 新增 2)
  • npx prettier --check packages/web-shell/client/App.test.tsx — 通过
  • npm run bundle 后的集成测试 — 未运行:本轮为 web-shell 包内的纯测试变更,由该包的单元测试框架覆盖,不经过打包 CLI 或集成测试框架。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

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

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

Not explored to full depth (tool budget reached): "agent reverse-audit (round 3)": Enter-key submit path gating on isPreparing inside useComposerCore not verified — only the send-button path (ChatEditor composerPreparing ) was confirmed, ….

Test Plan (not a blocker): 525 passed — this review observed 4176 passed.

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

  • packages/web-shell/client/App.tsx:9363 — [probe] Unknown-admission prepared-payload record unpinned
  • packages/web-shell/client/App.tsx:6716 — [probe] Queued-path host gate prepared-prompt assertion missing
  • packages/web-shell/client/App.tsx:5980 — [probe] Internal daemon syncs routed through host prepareSubmit (settings/editor language, /model --fast)
  • packages/web-shell/client/App.test.tsx:5537 — [review] /help local handling lacks a positive test assertion
  • packages/web-shell/client/App.test.tsx:12152 — [review] Empty-prepared cancel test omits the isPreparing reset assertion
  • packages/web-shell/client/App.tsx:567 — [review] Malformed prepareSubmit result discarded without console.warn
  • packages/web-shell/client/App.tsx:9541 — [review] Language-sync deferComposerCommit extension unpinned
  • packages/web-shell/client/App.test.tsx:12240 — [probe] Mid-flight isPreparing === true never pinned
  • packages/web-shell/client/App.test.tsx:5537 — [probe] Local slash-branch return-true composer-commit contract unpinned
  • packages/web-shell/client/App.tsx:6135 — [probe] Internal syncs disarm a pending turn-error retry even on success
  • packages/web-shell/client/App.test.tsx:12198 — [probe] Annotation-strip (empty array) branch unpinned
  • packages/web-shell/client/App.test.tsx:12169 — [probe] Malformed-result fallback behaviour unpinned
  • packages/web-shell/client/App.tsx:6064 — [probe] Empty-prepared bail-out runs after the host gate

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had none either, so the next review re-reads the whole diff and will keep doing so until a round's marker carries an anchor again. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

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

未审查:reverse audit — did not converge within the reverse-audit round cap of 5。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 3)"Enter-key submit path gating on isPreparing inside useComposerCore not verified — only the send-button path (ChatEditor composerPreparing ) was confirmed, …

Test Plan(非阻断):525 passed — this review observed 4176 passed

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

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round: no action (growth-audit round, verdict sound)

This round was triggered by the growth brake, not by new actionable feedback: Critical-only mode is active because this counting window's net growth (source 23 / test 499 lines vs budgets 400/400) crossed the test budget. Per the growth-audit rule, the audit ran before any other work this round.

Growth audit result: sound (KISS pass, minimal change pass)

Recorded in growth-audit.json. The window's growth (commits 97a782e, 4c1094a, 2f17b33) is net +23 source lines and +499 test lines, and every hunk traces to the PR's feature or an accepted review finding:

  • +7 source lines (skipPrepareSubmit option + gate): minimal fix for Critical R1-1 — the failed-prompt retry's retry: true was forwarded to the daemon, whose retry branch skips recording the user message (transcript loss). Reusing retry: true is the defect itself, so an internal skip flag is the smallest correct fix.
  • +16 source lines (slash-prepared disarm branch) and the isUserPrompt removal: fix for R2-1/R1-7 — a host rewrite that turns the prepared prompt into a slash command left the previous submit's retry state armed, so a turn error on the slash turn could resend the already-succeeded earlier prompt. A blanket "clear on any submit" would also disarm the failed submit's own Ctrl+Y retry, so the targeted branch is the minimal shape.
  • +499 test lines (12 witness tests, no new helpers or fixtures): each test pins one of the findings' own mutation probes (R1-2/R1-3/R1-4/R1-5/R2-1/R2-3/R3-1/R3-2). The round-3 findings shipped probes proving that deleting individual clear lines in the disarm branch keeps the suite green; the attachment and stash variants are the sole witnesses for those lines, so shrinking them would re-open the exact regressions the fixes prevent.

The KISS axis tried four structurally simpler designs (reuse retry: true, blanket retry-state clear, per-turn-keyed retry state, deleting variant tests) and all are refuted by the recorded findings and their probes. No untraceable hunks; all changes stay inside the PR's own packages/web-shell/client footprint.

Why no code change this round

  • Actionable sections are empty: no Reviews, no Inline comments, no Issue-level comments, no Failed checks, no Still-red checks.
  • The only remaining review (round 4, pullrequestreview-5004613358, COMMENTED) sits in the Deferred non-Critical feedback section — an audit record under Critical-only mode, not work: no code changes, no thread resolutions, no comment replies for those items.
  • All 40 CI checks on the current head are green (SUCCESS/SKIPPED).

No commit was made; the branch head is unchanged at 2f17b3335d.

中文说明

Autofix 轮次:无操作(增长审计轮,结论 sound

本轮由增长刹车触发,而非新的可操作反馈:由于本计数窗口的净增长(源码 23 / 测试 499 行,预算 400/400)超出了测试预算,仅处理 Critical 的模式已生效。按增长审计规则,审计在本轮任何其他工作之前执行。

增长审计结果:sound(KISS 通过,最小改动通过)

已记录于 growth-audit.json。窗口内的增长(提交 97a782e4c1094a2f17b33)为源码净 +23 行、测试 +499 行,每个代码块都可追溯到本 PR 的功能或已被接受的评审发现:

  • 源码 +7 行(skipPrepareSubmit 选项与门控): Critical R1-1 的最小修复——失败提示重试原先携带的 retry: true 会被转发给 daemon,而 daemon 的重试分支会跳过记录用户消息(导致转录丢失)。复用 retry: true 本身就是缺陷,因此内部跳过标志是最小的正确修复。
  • 源码 +16 行(slash-prepared 解除分支)及 isUserPrompt 的移除: R2-1/R1-7 的修复——宿主改写把准备后的提示变成斜杠命令时,上一次提交记录的重试状态仍处于武装状态,slash 回合出现 turn error 时可能重发已经成功的先前提示。若采用"任何提交都清除"的一刀切方案,会同时解除失败提交自身的 Ctrl+Y 重试,因此定向分支才是最小形态。
  • 测试 +499 行(12 个见证测试,无新增辅助函数或 fixture): 每个测试都钉住发现自带的变异探针之一(R1-2/R1-3/R1-4/R1-5/R2-1/R2-3/R3-1/R3-2)。第 3 轮发现自带探针证明:删除解除分支中的个别清理行后整个套件仍然全绿;附件与暂存变体测试是这些清理行的唯一见证,缩减它们会重新打开这些修复本要防止的回归。

KISS 轴尝试了四种结构上更简单的方案(复用 retry: true、一刀切清除重试状态、按回合键控重试状态、删除变体测试),全部被记录的发现及其探针证伪。没有无法追溯的代码块;所有改动都在本 PR 自身的 packages/web-shell/client 足迹范围内。

本轮为何不改代码

  • 可操作区域为空:没有 Reviews、没有 Inline comments、没有 Issue-level comments、没有 Failed checks、没有 Still-red checks。
  • 唯一剩余的评审(第 4 轮,pullrequestreview-5004613358,COMMENTED)位于 Deferred non-Critical feedback 区域——在仅处理 Critical 模式下属于审计记录而非工作:不为这些条目改代码、不解决线程、不写评论回复。
  • 当前 head 上的全部 40 项 CI 检查均为绿色(SUCCESS/SKIPPED)。

未创建提交;分支 head 保持在 2f17b3335d 不变。

Deferred non-Critical feedback

Critical-only mode is active: the PR's diff grew src 23 / test 499 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback continues to flow unaffected during a growth-only engagement (the per-author batch budget applies only after 5 change-producing rounds). (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 23 / 测试 499 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Local verification report for PR #9802

Status: ✅ Verified — feature works as intended, tests/typecheck/lint/build all green.

Environment

  • Host: macOS (Apple Silicon)
  • Node.js: v22.22.2
  • npm: 10.9.7
  • Local branch: pr-9802 (fetched from origin/pull/9802/head)
  • Commit under test: 2f17b3335d (HEAD of PR branch)

What was verified

PR #9802 introduces an optional asynchronous prepareSubmit customization on WebShell/WebShellWithProviders. It lets embedded hosts rewrite the outgoing prompt and inputAnnotations after local slash/shell commands have been routed, but before the submit gate, session allocation, queue admission, and transport.

Verification steps

Step Command Result
1. Clean install + build npm install (triggers preparebuild) ✅ success
2. TypeScript typecheck (web-shell) cd packages/web-shell && npm run typecheck ✅ no errors
3. ESLint (repo-wide) npm run lint ✅ no errors
4. Full web-shell unit-test suite cd packages/web-shell && npm run test 197 files, 4,176 tests passed
5. App.test.tsx (main touched file) npx vitest run client/App.test.tsx 537 tests passed
6. prepareSubmit-focused tests npx vitest run client/App.test.tsx -t "prepare" 8 passed, 529 skipped

Key test coverage observed

The new tests exercise the contract described in the PR body:

  1. Direct submission preparationprepareSubmit runs before onSubmitBefore, and the prepared payload reaches both sendPrompt and the onSessionChange submit event.
  2. Local commands are skippedprepareSubmit is not called for /help, /theme dark, or !echo hello.
  3. Queued submissions use a frozen snapshot — a prompt queued while another turn is active captures the prepared payload at admission time; later host-side mutation does not affect the queued entry.
  4. Retries reuse the prepared payload — failed-prompt retries skip prepareSubmit and resend the already prepared snapshot.
  5. Empty prepared prompt cancels submission — returning { prompt: '' } aborts the send while keeping the draft intact.
  6. Rejection handling — a rejected prepareSubmit cancels the send/queue, leaves the draft, and clears isPreparing.
  7. Staleness guards — preparation is discarded if the session owner, session id, workspace, or composer source version changes while the async callback is in flight.

Implementation notes from code review

  • prepareSubmit is wired through a ref (prepareSubmitRef) so the async preflight can see the latest host callback without re-creating sendPrompt/enqueuePrompt on every render.
  • Direct sends normalize the result with resolvePreparedSubmit(...) and fall back to the original prompt/annotations when the callback returns undefined.
  • Queued sends wrap the same resolvePreparedSubmit(...) path and pass the snapshot to rawEnqueuePrompt, preserving images/files.
  • Retries set skipPrepareSubmit: true so a retried failed prompt does not re-resolve mutable host context.
  • The empty-prompt guard (!prepared.prompt.trim() && no images/files) is shared between direct and queued paths.

Caveats / non-blocking observations

Recommendation

Approve for merge after the usual rebase/CI cycle. The change is minimal, well-tested, and keeps the existing onSubmitBefore wait-only contract intact while adding the requested preparation stage.


📝 中文验证报告(点击展开)

PR #9802 本地验证报告

状态: ✅ 验证通过 — 功能按预期工作,测试 / 类型检查 / Lint / 构建全部通过。

验证环境

  • 主机:macOS(Apple Silicon)
  • Node.js:v22.22.2
  • npm:10.9.7
  • 本地分支:pr-9802(从 origin/pull/9802/head 检出)
  • 被测提交:2f17b3335d(PR 分支 HEAD)

验证内容

PR #9802WebShell/WebShellWithProviders 新增了一个可选的异步回调 prepareSubmit。嵌入方可以在本地 slash/shell 命令路由完成后、提交门控 / 会话分配 / 队列准入 / 传输之前,重写即将发送的 prompt 与 inputAnnotations

验证步骤

步骤 命令 结果
1. 清理安装并构建 npm install(触发 preparebuild ✅ 成功
2. web-shell TypeScript 类型检查 cd packages/web-shell && npm run typecheck ✅ 无错误
3. 全仓库 ESLint npm run lint ✅ 无错误
4. web-shell 完整单元测试 cd packages/web-shell && npm run test 197 个文件,4,176 个测试通过
5. App.test.tsx(主要改动文件) npx vitest run client/App.test.tsx 537 个测试通过
6. prepareSubmit 聚焦测试 npx vitest run client/App.test.tsx -t "prepare" 8 个通过,529 个跳过

核心测试覆盖

新增测试完整覆盖了 PR 描述中的行为约定:

  1. 直接提交准备prepareSubmitonSubmitBefore 之前运行,准备好的 payload 同时进入 sendPromptonSessionChangesubmit 事件。
  2. 本地命令跳过准备/help/theme dark!echo hello 不会调用 prepareSubmit
  3. 队列提交使用冻结快照 — 在另一轮回复进行中队列的 prompt,会在准入时捕获准备好的快照;后续宿主端修改不会影响已队列的内容。
  4. 重试复用已准备 payload — 失败 prompt 的重试跳过 prepareSubmit,直接重发已准备好的快照。
  5. 空 prompt 取消提交 — 返回 { prompt: '' } 会中止发送并保留草稿。
  6. 拒绝处理prepareSubmit reject 时取消发送/队列,保留草稿,并清除 isPreparing
  7. 过期保护 — 如果异步回调执行期间 session owner、session id、workspace 或 composer source version 发生变化,准备结果会被丢弃。

代码审阅要点

  • prepareSubmit 通过 ref(prepareSubmitRef)接入,保证异步预检阶段始终看到最新的宿主回调,而无需在每次渲染时重建 sendPrompt/enqueuePrompt
  • 直接发送使用 resolvePreparedSubmit(...) 规范化结果,回调返回 undefined 时回退到原始 prompt/annotations。
  • 队列发送复用同一条 resolvePreparedSubmit(...) 路径,并将快照传给 rawEnqueuePrompt,保留图片/文件附件。
  • 重试通过 skipPrepareSubmit: true 避免重新解析可能已变化的宿主上下文。
  • 空 prompt 保护逻辑在直接发送和队列发送两条路径中一致。

注意事项 / 非阻塞观察

建议

按常规 rebase / CI 流程后即可批准合并。该改动范围小、测试充分,且在保持现有 onSubmitBefore 仅等待约定不变的前提下,新增了所请求的准备阶段。

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge August 24, 2026 05:57
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 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: 5294 passed · 1 failed · 5295 total

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

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

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

脚本断言:5294 通过 · 1 失败 · 5295 总计

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

Verification report

PR 9802 Deep Verification — feat(web-shell): add async submit preparation

Verdict: findings — all executed behavioral assertions passed except one
pre-registered mutation-lattice expectation (a runtime type-guard the PR's new
tests do not pin; the guard itself is proven correct). Central claim proven
load-bearing: 16/18 new tests flip red→green between base and head; full
web-shell suite 4207/4207 green; typecheck and lint green.
Assertion ledger: 5294 pass / 1 fail / 5295 total.
Verified head: 2f17b3335d5b41101e96651a9017b05ed3b51ec1 (git rev-parse HEAD^2),
base tip: b2edb80a57bf640ccb933d7e90d5b066f854f014 (HEAD^1).

中文摘要

结论:findings(唯一发现为 Suggestion 级,不阻塞合并)

  • A/B 结论:将 head 的测试文件原样套到 base 工作树上运行(哈希一致,
    唯一变量是 App.tsx):base 为 16 红 / 521 绿(537),head 为
    0 红 / 537 绿。16 个红色恰好全部是 prepareSubmit 行为测试,
    证明中心改动是承重的;其余 521 个测试两侧一致,隔离干净。
    剩余 2 个新测试在 base 上按设计即通过(本地命令跳过回调、无
    prepareSubmit 时的兼容性门禁),属于回归守卫而非 diff 钉桩。
  • findings:仅 1 条 Suggestion——resolvePreparedSubmit
    typeof prepared.prompt !== 'string' 守卫删除后 18 个新测试全绿
    (变异存活)。补做的 scratch 夹具有效证明该守卫行为正确
    (pristine 绿、变异红),因此定性为覆盖缺口而非缺陷;
    commit 2 声称“为 resolvePreparedSubmit 的 fallback 增加了针对性覆盖”,
    但该子句未被任何测试钉住。建议补上本报告给出的夹具。
  • 未覆盖范围:逐提交归属(浅克隆仅 3 个提交可达);Windows/Linux
    实机运行;真实 daemon 的转录记录行为(仅以 daemon 自身类型契约与
    发送代码佐证 retry: true 语义);排队 slash 提交不清空重试状态的
    既有形态(base 即如此,本 PR 未改变,仅直接路径获得 disarm 修复)。

Central claim and A/B proof

Central claim (from PR body): prepareSubmit replaces the outgoing prompt
and input annotations after local command routing and before submission gates,
session allocation, queue admission, and transport; direct and queued
submissions use the prepared snapshot; retries reuse the prepared payload
without re-invoking the callback.

A/B design. The PR's own head App.test.tsx is the harness. It was copied
byte-identical into a base worktree (sha256 348d2bf3… equal on both arms),
so the only variable between cells is App.tsx. The two external workspace
imports (@qwen-code/webui/daemon-react-sdk, @qwen-code/sdk/daemon) are
fully vi.mock()ed in the harness, and the PR touches no lockfile or
package.json, so the shared node_modules (realpaths verified to resolve
into the head tree for webui and sdk-typescript) is not a confound.

cell App.tsx harness (App.test.tsx) oracle result
head 2f17b333 sha256 348d2bf3… vitest, full file 537 passed, 0 failed
base b2edb80a sha256 348d2bf3… (identical) vitest, full file 16 failed / 521 passed (537)

The 16 base failures are exactly the prepareSubmit behavior tests
(prepares direct transport before the submit gate and session event,
keeps the draft when preparation removes all prompt content,
cancels a direct submission when the session owner changes while preparing,
classifies the retry payload from the prepared prompt, not the raw text,
the three slash-prepared disarm tests, freezes a normalized prepared submission before it enters the queue, both queued-staleness/reject/empty
tests, both retry-reuse tests, etc.). No other test differs between the arms —
the diff is fully isolated. Witness: 02-ab-base-16-flips.png,
01-ab-head-537-green.png.

Two new tests pass on base by design and are labeled accordingly:
skips prompt preparation for locally handled slash and shell commands
(base has no callback wiring, so "not called" holds trivially) and
gates direct submissions and dispatches compatible submit events
(pins the pre-existing onSubmitBefore path against refactor regressions).
Neither weakens the A/B; they are forward guards, not diff pins.

Reviewer Test Plan, per step

  1. Prepare before gate; prepared payload reaches transport + event
    pinned by prepares direct transport before the submit gate and session event, which asserts the exact order ['prepare', 'gate:resolved', 'transport:resolved'], the transport call with prepared annotations, and
    the onSessionChange submit event carrying the prepared prompt. Flips
    red→green in the A/B; killed 7 ways by mutant M1. ✅
  2. Local commands skip the callback/help, /theme dark, !echo hello exercise local routing; prepareSubmit asserted not called. Green
    on both arms (control shape, see above). ✅
  3. Queued turn sends the preparation-time snapshot — pinned by freezes a normalized prepared submission before it enters the queue
    (rawEnqueuePrompt receives 'queued target' + prepared annotations,
    prepareSubmit called exactly once, queued submit event carries the
    prepared prompt). The drain side is by-value: useQueuedPrompts'
    submitPendingPrompt sends the stored prompt.text /
    prompt.inputAnnotations through sessionActions.submitPrompt directly —
    it never re-enters the sendPrompt wrapper, so the callback cannot re-run
    at drain. Nuance: the plan's "mutate the host-side value after queue
    admission" is pinned indirectly (enqueue-by-value + calledOnce), not by
    an actual host mutation — the mock callback resolves a constant. ✅
  4. Retry reuses the prepared payload without re-preparingreuses the first prepared payload without preparing retry again asserts
    prepareSubmit calledOnce, both onSubmitBefore calls see
    'resolved', and the retried transport call carries the prepared
    annotations. ✅
  5. Empty prepared prompt cancels, draft keptkeeps the draft when preparation removes all prompt content (direct) and keeps the draft when preparation empties a queued prompt (queued); both assert no transport,
    no editor commit/clear, draft text intact. Both flip red→green. ✅

Corrections

None needed — no prior review round or bot comment on record required one.

Findings

1. Suggestion — resolvePreparedSubmit's non-string-prompt guard is unpinned (mutation survivor)

Mutation matrix (see below): deleting the typeof prepared.prompt !== 'string' clause from resolvePreparedSubmit leaves all 18 new tests green
(mutant M6 survived). Adjudication per method — the survivor got a finer
probe, not a coarser verdict:

  • Scratch fixture (prepareSubmit resolves { prompt: 42 }, asserts
    transport still receives the original 'hello'): green on pristine
    head
    — the guard works and falls back safely.
  • Same fixture against the M6 mutant: red — without the clause the
    submission is silently cancelled (TypeError → catch) instead of falling
    back. So the clause is live and load-bearing for untyped hosts, but
    no test pins it.

Classification: coverage gap (behavior right, nothing asserts it), not
dead code and not redundant defense — no other hunk covers a non-string
prompt. This is the one assertion failure in the ledger, because commit 2's
message claims "focused coverage for the resolvePreparedSubmit fallbacks" and
this branch of the fallback condition is uncovered. Non-blocking: TypeScript
hosts cannot produce the input, and the guard is proven correct. Suggested
fixture (drop-in next to keeps the original payload when preparation resolves without a result):

it('keeps the original payload when preparation returns a non-string prompt', async () => {
  const prepareSubmit = vi.fn().mockResolvedValue({ prompt: 42 });
  const { container } = renderApp({ prepareSubmit });
  await flush();

  await clickSubmit(container);
  await flush();

  expect(prepareSubmit).toHaveBeenCalledOnce();
  expect(mockSessionActions.sendPrompt).toHaveBeenCalledWith(
    'hello',
    expect.objectContaining({ retry: undefined }),
  );
});

(Same defensive surface, one line away: prepared.inputAnnotations ?? inputAnnotations trusts any truthy non-array — a string there spreads into
per-character "annotations" on the wire. An Array.isArray check would close
both shapes; no typed host can hit either.)

Mutation matrix (vacuity + guard attribution)

Scratch worktree at HEAD; one mutation per run; harness = the 18 new tests
selected via -t; unmutated control first; each run's raw log in
logs/mut-*.log. Pre-registered expectation: control green; every introduced
guard killed by at least one named test. Witness: 03-mutation-matrix.png.

mutant change result killed by
M0 control none 18/18 green (harness-live positive control)
M1 transport sends raw text instead of preparedPrompt KILLED, 7 red prepares direct transport…; inherits…; classifies the retry payload…; 3× slash-disarm tests; reuses the first prepared payload…
M2 drop post-prepare staleness check (direct path) KILLED, 1 red cancels a direct submission when the session owner changes while preparing
M3 drop empty-prepared cancellation guard KILLED, 1 red keeps the draft when preparation removes all prompt content
M4 drop slash-prepared retry-disarm branch KILLED, 3 red the disarm test + attachment variant + stashed-retry variant
M5 failed-prompt retry forwards daemon retry: true instead of skipPrepareSubmit KILLED, 1 red does not resend a failed-prompt retry with daemon retry semantics
M6 drop typeof prepared.prompt !== 'string' from resolve guard SURVIVED — (adjudicated above as coverage gap)
M7 drop submissionOwnerIsCurrent recheck on queued path KILLED, 1 red cancels a queued submission when the owner changes while preparing

The positive control is in the same file as every mutant (all tests live in
App.test.tsx): M1 turning 7 tests red proves the runner collects and fails
this file; M0 proves the scenario set is green unmutated. Every kill is
attributed to the test named for the guard it pins — no unrelated test went
red for any mutant.

Commit 2's daemon-semantics claim was verified against the daemon's own
contract, not the PR's prose: packages/webui/src/daemon/session/types.ts
documents retry?: boolean as "strips orphaned user entries … and skips
recording a duplicate user message in the JSONL transcript. Used by Ctrl+Y
retry", and actions.ts forwards it on the wire (promptRequest['retry'] = true). At head, the turn-error retry keeps retry: true (App.tsx:10606)
while the failed-prompt retry uses skipPrepareSubmit (App.tsx:6516) — the
split the commit describes, pinned by M5's dedicated test.

Targeted gates

gate command result
web-shell full suite (head) npx vitest run --config vitest.config.ts in packages/web-shell 199 files, 4207/4207 passed (logs/full-suite-head.log)
typecheck npm run typecheck (tsc -p tsconfig.json --noEmit) exit 0 (logs/typecheck.log)
lint (workspace) npm run lint in packages/web-shell (eslint over the package) exit 0
lint liveness planted unused-var file in scratch tree reported (@typescript-eslint/no-unused-vars), then removed
built artifact ships the API grep of dist/ prepareSubmit prop in dist/types/App.d.ts, types re-exported in dist/types/index.d.ts, minified bundle contains the skipPrepareSubmit selection logic

Not covered

  • Per-commit attribution. Shallow merge-ref checkout (depth 2): only the
    merge commit, base tip, and PR head are reachable; git rev-list at a
    shallow boundary returns 1, while the metadata snapshot lists 4 commits.
    The aggregate HEAD^1..HEAD diff was verified; individual commits
    (including commit 2's and 3's fix claims) only as part of the aggregate.
  • Live-daemon transcript behavior. The retry: true transcript-skip
    consequence was verified from the daemon's type contract and wire code, not
    by running a real daemon turn — the harness mocks @qwen-code/webui at the
    SDK seam. This reproduces the client-side wire shape, not a daemon-side
    transcript write.
  • Windows / Linux rendering runs (author flagged ⚠️); no visual change is
    claimed and the surface is logic-only, so nothing was attempted here.
  • Repo-wide gates. Only the affected workspace (packages/web-shell) was
    run — per the PR's own note the CLI baseline has unrelated aggregate-run
    failures; the PR has no CLI diff and repo-wide preflight was out of scope.
  • Queued slash-prepared submits and retry state. The disarm branch covers
    the direct path only. A slash prompt that enters via the queue and later
    turn-errors would still find the previous submit's retry state, because the
    drain bypasses the wrapper entirely. This shape is pre-existing on base
    (queue drain never went through the wrapper there either) and unchanged by
    this PR — recorded as an observation for a possible follow-up, not as a
    finding against this diff.
  • verify-capture.mjs covered all three headline captures; no gaps.

Assertion ledger breakdown

source expected outcome pass fail
A/B head arm (537 vitest tests) all green 537 0
A/B base arm (same 537 tests) 521 green + 16 named reds 537 0
Matrix M0 control (18 new tests green) green 1 0
Matrix M1–M5, M7 (guard deleted → named test red) killed 6 0
Matrix M6 (typeof guard deleted → any new test red) killed 0 1
Scratch non-string fixture on pristine head green 1 0
Scratch non-string fixture on M6 mutant red 1 0
Full web-shell suite (4207 tests) all green 4207 0
typecheck / lint changed / lint workspace / lint liveness exit 0 / clean / clean / caught 4 0
total 5294 1

Methodology

Environment: CI verify container (node:22-bookworm class), Node v22.23.2,
merge-ref checkout at depth 2 (HEAD = merge commit 5442c29e, HEAD^1 =
base tip b2edb80a, HEAD^2 = verified head 2f17b333; the metadata
snapshot's baseRefOid 431a0bd9 is stale relative to the merge checkout —
HEAD^1 is authoritative per the CI contract). npm ci + npm run build
were pre-run at HEAD; no rebuild was needed because vitest transforms TS
source directly. A/B base cell: git worktree add tmp/base-tree HEAD^1 with
root and package-local node_modules symlinked (realpaths asserted:
@qwen-code/webui → head tree's packages/webui, @qwen-code/sdk
packages/sdk-typescript; both fully vi.mock()ed by the harness, and the
PR touches no dependency manifest — clean control). The head test file was
copied into the base tree (sha256-verified identical) so the harness is a
constant. Mutation runs used a second scratch worktree at HEAD
(tmp/head-scratch), restoring the pristine App.tsx between mutants and
asserting each anchor replaced exactly once. Raw logs: logs/ab-head.log,
logs/ab-base.log, logs/mut-M*.log, logs/mutation-matrix.progress,
logs/full-suite-head.log, logs/typecheck.log, capture stderr in
logs/cap-*.stderr. Harness scripts: mutation-run.mjs in this directory.

Flakiness gate log

rounds=5 files=1 skipped=0
file packages/web-shell/client/App.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/App.test.tsx


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/web-shell/client/App.test.tsx: PPPPP

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

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/App.test.tsx: P (exit 0)

Evidence images

01-ab-head-537-green

02-ab-base-16-flips

03-mutation-matrix

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.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Aug 24, 2026
Merged via the queue into QwenLM:main with commit 717ad10 Aug 24, 2026
69 checks passed
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

All 13 open inline findings were already fixed by earlier commits on this branch; this round re-verified every one of them against the current head 2f17b33 and found nothing left to change, so it resolves the threads without a code change (re-verification is a no-code claim by design).

What landed when

  • 97a782e fixed the Critical (failed-prompt retry no longer forwards retry to the daemon; an internal skipPrepareSubmit flag skips host preparation instead) and classified the retry payload from the prepared prompt; it also added the coverage requested by R1-2, R1-3, R1-4 and R1-5.
  • 4c1094a added the slash-prepared disarm branch and pinned R2-1..R2-5 (prepared annotations in the retry assertion, prepareSubmit call-count pin, queued snapshot arguments, queued owner-change test with both callbacks).
  • 2f17b33 pinned the disarm clears with the attachment and deferred-retry-stash variants requested by R3-1 and R3-2.

Findings, point by point

Finding Status Fixed by Witness test(s) Mutation probe (mutant ⇒ witness fails)
rc:3838710427 R1-1 [Critical] daemon retry semantics on failed-prompt retry resolved in code 97a782e does not resend a failed-prompt retry with daemon retry semantics; reuses the first prepared payload without preparing retry again re-adding retry: true to the failed-prompt retry ⇒ retry assertion fails
rc:3838710435 R1-2 resolvePreparedSubmit fallbacks untested resolved in code 97a782e keeps the original payload when preparation resolves without a result; inherits the original annotations when preparation returns only a prompt ?? inputAnnotations?? [] ⇒ inheritance test fails
rc:3838710437 R1-7 retry gate classified from raw text resolved in code 97a782e classifies the retry payload from the prepared prompt, not the raw text classifying from the raw text instead of preparedPrompt ⇒ retry affordance never arms, test fails
rc:3838710442 R1-3 preparation-time staleness cancellation untested resolved in code 97a782e cancels a direct submission when the session owner changes while preparing; cancels a queued submission when the owner changes while preparing; cancels an approved queued submission after an A-to-B-to-A owner cycle dropping the direct post-prepare check, the queued pre-gate check, or the queued post-gate check ⇒ the matching test fails
rc:3838710443 R1-4 prepareSubmit rejection untested resolved in code 97a782e keeps the draft when prepareSubmit rejects; cancels queued submissions when prepareSubmit rejects swallowing the direct rejection and proceeding, or letting the queued catch enqueue the unprepared prompt ⇒ the matching test fails
rc:3838710446 R1-5 queued empty-prepared guard untested resolved in code 97a782e keeps the draft when preparation empties a queued prompt deleting the queued empty-content guard ⇒ enqueue/clear assertions fail
rc:3839711729 R2-1 stale retry state armed after slash-prepared submit resolved in code 4c1094a disarms retry state when preparation turns the submit into a slash command deleting the disarm branch ⇒ all three disarm tests fail
rc:3839711731 R2-2 prepared annotations unpinned in retry recording resolved in code 4c1094a classifies the retry payload from the prepared prompt, not the raw text (non-empty inputAnnotations asserted on the retry call) recording opts?.inputAnnotations instead of the prepared ones ⇒ retry-payload assertion fails
rc:3839711733 R2-3 retry-skips-preparation unpinned resolved in code 4c1094a same test, expect(prepareSubmit).toHaveBeenCalledOnce() after the retry click dropping opts?.retry from the prepare gate ⇒ prepareSubmit runs twice, test fails
rc:3839711744 R2-4 queued snapshot arguments unpinned resolved in code 4c1094a freezes a normalized prepared submission before it enters the queue (toHaveBeenCalledWith) feeding the queued host call prompt: '' ⇒ snapshot assertion fails
rc:3839711748 R2-5 queued pre-gate staleness check unpinned resolved in code 4c1094a cancels a queued submission when the owner changes while preparing (both callbacks passed, onSubmitBefore asserted not called) dropping the pre-gate submissionOwnerIsCurrent() check ⇒ onSubmitBefore is called, test fails
rc:3840151717 R3-1 attachment clears in disarm unpinned resolved in code 2f17b33 keeps attachment retry state disarmed after a slash-prepared submit deleting the image/file clears from the disarm branch ⇒ stale attachments re-arm the affordance, test fails
rc:3840151723 R3-2 deferred-retry-stash clear unpinned resolved in code 2f17b33 keeps a stashed turn-error retry disarmed after a slash-prepared submit deleting the failedTurnErrorRetryRef clear from the disarm branch ⇒ the stash resurrects for the next turn error, test fails

Every probe was run against the real tree: apply the mutant, confirm the named witness fails, restore, confirm green. The post-restore tree is byte-identical to head 2f17b33.

Issue-level comments

  • ic:5386028539 (web-shell visual preview failed to render on 2f17b33): investigated, not actionable from this runner. The workflow logs are not reachable here (no GitHub credentials in this environment) and the runner has no browser binaries to execute the Playwright scenarios locally. Code inspection found no plausible regression path: the visuals harness passes neither prepareSubmit nor onSubmitBefore, and every behavioral change in this PR is gated behind those host callbacks (the only ungated deltas — the empty-prompt bail-out, the slash-submit disarm, and empty-annotation normalization — cannot trigger in the canned scenarios, which submit local slash commands and one plain prompt against the mock daemon). The preview is informational, not a required check, and regenerates on the next push; nothing was changed on speculation.
  • ic:5387688022 (@qwen-code takeover): this comment is the takeover trigger itself — no action to take.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npm run test --workspace @qwen-code/web-shell — 4176 passed, 0 failed, 0 errors (matches the round-4 review's observed count)
  • 15 focused mutation probes on the finding witnesses — all bite (mutant fails its witness; restored tree green)
  • Focused baseline of the 17 finding-related tests — 17 passed
  • git status clean at 2f17b33 (no code change this round)
中文说明

全部 13 条仍开放的内联反馈均已由本分支早前的提交修复;本轮在当前 head 2f17b33 上逐条重新核实,确认没有需要再改动的内容,因此以"无代码改动"的方式解决这些线程(重新核实按设计就是无代码声明)。

各项修复的落点

  • 97a782e 修复了 Critical(失败 prompt 重试不再向 daemon 转发 retry,改由内部 skipPrepareSubmit 标志跳过宿主准备),并改为基于准备后的 prompt 对重试 payload 分类;同时补充了 R1-2、R1-3、R1-4、R1-5 所要求的覆盖。
  • 4c1094a 新增了斜杠准备提交的解除(disarm)分支,并钉死了 R2-1..R2-5(重试断言中的准备后 annotations、prepareSubmit 调用次数、排队路径快照参数、同时传入两个回调的排队 owner 切换测试)。
  • 2f17b33 按 R3-1、R3-2 的要求,用附件变体和延迟重试暂存(stash)变体钉死了 disarm 分支中的各项清理。

逐点说明

反馈 状态 修复提交 见证测试 变异探针(变异体 ⇒ 见证测试失败)
rc:3838710427 R1-1 [Critical] 失败 prompt 重试带上了 daemon 重试语义 已在代码中解决 97a782e does not resend a failed-prompt retry with daemon retry semanticsreuses the first prepared payload without preparing retry again 在失败 prompt 重试处重新加回 retry: true ⇒ 重试断言失败
rc:3838710435 R1-2 resolvePreparedSubmit 兜底分支无测试 已在代码中解决 97a782e keeps the original payload when preparation resolves without a resultinherits the original annotations when preparation returns only a prompt ?? inputAnnotations 改为 ?? [] ⇒ 继承测试失败
rc:3838710437 R1-7 重试门禁基于原始文本分类 已在代码中解决 97a782e classifies the retry payload from the prepared prompt, not the raw text 改为基于原始 text 而非 preparedPrompt 分类 ⇒ 重试入口不再出现,测试失败
rc:3838710442 R1-3 准备期间的过期取消无测试 已在代码中解决 97a782e cancels a direct submission when the session owner changes while preparingcancels a queued submission when the owner changes while preparingcancels an approved queued submission after an A-to-B-to-A owner cycle 分别删除直接路径准备后的检查、排队门禁前检查、排队门禁后检查 ⇒ 对应测试失败
rc:3838710443 R1-4 prepareSubmit 拒绝无测试 已在代码中解决 97a782e keeps the draft when prepareSubmit rejectscancels queued submissions when prepareSubmit rejects 吞掉直接路径的拒绝并继续发送,或让排队路径的 catch 把未准备的 prompt 入队 ⇒ 对应测试失败
rc:3838710446 R1-5 排队路径空准备结果守卫无测试 已在代码中解决 97a782e keeps the draft when preparation empties a queued prompt 删除排队路径的空内容守卫 ⇒ 入队/清空断言失败
rc:3839711729 R2-1 斜杠准备提交后旧的重试状态仍被武装 已在代码中解决 4c1094a disarms retry state when preparation turns the submit into a slash command 删除整个解除分支 ⇒ 三个解除测试全部失败
rc:3839711731 R2-2 重试记录中的准备后 annotations 未被钉住 已在代码中解决 4c1094a classifies the retry payload from the prepared prompt, not the raw text(对重试调用断言非空 inputAnnotations 记录 opts?.inputAnnotations 而非准备后的 annotations ⇒ 重试 payload 断言失败
rc:3839711733 R2-3 "重试跳过宿主准备"未被钉住 已在代码中解决 4c1094a 同一测试,重试点击后断言 expect(prepareSubmit).toHaveBeenCalledOnce() 从准备门禁中移除 opts?.retry ⇒ prepareSubmit 被调用两次,测试失败
rc:3839711744 R2-4 排队路径快照参数未被钉住 已在代码中解决 4c1094a freezes a normalized prepared submission before it enters the queuetoHaveBeenCalledWith 给排队路径的宿主调用传入 prompt: '' ⇒ 快照断言失败
rc:3839711748 R2-5 排队路径门禁前过期检查未被钉住 已在代码中解决 4c1094a cancels a queued submission when the owner changes while preparing(同时传入两个回调,并断言 onSubmitBefore 未被调用) 删除门禁前的 submissionOwnerIsCurrent() 检查 ⇒ onSubmitBefore 被调用,测试失败
rc:3840151717 R3-1 解除分支中的附件清理未被钉住 已在代码中解决 2f17b33 keeps attachment retry state disarmed after a slash-prepared submit 删除解除分支中的 images/files 清理 ⇒ 遗留附件重新武装重试入口,测试失败
rc:3840151723 R3-2 延迟重试暂存(stash)清理未被钉住 已在代码中解决 2f17b33 keeps a stashed turn-error retry disarmed after a slash-prepared submit 删除解除分支中的 failedTurnErrorRetryRef 清理 ⇒ 暂存在下一次 turn error 时复活,测试失败

每个探针都在真实代码树上执行:应用变异体、确认指定见证测试失败、恢复、确认恢复后全绿。恢复后的代码树与 head 2f17b33 逐字节一致。

Issue 级评论

  • ic:5386028539(web-shell 视觉预览在 2f17b33 上渲染失败):已排查,本 runner 上无法处理。工作流日志在此环境不可达(没有 GitHub 凭据),且 runner 上没有浏览器可执行文件,无法在本地运行 Playwright 场景。代码检查未发现可信的回归路径:视觉预览的 harness 既不传 prepareSubmit 也不传 onSubmitBefore,而本 PR 的所有行为变化都由这两个宿主回调门禁控制(唯一不受门禁控制的变化——空 prompt 兜底退出、斜杠提交解除、空 annotations 归一化——在固定场景中不可能触发,因为这些场景只提交本地斜杠命令和一条发往 mock daemon 的普通 prompt)。该预览是信息性的,不是必选检查,并会在下一次 push 时重新生成;因此不做任何猜测性改动。
  • ic:5387688022(@qwen-code takeover):该评论本身就是接管触发指令——无需采取任何行动。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npm run test --workspace @qwen-code/web-shell — 4176 通过,0 失败,0 错误(与第 4 轮评审观察到的数量一致)
  • 针对反馈见证的 15 个聚焦变异探针 — 全部咬合(变异体使其见证失败;恢复后的代码树全绿)
  • 17 个反馈相关测试的聚焦基线 — 17 通过
  • git status2f17b33 上干净(本轮无代码改动)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

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

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants