Skip to content

test(core): pin transport retry diagnostics and correct the replay-safety comment - #8861

Merged
wenshao merged 7 commits into
QwenLM:mainfrom
ComplexSimply:test/thinking-retry-diagnostics-followup
Aug 25, 2026
Merged

test(core): pin transport retry diagnostics and correct the replay-safety comment#8861
wenshao merged 7 commits into
QwenLM:mainfrom
ComplexSimply:test/thinking-retry-diagnostics-followup

Conversation

@ComplexSimply

Copy link
Copy Markdown
Collaborator

What this PR does

Follow-up to #7938, landing the two non-blocking findings from the maintainer verification that were fixed locally but did not make it into the branch before the merge landed.

  1. Corrects the replay-safety rationale in both comment sites (the hasNonThoughtCandidateParts docstring and the replay gate comment). The comments claimed thought parts are never recorded in history; that is not the invariant — the successful attempt's thoughts are recorded there. What actually makes the replay safe is that the failed attempt's accumulated partial turn is discarded wholesale before the retry (popPendingPartialAssistantTurn), and thought parts are never user-visible content, so nothing the caller saw from the failed attempt can appear twice.

  2. Pins the two retry diagnostics with test assertions. yieldedNonContentChunks on the "Transport stream retry scheduled" log is now asserted in the thinking-only replay test, and the skipped_after_content decision on the "Transport stream retry not taken" log is covered by a new test. Since fix(core): resume long streams cut by a socket-level close #7896 added continuation recovery, a text cut after visible content no longer reaches the not-taken log — the one path on current main that still emits skipped_after_content is a cut after a delivered functionCall, where the replay gate (non-thought output delivered) and the continuation gate (functionCall boundary excluded) are both closed. The new test exercises exactly that path.

The source diff to geminiChat.ts is comment-only; there is zero runtime behavior change.

Why it's needed

The verification on #7938 confirmed the fix merge-ready but reported two follow-ups: an imprecise safety rationale in the comments (a future reader relying on "thoughts never enter history" would be misled), and two mutation survivors showing the diagnostic fields were untested — hardcoding yieldedNonContentChunks or relabeling skipped_after_content passed the full suite. Those diagnostics are what make thinking-phase replays auditable in the debug log, so they should be pinned.

Reviewer Test Plan

How to verify

  • vitest run src/core/geminiChat.test.ts in packages/core — 301/301 pass.
  • Mutation check for finding (b), each mutant fails exactly one test:
    • yieldedNonContentChunks: streamYieldedChunkyieldedNonContentChunks: false ⇒ 1 failed / 300 passed (killed by "retries a transport stream error after yielding only thinking chunks").
    • 'skipped_after_content''skipped_after_chunk' ⇒ 1 failed / 300 passed (killed by the new "attributes a blocked replay to delivered content when a function call was cut").
  • For finding (a), check the two corrected comments against the code: the replay branch calls popPendingPartialAssistantTurn() before retrying, and recordHistory keeps the successful attempt's thought parts.

Evidence (Before & After)

N/A (comment-only source change + test additions).

Tested on

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

Environment (optional)

N/A (unit tests only).

Risk & Scope

  • Main risk or tradeoff: none at runtime — the source change is comments; the new assertions could in principle over-pin the log shape, but they assert only the two fields the verification flagged, via objectContaining.
  • Not validated / out of scope: nothing beyond the geminiChat suite plus tsc/eslint/prettier on the two files; the remaining verification follow-ups tracked elsewhere (continuation semantics) are untouched.
  • Breaking changes / migration notes: none.

Linked Issues

Follow-up to #7938 (issue #7832); addresses the two findings in #7938 (comment).

中文说明

本 PR 做了什么

#7938 的后续:落地维护者验证报告中两个不阻塞 finding 的修复(当时已在本地完成,但没赶在合并前推上分支)。

  1. 修正两处注释中的重放安全性论证hasNonThoughtCandidateParts 的 docstring 与重放门控处的注释)。原注释声称 thought part 永远不会进入 history;这不是真正的不变量——成功那次 attempt 的 thought 是会被记录的。重放之所以安全,是因为失败 attempt 累积的 partial turn 在重试前会被整体丢弃(popPendingPartialAssistantTurn),且 thought part 永远不是用户可见内容,所以调用方从失败 attempt 看到的任何东西都不可能出现两次。

  2. 用测试断言钉住两个重试诊断字段。"Transport stream retry scheduled" 日志上的 yieldedNonContentChunks 现由 thinking-only 重放测试断言;"Transport stream retry not taken" 日志上的 skipped_after_content 由一个新测试覆盖。由于 fix(core): resume long streams cut by a socket-level close #7896 引入了续传恢复,可见文本被切断后已不再走 not-taken 日志——当前 main 上仍会产生 skipped_after_content 的路径是 functionCall 送达后被切断:重放门控(已送达非 thought 输出)与续传门控(functionCall 边界被排除)同时关闭。新测试正是覆盖这条路径。

geminiChat.ts 的源码 diff 仅为注释,运行时行为零变化。

为什么需要

#7938 的验证确认修复可合并,但报告了两个后续项:注释中的安全性论证不精确(未来读者若依赖「thought 不进 history」会被误导);两个变异体幸存说明诊断字段无测试——硬编码 yieldedNonContentChunks 或改写 skipped_after_content 标签都能通过全量套件。这些诊断字段是 thinking 阶段重放在 debug 日志中可审计的依据,应当钉住。

验证方式

  • packages/corevitest run src/core/geminiChat.test.ts —— 301/301 通过。
  • finding (b) 的变异检查,每个变异体恰好导致一个测试失败:yieldedNonContentChunks 硬编码为 false ⇒ 1 失败 / 300 通过;'skipped_after_content' 改为 'skipped_after_chunk' ⇒ 1 失败 / 300 通过(由新增的 function-call 切断测试杀死)。
  • finding (a):对照代码检查两处修正后的注释——重放分支在重试前调用 popPendingPartialAssistantTurn(),且 recordHistory 保留成功 attempt 的 thought part。

风险与范围

  • 主要风险:运行时无——源码改动仅注释;新断言理论上可能过度固定日志形状,但只通过 objectContaining 断言验证报告点名的两个字段。
  • 未验证/范围外:除 geminiChat 套件与两文件的 tsc/eslint/prettier 外未做其他验证;验证报告中其余后续项(续传语义)不在本 PR 范围。
  • 破坏性变更:无。

关联 Issue

#7938(issue #7832)的后续;处理 #7938 (comment) 中的两个 finding。

…fety comment

Follow-up to the QwenLM#7938 maintainer verification, addressing both
non-blocking findings.

The comments justifying the thinking-phase replay claimed thought parts
are never recorded in history. That is not the invariant: the successful
attempt's thoughts are recorded. What makes the replay safe is that the
failed attempt's accumulated partial turn is discarded wholesale before
the retry (popPendingPartialAssistantTurn) and thought parts are never
user-visible content. Both comment sites now state that.

The two retry diagnostics were unpinned: hardcoding
yieldedNonContentChunks on the scheduled log or relabeling the
skipped_after_content decision on the not-taken log survived the suite.
The scheduled-log field is now asserted in the thinking-only replay
test, and a new test covers the path on current main that still emits
skipped_after_content — a cut after a delivered functionCall, where the
replay gate and the continuation gate are both closed. Each mutant now
fails exactly one test.
@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-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 10, 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-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run: five commits landed since the last pass (review-round fixes + a merge of main), so the gate re-ran against the current head.

Template looks good ✓

Problem: observed and documented — this is the direct follow-up to the maintainer verification on #7938, which explicitly named both items: an imprecise replay-safety rationale in the comments, and yieldedNonContentChunks / skipped_after_content surviving mutation with no coverage. Nothing theoretical here.

Direction: aligned — comment accuracy plus test pinning for the retry diagnostics that make thinking-phase replays auditable in the debug log. Exactly the kind of follow-up a verification report should produce.

Size: core path touched. Production logic: 0 lines — both geminiChat.ts hunks sit entirely inside comment blocks (verified hunk-by-hunk). Tests: +198/−65. No generated/schema files.

Approach: minimal — the diff answers the two named findings one-for-one. The fixture hoist (socketCut/cutAfter/drainCollecting) came out of the review rounds and is also what the new tests need at the outer describe scope; it replaces six inline copies. No drive-by changes.

Risk: geminiChat is on the revert-correlated high-risk path list, so Stage 2 quotes the full CI picture before approval. The change surface here is comments + tests only, so there is no runtime behavior risk to settle beyond that.

Moving on to code review. 🔍

中文说明

重跑:自上次审查后分支新增了五个提交(review 轮次修复 + 合并 main),因此对当前 head 重新执行了 gate。

模板完整 ✓

问题:已观测且有据可查——这是 #7938 维护者验证报告的直接后续,报告明确点名了这两项:注释中不准确的 replay 安全性论证,以及 yieldedNonContentChunks / skipped_after_content 在 mutation 测试中存活且无覆盖。不是理论性问题。

方向:对齐——修正注释并补测试,固定让 thinking 阶段重放在调试日志中可审计的重试诊断。正是验证报告应该产出的那种后续。

规模:触及核心路径。生产逻辑:0 行——geminiChat.ts 的两处改动完全位于注释块内(逐 hunk 核实)。测试:+198/−65。无生成/schema 文件。

方案:最小化——diff 与两个点名的 finding 一一对应。fixture 提升(socketCut/cutAfter/drainCollecting)来自 review 轮次,也是新测试在外层 describe 所需的;它替换了六处内联副本。无顺手改动。

风险:geminiChat 位于与 revert 相关的高风险路径列表,因此 Stage 2 会在批准前引用完整 CI 结果。本次改动面仅为注释 + 测试,除此之外没有需要验证的运行时行为风险。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review of the five commits landed since the last pass — no blockers. I checked the claims against the source rather than taking the description at face value:

  • The source diff is genuinely comment-only. Both geminiChat.ts hunks sit entirely inside comment blocks (the hasNonThoughtCandidateParts docstring and the replay-gate comment); zero code lines change, so "zero runtime behavior change" holds.
  • The corrected rationale is accurate. Verified against the code: mid-stream persistence on error reduces to hasToolCall && … — plain-text and thinking-only partial turns are deliberately not persisted; thought parts are excluded from the continuation buffer (getPlainTextFromParts filters them); and the replay gate excludes delivered functionCalls. So the retired "thoughts never enter history" claim was indeed wrong, and the replacement grounds replay safety in what the code actually does. The docstring now also states plainly that popPendingPartialAssistantTurn() has nothing to pop on this path today — defense in depth — which resolves the earlier misattribution thread.
  • The new assertions pin both branches of both diagnostics. yieldedNonContentChunks true (thinking-only retry) and false (tool-preparation retry); retryDecision: 'exhausted' both with and without thought chunks flowing — the thinking variant is the scenario where streamYieldedChunk and streamYieldedContentChunk diverge, which is what kills a ternary re-key. skipped_after_content is covered twice: by the new functionCall-cut test and by the continuation-budget-exhaustion test. Timer arithmetic matches TRANSPORT_STREAM_RETRY_CONFIG (2 retries at 1s+2s within the 10s advance; 3 continuations at 1s+2s+3s within 30s), and the suite's top-level vi.clearAllMocks() keeps the toHaveBeenCalledWith pins honest. The shared-fixture comment correctly calls out that the it.each drift-guard constructs retryable shapes inline on purpose.
  • Nit (non-blocking): the PR body says the only path still emitting skipped_after_content is a cut after a delivered functionCall, but the assertion this PR adds to the continuation-budget test pins the plain-text path too. The tests cover more than the body claims — the body predates the last three commits.
  • One open Suggestion from /review round 5 remains on the current head: swapping the diagnostic to the cross-attempt flag (streamYieldedChunkstreamYieldedAnyChunk) survives, because the two flags agree in every current scenario. Suggestion-level, diagnostic-field only — reasonable as a follow-up if a maintainer wants that mutant dead, not merge-blocking. The author's mutation-kill counts are their own measurements; this review re-checked the assertion shapes against the code but never runs PR code.

Testing — the PR's own CI on the reviewed commit (triage never builds or runs PR code): the Qwen Code CI run is green on this commit. The macOS/Windows/integration skips are by design — those jobs run only in the merge queue, with ubuntu as the PR signal. No red checks, nothing to attribute.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped (merge-queue-only)
Test (windows-latest, Node 22.x) ⏭️ skipped (merge-queue-only)
Integration Tests (CLI, No Sandbox) ⏭️ skipped (merge-queue-only)
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
precheck-pr / precheck ✅ success

Real-scenario testing: N/A — comment-only source change, nothing user-visible, and this is an unattended run.

中文说明

针对上次审查后新增的五个提交的代码审查——无阻断问题。我对照源码而非仅凭 PR 描述核实了各项声明:

  • 源码 diff 确实只有注释。 geminiChat.ts 的两处改动完全位于注释块内(hasNonThoughtCandidateParts 的 docstring 与重放门注释),没有任何代码行变化,"零运行时行为变化"成立。
  • 修正后的论证是准确的。 对照代码核实:流中出错时的持久化条件归约为 hasToolCall && …——纯文本和纯 thinking 的 partial turn 有意不持久化;thought part 被排除在续传缓冲区之外(getPlainTextFromParts 会过滤);重放门排除已交付的 functionCall。因此被废弃的"thought 从不进入 history"说法确实是错的,新注释把重放安全性建立在代码实际行为之上。docstring 现在也明确说明 popPendingPartialAssistantTurn() 在此路径上目前无物可弹——属于纵深防御——解决了此前关于归因错误的讨论串。
  • 新断言固定了两个诊断字段的两个分支。 yieldedNonContentChunks 为 true(仅 thinking 的重试)与 false(仅 tool-preparation 的重试);retryDecision: 'exhausted' 在有/无 thought chunk 两种情形下都被断言——thinking 变体正是 streamYieldedChunkstreamYieldedContentChunk 取值不同的场景,因此能杀死三元键替换 mutant。skipped_after_content 有双重覆盖:新增的 functionCall 切断测试与续传预算耗尽测试。计时与 TRANSPORT_STREAM_RETRY_CONFIG 一致(2 次重试 1s+2s,在 10s 推进内;3 次续传 1s+2s+3s,在 30s 内),套件顶层的 vi.clearAllMocks() 保证 toHaveBeenCalledWith 断言不被污染。共享 fixture 的注释也正确说明了 it.each 漂移守卫测试有意内联构造可重试错误形状。
  • 小问题(非阻断):PR 描述说仍在发出 skipped_after_content 的唯一路径是 functionCall 交付后的切断,但本 PR 给续传预算耗尽测试添加的断言同样固定了纯文本路径。测试覆盖比描述更广——描述写于最后三个提交之前。
  • /review 第 5 轮在当前 head 上遗留一个 Suggestion:把诊断字段换成跨尝试标志(streamYieldedChunkstreamYieldedAnyChunk)的 mutant 存活,因为在现有所有场景中两个标志取值一致。Suggestion 级别,只影响诊断字段——如果维护者想杀死该 mutant,可以作为后续跟进,不阻塞合并。作者的 mutation 杀死计数是其自测结果;本审查对照代码复核了断言形状,但从不运行 PR 代码。

测试——被审查提交上 PR 自身的 CI(triage 从不构建或运行 PR 代码):Qwen Code CI 在该提交上为绿色。macOS/Windows/集成测试的跳过是设计使然——这些任务只在合并队列中运行,PR 阶段以 ubuntu 为信号。无红色检查,无需归因。

真实场景测试:N/A——源码改动仅注释,没有用户可见变化,且本次为无人值守运行。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, tightly-scoped follow-up; the only open items are a Suggestion-level surviving mutant on a diagnostic field and a PR-body sentence the tests outgrew.

Stepping back: this is the kind of PR the gate should wave through. The motivation is a maintainer's own verification report naming two findings, and the diff answers them one-for-one with nothing else attached. My independent proposal — rewrite both comment sites around the actual invariant (error-path persistence needs a delivered functionCall, which the replay gate excludes; pop-before-retry as defense in depth) and pin each diagnostic field in the tests that already exercise its paths — matches what landed, and the final state goes one step further by pinning the ternary's key with a scenario where the attempt flags diverge. I verified the corrected comments against the code rather than taking them on faith, and both hold. The five review rounds did their job: each round's substantive suggestions (the stale test-header rationale, duplicated fixtures, the untested false and exhausted arms) are visibly answered in the current diff. The one round-5 Suggestion still open (cross-attempt flag swap) is worth a follow-up issue if a maintainer wants that mutant dead, but it does not gate this PR. A maintainer has already approved exactly this commit, CI is green on it, and nothing user-visible changed.

Verdict: approve. CI is settled on the reviewed commit, so no deferred-approval marker is needed.

中文说明

置信度:4/5 —— 干净、范围紧凑的后续 PR;仅剩的开放项是一个 Suggestion 级别的诊断字段 mutant,以及 PR 描述中一句已被测试覆盖超越的话。

退一步看:这正是 gate 应该放行的那种 PR。动机是维护者自己的验证报告点名的两个 finding,diff 一一对应,没有夹带任何其他内容。我的独立方案——围绕真实不变量重写两处注释(错误路径持久化需要已交付的 functionCall,而重放门将其排除;重试前的 pop 作为纵深防御),并在已覆盖相应路径的测试中固定每个诊断字段——与最终落地的方案一致,且最终版本更进一步:用两个尝试标志取值不同的场景固定了三元键。我对照代码核实了修正后的注释而非轻信,两者都成立。五轮 review 尽到了职责:每一轮的实质性建议(测试头部过时的论证、重复的 fixture、未被测试的 false 与 exhausted 分支)都在当前 diff 中得到了可见的回应。第 5 轮遗留的一个 Suggestion(跨尝试标志互换)如果维护者想杀死该 mutant,值得开个后续 issue,但不构成本 PR 的门槛。维护者已在完全相同的提交上批准,该提交的 CI 为绿色,且没有任何用户可见变化。

结论:批准。被审查提交上的 CI 已尘埃落定,无需延迟批准标记。

Qwen Code · qwen3.8-max

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — 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.

Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 1090, 18137, 1463, 475, 2861, 444 passed.

中文说明

已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 1090, 18137, 1463, 475, 2861, 444 passed

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

Comment on lines +7448 to +7450
// The retry log must record that non-content chunks (the
// thinking) had already flowed — the diagnostic that makes
// thinking-phase replays visible in the debug log.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The stale replay-safety rationale this PR removes from geminiChat.ts survives in one more place: the header comment of this very test (lines 7382-7386) still says "Thought parts are ephemeral (never recorded as the assistant's response in history)", directly contradicting the corrected source comment ("the successful attempt's thoughts are recorded there" — verified against the success-path history.push, which includes thoughtContentPart at ~geminiChat.ts:4902). — Concrete cost: a future maintainer tracing why thinking-phase replays are safe reads this test (the canonical entry point, now that it pins the diagnostic) and adopts the falsified invariant this PR exists to eliminate; a change reasoned from it (e.g. reworking popPendingPartialAssistantTurn or pruning thoughts from history) could silently break the real safety mechanism — wholesale discard of the failed attempt.

Update the header comment at lines 7384-7386 to the corrected rationale, e.g.:

// Replaying after thinking-only output is safe because the failed
// attempt's partial turn is popped wholesale before the retry
// (`popPendingPartialAssistantTurn`), and thought parts are never
// user-visible answer content, so the replay cannot duplicate
// anything the caller saw.
中文说明

[建议] 本 PR 从 geminiChat.ts 中删除的过时重放安全性论证还有一处残留:本测试的头部注释(第 7382-7386 行)仍写着 "Thought parts are ephemeral (never recorded as the assistant's response in history)",与修正后的源码注释直接矛盾("the successful attempt's thoughts are recorded there"——已对照成功路径的 history.push 验证,其中包含 thoughtContentPart,约 geminiChat.ts:4902)。—— 具体代价:未来维护者追溯 thinking 阶段重放为何安全时会读到这个测试(它现在钉住了诊断字段,是天然的入口),从而接受本 PR 要消除的被证伪的不变量;基于它做出的修改(例如重构 popPendingPartialAssistantTurn 或从 history 中裁剪 thought)可能悄悄破坏真正的安全机制——整体丢弃失败 attempt 的累积内容。

建议将第 7384-7386 行的头部注释更新为修正后的论证,示例见上方英文代码块。

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

Comment on lines +7552 to +7556
const transportError = Object.assign(new TypeError('terminated'), {
cause: Object.assign(new Error('other side closed'), {
code: 'UND_ERR_SOCKET',
}),
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This hand-rolled socket-cut fixture is character-for-character socketCut() from the sibling describe('transport stream continuation (#7832)') block (~line 7664), and the yield-two-chunks-then-throw generator is exactly cutAfter([...]) (~line 7686). — Concrete cost: if the error shape the retry gate classifies as transport changes (the lists live in stream-transport-retry.ts / retryErrorClassification.ts), a maintainer fixing the shared helper leaves this inline copy stale — it would stop classifying as transport, silently flipping this test from exercising the skipped_after_content attribution it was written to pin to a plain error-propagation path, or failing with a misleading diagnostic.

Hoist socketCut/cutAfter out of the nested describe (or move this test into it), then build the mock as:

vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(
  cutAfter([thoughtChunkLiteral, functionCallChunkLiteral]),
);

(Context: seven pre-existing inline copies of this fixture already exist in the outer describe, so hand-rolling is its established idiom — this is the newest copy, ~110 lines from the helpers.)

中文说明

[建议] 这里手写的 socket 切断错误 fixture 与相邻的 describe('transport stream continuation (#7832)') 块(约第 7664 行)中的 socketCut() 逐字符相同,「产出两个 chunk 后抛错」的生成器也恰好是 cutAfter([...])(约第 7686 行)。—— 具体代价:如果重试门控归类为 transport 的错误形状发生变化(清单位于 stream-transport-retry.ts / retryErrorClassification.ts),维护者修复共享 helper 时会让这份内联副本过时——它将不再被归类为 transport,使本测试从覆盖它所要钉住的 skipped_after_content 归因,悄悄变成覆盖普通的错误传播路径,或以误导性的诊断失败。

建议将 socketCut/cutAfter 提升出嵌套 describe(或把本测试移进去),然后按上方英文代码块构造 mock。

(背景:外层 describe 中已有 7 处相同的内联 fixture 副本,手写是该处的既有惯例——本 PR 增加的是最新的一份,且距离 helper 仅约 110 行。)

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

Comment on lines +7451 to +7455
expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
'Transport stream retry scheduled',
expect.objectContaining({
retryDecision: 'retry',
yieldedNonContentChunks: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This pins yieldedNonContentChunks: true, but the false side of the field is asserted nowhere in the repo — the existing zero-chunk retry fixtures (e.g. 'retries a transport stream error after yielding only tool preparation metadata', ~line 7607, whose empty-parts prep chunk fails hasCandidateOutput so streamYieldedChunk stays false) log yieldedNonContentChunks: false without asserting it. — Concrete cost: the mutant hardcoding yieldedNonContentChunks: true at geminiChat.ts:2880 ships green, reporting thinking-phase chunks on every chunkless retry in the debug log — erasing exactly the thinking-phase-replay visibility this assertion's comment says it pins. (Upstream corroboration: the maintainer's M6 mutant survived PR 7938's suite for this same reason.)

Add to the tool-preparation-metadata test, which already exercises the false path:

expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
  'Transport stream retry scheduled',
  expect.objectContaining({
    retryDecision: 'retry',
    yieldedNonContentChunks: false,
  }),
);
中文说明

[建议] 此处钉住了 yieldedNonContentChunks: true,但该字段的 false 一侧在整个仓库中没有任何断言——已有的零 chunk 重试 fixture(例如 'retries a transport stream error after yielding only tool preparation metadata',约第 7607 行,其空 parts 的准备 chunk 无法通过 hasCandidateOutput,因此 streamYieldedChunk 保持 false)会记录 yieldedNonContentChunks: false,却没有断言。—— 具体代价:在 geminiChat.ts:2880 把 yieldedNonContentChunks 硬编码为 true 的变异体可以在全套件绿灯下通过,使每次无 chunk 的重试都在 debug 日志中报告 thinking 阶段 chunk——恰好抹掉本断言注释声称要钉住的 thinking 阶段重放可见性。(上游佐证:维护者的 M6 变异体正是因此在 PR 7938 的套件中幸存。)

建议在已经覆盖 false 路径的 tool-preparation-metadata 测试中补充上方英文代码块中的断言。

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

Comment on lines +7600 to +7604
expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
'Transport stream retry not taken',
expect.objectContaining({
retryDecision: 'skipped_after_content',
}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This pins the 'skipped_after_content' arm of the retryDecision ternary (geminiChat.ts:2961-2962), but the sibling 'exhausted' arm is asserted nowhere in the repo — the budget-exhaustion test (~line 7244) reaches 'Transport stream retry not taken' with streamYieldedContentChunk === false and never asserts the value it gets. — Concrete cost: the mutant collapsing the ternary to always 'skipped_after_content' ships green, misattributing a budget-exhausted "gave up" as "unsafe to recover" in the debug log — silently deleting the very distinction this test's own comment names ("separates 'unsafe to recover' from 'gave up'"). (Upstream corroboration: the M7 mutant survived PR 7938's suite for this same reason.)

Add to the budget-exhaustion test:

expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
  'Transport stream retry not taken',
  expect.objectContaining({ retryDecision: 'exhausted' }),
);
中文说明

[建议] 此处钉住了 retryDecision 三元表达式(geminiChat.ts:2961-2962)的 'skipped_after_content' 分支,但兄弟分支 'exhausted' 在整个仓库中没有任何断言——预算耗尽测试(约第 7244 行)会在 streamYieldedContentChunk === false 时到达 'Transport stream retry not taken',却从未断言它得到的值。—— 具体代价:把三元表达式坍缩为恒返回 'skipped_after_content' 的变异体可以在全套件绿灯下通过,使预算耗尽的「放弃重试」在 debug 日志中被误报为「不安全而无法恢复」——悄悄删除本测试注释自己点名的区分("separates 'unsafe to recover' from 'gave up'")。(上游佐证:M7 变异体正是因此在 PR 7938 的套件中幸存。)

建议在预算耗尽测试中补充上方英文代码块中的断言。

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

@qwen-code-dev-bot

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

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (1080000ms)) (attempt 1/100) — it will retry on the next scan.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:
Qwen failed during address-review: timeout (1080000ms).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31379619548


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (idle-timeout (no output for 1200000ms — the sandbox likely hung at startup)) (attempt 2/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31383566918


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

Address the inline review on the follow-up:

- Correct the last surviving copy of the stale replay-safety rationale
  (the thinking-phase test's header comment still claimed thoughts never
  enter history).
- Hoist socketCut/cutAfter out of the continuation suite and reuse them
  in the function-call cut test instead of a character-for-character
  inline copy, so the transport error shape has a single producer.
- Pin the false side of yieldedNonContentChunks in the tool-preparation
  retry test, and the 'exhausted' arm of retryDecision in the budget
  exhaustion test. Hardcoding the diagnostic true or collapsing the
  ternary now each fail exactly one test.
@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

All four inline suggestions addressed in 0f95b42 — each one was a real gap:

  1. Stale rationale in the test header — corrected; that comment was the last surviving copy of the falsified invariant this PR exists to remove, and the worst-placed one (the canonical entry point for anyone tracing replay safety).
  2. Duplicated socket-cut fixturesocketCut/cutAfter hoisted out of the continuation suite and reused in the function-call cut test; the transport error shape now has a single producer.
  3. yieldedNonContentChunks: false unpinned — asserted in the tool-preparation retry test. Mutation check: hardcoding the field to true now fails exactly that test (1 failed / 300 passed).
  4. 'exhausted' arm unpinned — asserted in the budget-exhaustion test. Collapsing the ternary to always 'skipped_after_content' now fails exactly that test (1 failed / 300 passed).

301/301 green, lint/prettier clean. The point about both sides of a diagnostic needing a pin is well taken — it mirrors exactly how the original M6/M7 mutants survived #7938's suite, one level up.

中文

四条 inline 建议已全部在 0f95b42 落地——每条都是真实缺口:

  1. 测试头注释里的过时论证——已修正;那是本 PR 要消除的错误不变量的最后一处残留,而且位置最糟(任何人追溯重放安全性都会先读这个测试)。
  2. socket-cut fixture 重复——socketCut/cutAfter 已提升出 continuation 套件并在 function-call 切断测试中复用;传输错误形状现在只有单一产地。
  3. yieldedNonContentChunks: false 侧未钉住——已在 tool-preparation 重试测试中断言。变异检查:字段硬编码为 true 现在恰好导致该测试失败(1 失败 / 300 通过)。
  4. 'exhausted' 臂未钉住——已在预算耗尽测试中断言。三元恒取 'skipped_after_content' 现在恰好导致该测试失败(1 失败 / 300 通过)。

301/301 全绿,lint/prettier 干净。「诊断字段两侧都要钉住」这点很受用——这正是当初 M6/M7 变异体在 #7938 套件中幸存的同款成因,高了一层。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions (pushed rounds in between included; this round itself may have failed differently). That is 3 full agent runs that pushed nothing. 2 of those were silent-sandbox (idle) timeouts that no budget increase can cure — investigate the sandbox image and runner docker daemon for those. A human should split or reduce the PR (or raise the agent time budget AND its step backstop together), then comment @qwen-code /retry to re-arm. Until then future scans will skip this PR.

What I found before stopping:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31389561905


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

@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

CI note

The Test (ubuntu-latest, Node 22.x) failure on 0f95b42e is an infra flake, not a regression from this PR — re-run triggered.

  • All 4 failures are in src/services/backgroundShellRegistry.test.tsterminal-entry retention cap, every one a 15s Test timed out, alongside a vitest worker RPC timeout ([vitest-worker]: Timeout calling "onTaskUpdate") — the whole-describe-times-out-together + worker-RPC-timeout signature of a hung runner worker, not an assertion failure.
  • This PR's diff touches only geminiChat.test.ts (test additions) and two comments in geminiChat.ts; it does not touch the registry or its tests.
  • Locally on this branch the same file is green: vitest run src/services/backgroundShellRegistry.test.ts → 57/57. The rest of the CI matrix on this head is green (the same suite passed on e52eebcb before the test-only push).
中文说明

0f95b42e 上的 Test (ubuntu-latest, Node 22.x) 失败是基础设施抖动,非本 PR 回归——已触发重跑。

  • 4 个失败全部位于 src/services/backgroundShellRegistry.test.tsterminal-entry retention cap,每个都是 15s 超时,并伴随 vitest worker RPC 超时(Timeout calling "onTaskUpdate")——整个 describe 一起超时 + worker RPC 超时是 runner worker 挂起的典型指纹,不是断言失败。
  • 本 PR diff 只动 geminiChat.test.ts(测试新增)与 geminiChat.ts 两处注释,与 registry 及其测试零交集。
  • 本分支本地同文件全绿:vitest run src/services/backgroundShellRegistry.test.ts → 57/57。该 head 其余 CI 矩阵全绿(test-only 推送前 e52eebcb 上同套件亦通过)。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline. Not explored to full depth (tool budget reached): This PR is a follow-up to PR #7938 (issue #7832): it corr...: full-package npm run typecheck /lint was not run (vitest transform of the changed files passed cleanly; changes are comments + test expectations + const fixtur…; This PR is a follow-up to PR #7938 (issue #7832): it corr...: did not fetch the PR #8861 round-1 conversation (gh/PR thread) to confirm whether the "never user-visible content" phrasing was already discussed — relevant onl…. Test Plan (not a blocker): src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 19230, 1090, 18140, 1463, 475, 2861, 444 passed.

中文说明

已审查。 建议见行内评论。 未探索到全部深度(达到工具调用预算):This PR is a follow-up to PR #7938 (issue #7832): it corr...:full-package npm run typecheck /lint was not run (vitest transform of the changed files passed cleanly; changes are comments + test expectations + const fixtur…;This PR is a follow-up to PR #7938 (issue #7832): it corr...:did not fetch the PR #8861 round-1 conversation (gh/PR thread) to confirm whether the "never user-visible content" phrasing was already discussed — relevant onl…。 Test Plan(非阻断):src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 19230, 1090, 18140, 1463, 475, 2861, 444 passed

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

Comment on lines +7556 to +7559
// Shared transport-cut fixtures — used by the function-call cut test
// below and by the 'transport stream continuation' suite. A single
// producer for the error shape the transport retry gate classifies,
// so a change to the retryable-code handling has one place to update.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The hoisted fixture's comment claims "a single producer … one place to update", but seven sibling tests in this same describe still inline-construct the byte-identical error shape (TypeError('terminated') + cause { code: 'UND_ERR_SOCKET' }) at lines ~7189, 7247, 7320, 7400, 7488, 7634, 8792 — three of them edited by this very PR. — Failure scenario: if the classified transport error shape changes (the lists live in stream-transport-retry.ts / retryErrorClassification.ts), a maintainer following this comment updates socketCut() only; the seven inline copies keep exercising the old shape — the tests pass while no longer verifying the production classification. Suggested fix: route the seven inline transportError constructions through socketCut() (and move the declaration above its first use — six of the seven call sites textually precede it), or reword the comment to the true scope: "so those consumers have one place to update".

中文说明

[建议] 提升后的 fixture 注释声称「单一产地……只需更新一处」,但同一 describe 中另有七个兄弟测试仍在逐字符地内联构造相同的错误形状(TypeError('terminated') + cause { code: 'UND_ERR_SOCKET' }),位于约第 7189、7247、7320、7400、7488、7634、8792 行——其中三个正是本 PR 修改的测试。—— 失败场景:如果重试门控归类的传输错误形状发生变化(清单位于 stream-transport-retry.ts / retryErrorClassification.ts),维护者依照此注释只更新 socketCut();七份内联副本会继续演练旧形状——测试仍然通过,却不再验证生产环境的归类逻辑。建议修复:将七处内联的 transportError 构造改为调用 socketCut()(并把声明移到首个调用点之前——七个调用点中有六个在文本上位于声明之前),或将注释改写为真实范围:"so those consumers have one place to update"。

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

Comment on lines +7302 to +7305
expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
'Transport stream retry not taken',
expect.objectContaining({
retryDecision: 'exhausted',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This pin leaves a one-line mutant alive: substituting streamYieldedChunk for streamYieldedContentChunk in the retryDecision ternary (geminiChat.ts:~2961) survives the whole suite, because the two not-taken tests sit exactly on the sides where both flags agree (zero chunks ⇒ both false; thought + functionCall ⇒ both true). — Failure scenario: a thinking-only stream that exhausts the replay budget (three consecutive socket cuts mid-thinking — the dominant #7832 shape) reaches the not-taken branch with the flags diverged; if the ternary's flag ever regresses, the log reports retryDecision: 'skipped_after_content' for a give-up — the exact misattribution the comment above warns against — and the suite stays green. Probe-verified at HEAD: the mutant passes 301/301; the test sketched below fails under the mutant and passes on the correct code. (Distinct from the round-1 'exhausted' pin, which killed only the constant-collapse mutant.) Suggested fix — add a budget-exhaustion variant that yields a thinking chunk before each cut:

it('attributes budget exhaustion correctly when thinking chunks flowed', async () => {
  vi.useFakeTimers();
  try {
    vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(
      cutAfter([
        {
          candidates: [
            { content: { parts: [{ text: 'hmm', thought: true }] } },
          ],
        },
      ] as unknown as GenerateContentResponse[]),
    );
    // drain the stream (it rejects after the third cut), advancing fake
    // timers past both retry delays: 3 generateContentStream calls,
    // 2 RETRY events, then the not-taken log —
    expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
      'Transport stream retry not taken',
      expect.objectContaining({ retryDecision: 'exhausted' }),
    );
  } finally {
    vi.useRealTimers();
  }
});
中文说明

[建议] 此断言仍留有一个单行变异体存活:把 retryDecision 三元表达式(geminiChat.ts:~2961)中的 streamYieldedContentChunk 替换为 streamYieldedChunk 可以通过整个套件,因为两个 not-taken 测试恰好都落在两个标志取值一致的侧(零 chunk ⇒ 两者皆 false;thought + functionCall ⇒ 两者皆 true)。—— 失败场景:仅产出 thinking 的流耗尽重试预算(thinking 阶段连续三次 socket 切断——#7832 的主要形态)时会以两标志分歧的状态到达 not-taken 分支;若三元表达式引用的标志将来发生回归,日志会把「放弃重试」误报为 retryDecision: 'skipped_after_content'——正是上方注释警告的误归因——而全套件保持绿灯。已在 HEAD 上用探针验证:变异体 301/301 通过;下方草图测试在变异体下失败、在正确代码下通过。(与第一轮钉住的 'exhausted' 不同——那一断言只杀死了常量坍缩变异体。)建议修复——新增一个在每次切断前先产出 thinking chunk 的预算耗尽变体测试(代码见上方英文代码块)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

⏸️ Takeover paused: this PR reached its round cap (100/100). Comment @qwen-code /takeover to re-arm a fresh window and continue management, or @qwen-code /takeover stop to release.

中文说明

⏸️ 托管已暂停:本 PR 达到轮次上限(100/100)。评论 @qwen-code /takeover 可重新武装、开启新窗口继续托管;或评论 @qwen-code /takeover stop 释放。

@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔄 Takeover re-armed: the round counter starts a fresh window (previous rounds no longer count toward the cap); management continues.

中文说明

🔄 已重新武装:轮次计数开启新窗口(此前轮次不再计入上限),托管继续。

@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

This review round raced the push: its four inline suggestions were generated against the previous head e52eebcb, and all four are already addressed in 0f95b42e (pushed before the review posted, see the response above):

  1. Stale rationale in the test header → corrected (no copy of the retired invariant remains in the diff's scope).
  2. Hand-rolled socket-cut fixture → socketCut/cutAfter hoisted out of the continuation suite and reused.
  3. yieldedNonContentChunks: false → pinned in the tool-preparation retry test (mutant: 1 failed / 300 passed).
  4. retryDecision: 'exhausted' → pinned in the budget-exhaustion test (mutant: 1 failed / 300 passed).
中文

本轮 review 与推送发生了竞速:四条 inline 建议是基于旧 head e52eebcb 生成的,全部已在 0f95b42e 落地(推送早于本轮 review 发布,见上方回复):测试头注释已修正、fixture 已提升复用、两个诊断字段两侧均已钉住(变异各恰 1 杀)。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (idle-timeout (no output for 1200000ms — the sandbox likely hung at startup)) (attempt 1/100) — it will retry on the next scan.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31399630023


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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): PR #8861 (QwenLM/qwen-code), a follow-up to #7938 (issue ...: none — all planned checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — the walk completed within budget..

Test Plan (not a blocker): src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 1200, 18901, 1502, 481, 3069, 473 passed.

中文说明

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

未探索到全部深度(达到工具调用预算):PR #8861 (QwenLM/qwen-code), a follow-up to #7938 (issue ...:none — all planned checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — the walk completed within budget.

Test Plan(非阻断):src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 1200, 18901, 1502, 481, 3069, 473 passed

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

Comment thread packages/core/src/core/geminiChat.ts Outdated
Comment on lines +154 to +156
* retry (`popPendingPartialAssistantTurn`), and thought parts are never
* user-visible content — so nothing the caller saw from the failed
* attempt can appear twice. The transport stream retry gate relies on

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The corrected rationale introduces a new empirically false clause: "thought parts are never user-visible content". Thought parts from a failed attempt DO reach user-visible surfaces before the retry gate runs: chunks are yielded to the caller first (geminiChat.ts ~2819); turn.ts converts them to GeminiEventType.Thought events (turn.ts:605-612); the TUI renders them as a live thinking indicator and commits them as a collapsible gemini_thought history item (useGeminiStream.ts ~742-744, ~1853-1866); non-interactive JSON output appends them (BaseJsonOutputAdapter.appendThinking); session resume re-renders persisted thoughts (resumeHistoryUtils.ts:465). The maintainer's own #7938 verification table lists attempt-1 thoughts under "streamed to caller", and this file's own usage of the phrase includes thoughts (the ~3829-3835 fallback skip keys on streamYieldedAnyChunk, which thought-only chunks set). — Failure scenario: this PR exists to remove a false invariant "so the next reader doesn't rely on it"; the replacement premise plants a new one. A maintainer generalizing the replay gate could admit other chunk classes judged "invisible", or notice the contradiction (a user reporting "I saw the interrupted attempt's thinking, then a second thinking block after the retry") and distrust the whole rationale — re-creating the exact failure mode this PR removes. The safety conclusion holds independently (wholesale discard via popPendingPartialAssistantTurn), so this is not a blocker. The same clause echoes in the gate comment at ~2933-2937 and in the thinking-test header (geminiChat.test.ts ~7624-7628).

Suggested change
* retry (`popPendingPartialAssistantTurn`), and thought parts are never
* user-visible content so nothing the caller saw from the failed
* attempt can appear twice. The transport stream retry gate relies on
* retry (`popPendingPartialAssistantTurn`), so nothing the caller saw
* from the failed attempt can appear twice. The transport stream retry
* gate relies on
中文说明

修正后的论证引入了一个新的、与事实不符的子句:「thought part 永远不是用户可见内容」。失败 attempt 的 thought part 在重试门控运行之前确实会到达用户可见的界面:chunk 会先 yield 给调用方(geminiChat.ts ~2819);turn.ts 将其转换为 GeminiEventType.Thought 事件(turn.ts:605-612);TUI 将其渲染为实时思考指示器,并作为可折叠的 gemini_thought 历史项提交(useGeminiStream.ts ~742-744、~1853-1866);非交互 JSON 输出通过 BaseJsonOutputAdapter.appendThinking 追加;会话恢复时会重新渲染持久化的 thought(resumeHistoryUtils.ts:465)。维护者在 #7938 的验证表格本身就把 attempt 1 的 thought 列在 "streamed to caller" 之下;本文件对该短语的用法也包含 thought(~3829-3835 处跳过回退链的依据是 streamYieldedAnyChunk,纯 thought chunk 同样会置位该标志)。失败场景:本 PR 的目的正是移除错误不变量以免「后续读者依赖它」,而替换后的前提又植入了一个新的错误不变量。后续维护者在泛化重放门控时可能据此放行其他被判定为「不可见」的 chunk 类别,或者发现矛盾(例如用户报告「看到了被打断 attempt 的思考,重试后又出现第二个思考块」)后不再信任整段论证——重新制造出本 PR 要消除的失败模式。安全结论本身独立成立(popPendingPartialAssistantTurn 整体丢弃),因此这不是阻断项。同一子句在 ~2933-2937 的门控注释与思考测试头部注释(geminiChat.test.ts ~7624-7628)中重复出现。

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

Comment on lines +7533 to +7538
expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
'Transport stream retry not taken',
expect.objectContaining({
retryDecision: 'exhausted',
}),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-2: This pin leaves a one-line mutant alive: substituting streamYieldedChunk for streamYieldedContentChunk in the retryDecision ternary (geminiChat.ts:3052) survives the whole suite — measured under the mutant: 318/318 pass. The only two not-taken assertions set both flags equal (this test: false/false; the function-call cut test ~7855: true/true), so neither discriminates the ternary's condition variable. — Failure scenario: a probe test that exhausts the retry budget while each attempt yields one thought-only chunk before the cut flips the mutant: it passes on unmutated code and fails under the mutant with retryDecision: 'skipped_after_content' where 'exhausted' is expected — the exact misattribution ("gave up" recorded as "unsafe to recover") these pins exist to prevent. Until such a test exists, a refactor of the flag computation or the ternary condition ships a mislabeled diagnostic past CI. Suggested fix: add that budget-exhaustion-after-thinking test asserting retryDecision: 'exhausted' (the verifier's probe confirmed it kills the mutant).

中文说明

R2-2:该 pin 留有一个单行变异体存活:将 retryDecision 三元式(geminiChat.ts:3052)中的 streamYieldedContentChunk 替换为 streamYieldedChunk 后,整套测试仍然通过——变异体下实测 318/318 通过。仅有的两处 not-taken 断言中两个标志都相等(本测试:false/false;function-call 切断测试 ~7855:true/true),都无法区分三元式的条件变量。失败场景:一个「每次切断前 yield 一个纯 thought chunk 直至预算耗尽」的探针测试可使变异体翻转——在未变异代码上通过,在变异体下失败:期望 'exhausted' 却得到 'skipped_after_content'(把「放弃」误标为「不可恢复」),正是这些 pin 要防止的误标。在补充该测试之前,对标志计算或三元式条件的重构会让被误标的诊断字段绕过 CI。建议修复:补充该 thinking 后预算耗尽测试并断言 retryDecision: 'exhausted'(验证代理的探针已确认该测试可杀死变异体)。

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

@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 轮)。改动内容与我反驳保留之处如下:

Round summary — PR #8861 (review rounds 1–2 feedback)

Verification-gate rejection from the previous round (fixed first)

The previous round was rejected because npm run build failed in
integrations/external-context with
src/http-client.ts(117,29): error TS2504: Type 'ReadableStream<Uint8Array<ArrayBufferLike>>' must have a '[Symbol.asyncIterator]()' method....
The gate measured this failure as pre-existing on the branch head (also
failing without that round's commit) and noted a base update as the usual
cure. The base has since been auto-updated: merge b50b05c1 brought main's
upstream fix into this branch — readBoundedBody now uses
response.body.getReader() instead of async-iterating the ReadableStream,
so the build no longer depends on which lib set resolves
[Symbol.asyncIterator]. This round verified the cure: npm run build
passes at the current head (the @qwen-code/external-context workspace
builds cleanly). No code change was needed for the rejection itself.

Round-2 inline findings

  1. [Suggestion] "single producer" fixture comment was inaccurate (rc:3750330518) — Implemented.
    Rather than rewording the comment, the claim is now true: all seven
    byte-identical inline constructions of the retryable socket-cut error
    shape (TypeError('terminated') + UND_ERR_SOCKET cause) now call the
    shared socketCut(), and the socketCut/cutAfter fixture block was
    moved above its first consumer. The file now contains exactly one literal
    of the classified shape (inside socketCut), consumed by eight call
    sites (seven tests + cutAfter). The comment was also rewritten to the
    true scope, noting that the non-retryable shapes (ECONNRESET + 4xx,
    ECONNREFUSED, the parameterized-code test) stay inline on purpose
    because they test different classifications. Net: −54/+94 lines for the
    whole commit, most of the additions being the new test below.

  2. [Suggestion] diverged-flags mutant survives the suite (rc:3750330527) — Implemented.
    Added 'attributes budget exhaustion correctly when thinking chunks flowed':
    every attempt yields a thought chunk before the socket cut, so the
    per-attempt flags diverge (streamYieldedChunk true,
    streamYieldedContentChunk false) when the replay budget exhausts — the
    dominant YOLO mode: mid-stream socket close is not retried, making large code generation impossible #7832 shape. The not-taken log is asserted to carry
    retryDecision: 'exhausted', pinning the ternary to the content flag.
    Mutation probe at this head: temporarily substituting
    streamYieldedChunk for streamYieldedContentChunk in the
    retryDecision ternary makes exactly this test fail (1 failed | 318
    skipped); reverting makes it pass. The probe mutant was reverted; the
    committed diff touches only the test file.

Round-1 inline findings (re-verified)

All four were already addressed in 0f95b42e (that push raced the review);
each was re-verified to still hold on the current head:

  • rc:3748662491 — stale replay-safety rationale in the test header: corrected comment present.
  • rc:3748662499 — duplicated socket-cut fixture: socketCut/cutAfter hoisted and reused (further unified this round).
  • rc:3748662502 — yieldedNonContentChunks: false unpinned: assertion present in the tool-preparation retry test.
  • rc:3748662513 — retryDecision: 'exhausted' unpinned: assertion present in the budget-exhaustion test.

Conflicts

None (--conflict false); no merge performed this round — the base update
was already on the branch when this round started.

Verification

Commands actually run this round (repo root unless noted):

  • npm run buildpassed (exit 0; @qwen-code/external-context, which failed the previous round, now builds cleanly)
  • npm run typecheckpassed (exit 0)
  • npm run lintpassed (exit 0; eslint . + eslint integration-tests)
  • npx prettier --check packages/core/src/core/geminiChat.test.tspassed
  • cd packages/core && npx vitest run src/core/geminiChat.test.ts319 passed (319)
  • Mutation probe (temporary, reverted): with the streamYieldedChunk-for-streamYieldedContentChunk ternary mutant, the new test fails (1 failed | 318 skipped); after git checkout -- packages/core/src/core/geminiChat.ts, it passes (1 passed | 318 skipped)
  • Integration tests after npm run bundlenot run: the change is test-only inside packages/core, and the covered behavior is exercised by these unit tests, not only through the bundled CLI or integration harness
  • npm run generate:settings-schemanot needed: no settings source changed
中文说明

本轮总结 — PR #8861(review 第 1–2 轮反馈)

上一轮验证门禁的拒绝(优先修复)

上一轮被拒绝的原因是 npm run buildintegrations/external-context 中失败:src/http-client.ts(117,29): error TS2504: Type 'ReadableStream<Uint8Array<ArrayBufferLike>>' must have a '[Symbol.asyncIterator]()' method...。门禁测量确认该失败在分支 head 上预先存在(不带该轮提交也同样失败),并指出基线更新(合并 main)是常规解法。基线此后已自动更新:合并提交 b50b05c1 将 main 上的上游修复带入本分支——readBoundedBody 改用 response.body.getReader(),不再对 ReadableStream 做 async 迭代,构建因此不再依赖 [Symbol.asyncIterator] 由哪个 lib 集合解析。本轮已验证该解法生效:当前 head 上 npm run build 通过(@qwen-code/external-context 工作区构建干净)。针对拒绝本身无需任何代码改动。

第 2 轮 inline 发现

  1. [建议] "single producer" fixture 注释名不副实(rc:3750330518)— 已实现。
    没有改写注释,而是让该说法变为事实:七处逐字符相同的可重试 socket 切断错误形状(TypeError('terminated') + UND_ERR_SOCKET cause)内联构造全部改为调用共享的 socketCut(),且 socketCut/cutAfter fixture 块被移到首个使用者之前。文件现在只有一处该归类形状的字面量(在 socketCut 内部),由八个调用点消费(七个测试 + cutAfter)。注释同时改写为真实范围,并说明不可重试的形状(ECONNRESET + 4xx、ECONNREFUSED、参数化 code 的测试)有意保持内联,因为它们测的是不同的归类。整个提交净变化 −54/+94 行,其中增加的部分主要是下面的新测试。

  2. [建议] 两标志分歧的变异体在全套件中幸存(rc:3750330527)— 已实现。
    新增 'attributes budget exhaustion correctly when thinking chunks flowed':每次 attempt 在 socket 切断前先产出一个 thought chunk,使重试预算耗尽时两次 attempt 级标志分歧(streamYieldedChunk 为 true、streamYieldedContentChunk 为 false)——这正是 YOLO mode: mid-stream socket close is not retried, making large code generation impossible #7832 的主要形态。not-taken 日志被断言携带 retryDecision: 'exhausted',把三元表达式钉在 content 标志上。本 head 上的变异探针:临时把三元中的 streamYieldedContentChunk 替换为 streamYieldedChunk,恰好使该测试失败(1 失败 | 318 跳过);还原后通过。探针变异已还原;提交的 diff 只动了测试文件。

第 1 轮 inline 发现(复查确认)

四条均已在 0f95b42e 落地(该推送与 review 发生了竞速);本轮逐条复查,确认在当前 head 上仍然成立:

  • rc:3748662491 — 测试头注释中的过时重放安全性论证:修正后的注释在位。
  • rc:3748662499 — 重复的 socket-cut fixture:socketCut/cutAfter 已提升并复用(本轮进一步统一)。
  • rc:3748662502 — yieldedNonContentChunks: false 未钉住:tool-preparation 重试测试中的断言在位。
  • rc:3748662513 — retryDecision: 'exhausted' 未钉住:预算耗尽测试中的断言在位。

冲突

无(--conflict false);本轮未做任何合并——基线更新在本轮开始前已在分支上。

验证

本轮实际执行的命令(未注明者均在仓库根目录):

  • npm run build通过(exit 0;上一轮失败的 @qwen-code/external-context 现在构建干净)
  • npm run typecheck通过(exit 0)
  • npm run lint通过(exit 0;eslint . + eslint integration-tests
  • npx prettier --check packages/core/src/core/geminiChat.test.ts通过
  • cd packages/core && npx vitest run src/core/geminiChat.test.ts319 通过(319)
  • 变异探针(临时、已还原):在 streamYieldedChunk 替换 streamYieldedContentChunk 的三元变异下,新测试失败(1 失败 | 318 跳过);git checkout -- packages/core/src/core/geminiChat.ts 还原后通过(1 通过 | 318 跳过)
  • npm run bundle 后的集成测试 — 未执行:改动仅为 packages/core 内的测试文件,所覆盖的行为由这些单元测试演练,并非只能通过打包 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-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round summary

Addressed both inline suggestions from the round-3 automated review of PR #8861. One required a comment correction in three places; the other was already resolved in the code and was re-verified empirically this round.

Feedback points and decisions

1. [rc:3751530800] packages/core/src/core/geminiChat.ts:156 — the corrected rationale plants a new empirically false clause ("thought parts are never user-visible content") — Acted (fixed).

Verified the finding's evidence against the code: thought parts DO reach user-visible surfaces before the retry gate runs. Chunks are yielded to callers first (the StreamEventType.CHUNK yield in the attempt loop), turn.ts converts them to GeminiEventType.Thought events, the TUI streams them into gemini_thought history items (useGeminiStream.ts), non-interactive JSON output appends them (BaseJsonOutputAdapter.appendThinking), session resume re-renders persisted thoughts (resumeHistoryUtils.ts:465), and the file itself treats thought-only chunks as user-visible output in the fallback skip (streamYieldedAnyChunk — "already emitted user-visible output").

Removed the false clause from all three places where it echoed, grounding the safety rationale solely in the wholesale partial-turn discard, which holds independently:

  • hasNonThoughtCandidateParts doc comment — applied the suggested replacement verbatim
  • the transport retry gate comment — also renamed "user-visible content" to "content (non-thought output)" to match the gate flag's semantics
  • the thinking-retry test header comment in geminiChat.test.ts — now states the replay is allowed even though thought chunks already reached the caller, with safety coming from the discard

2. [rc:3751530833] packages/core/src/core/geminiChat.test.ts:7549 — a one-line mutant (substituting streamYieldedChunk for streamYieldedContentChunk in the retryDecision ternary) survives — Resolved by an existing test; re-verified empirically this round.

The suggested probe test already exists at HEAD: attributes budget exhaustion correctly when thinking chunks flowed exhausts the retry budget with one thought-only chunk yielded per attempt (the flags diverge: streamYieldedChunk=true, streamYieldedContentChunk=false) and asserts retryDecision: 'exhausted' in the not-taken log. Measured this round: applying the exact one-line mutant at geminiChat.ts:3052 makes that test fail on the retryDecision assertion (the not-taken log would be mislabeled 'skipped_after_content'); reverting the mutant restores a green run. The mutant is dead, so no code change was needed. The finding's "318/318 pass under the mutant" measurement appears to predate this test, which brought the file to 319 tests.

Review-body notes (non-blocking)

The review body's Test Plan observation (description test counts differ from the observed run counts) reflects stale numbers in the PR description from earlier rounds, not a code defect — no action taken.

Conflicts

None (--conflict false; no merge performed).

Verification

All commands actually run this round; the change is comment-only in packages/core/src/core/geminiChat.ts and packages/core/src/core/geminiChat.test.ts, committed as 41ad67c771:

  • npx vitest run src/core/geminiChat.test.ts -t "sendMessageStream with retries" (in packages/core) — 79 passed | 240 skipped (319)
  • npx vitest run src/core/geminiChat.test.ts (full touched file, in packages/core) — 319 passed (319)
  • Mutant check for finding 2: applied the finding's exact one-line mutant at geminiChat.ts:3052, re-ran the retry describe — 1 failed (attributes budget exhaustion correctly when thinking chunks flowed, on the retryDecision: 'exhausted' assertion) | 78 passed; reverted the mutant, re-ran — 79 passed (mutant killed by the existing test)
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed

Integration tests: not run — the change is comment-only and the touched behavior is fully exercised by the unit suite above; nothing here is bundled-CLI-only behavior.

中文说明

本轮总结

处理了 PR #8861 第三轮自动审查中的两条行内建议。其中一条需要在三处修正注释;另一条在代码中已经被解决,本轮通过实验重新验证确认。

反馈点与决定

1. [rc:3751530800] packages/core/src/core/geminiChat.ts:156 —— 修正后的论证植入了一个新的、与事实不符的子句(「thought part 永远不是用户可见内容」)—— 已处理(已修复)。

已对照代码核实该发现的证据:thought part 在重试门控运行之前确实会到达用户可见的界面。chunk 会先 yield 给调用方(attempt 循环中的 StreamEventType.CHUNK yield);turn.ts 将其转换为 GeminiEventType.Thought 事件;TUI 将其流式写入 gemini_thought 历史项(useGeminiStream.ts);非交互 JSON 输出会追加它们(BaseJsonOutputAdapter.appendThinking);会话恢复时会重新渲染持久化的 thought(resumeHistoryUtils.ts:465);本文件自身在回退跳过逻辑中也把纯 thought chunk 视为用户可见输出(streamYieldedAnyChunk —— 「already emitted user-visible output」)。

已从该子句重复出现的三处全部移除,将安全性论证完全建立在独立成立的 partial turn 整体丢弃之上:

  • hasNonThoughtCandidateParts 的文档注释 —— 逐字应用了建议中的替换文本
  • transport 重试门控注释 —— 同时把「user-visible content」改为「content (non-thought output)」,与门控标志的语义保持一致
  • geminiChat.test.ts 中 thinking 重试测试的头部注释 —— 现在明确表述:即使 thought chunk 已经到达调用方,重放仍然必须被允许,安全性来自整体丢弃

2. [rc:3751530833] packages/core/src/core/geminiChat.test.ts:7549 —— 一个单行变异体(将 retryDecision 三元式中的 streamYieldedContentChunk 替换为 streamYieldedChunk)可以存活 —— 已被现有测试解决;本轮通过实验重新验证。

建议中的探针测试在当前 HEAD 上已经存在:attributes budget exhaustion correctly when thinking chunks flowed 会让每次 attempt 在切断前 yield 一个纯 thought chunk 直至重试预算耗尽(两个标志发生分叉:streamYieldedChunk=truestreamYieldedContentChunk=false),并断言 not-taken 日志中 retryDecision: 'exhausted'。本轮实测:在 geminiChat.ts:3052 处应用完全相同的一行变异体后,该测试在 retryDecision 断言上失败(not-taken 日志会被误标为 'skipped_after_content');还原变异体后恢复全绿。该变异体已被杀死,因此无需代码改动。该发现中「变异体下 318/318 通过」的测量应早于这个测试的加入——正是这个测试把该文件的测试数带到了 319。

审查正文备注(非阻断)

审查正文中的 Test Plan 观察(PR 描述中的测试数量与实际运行数量不一致)反映的是 PR 描述中来自早前轮次的过期数字,并非代码缺陷——未采取行动。

冲突

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

验证

本轮实际运行的所有命令;改动为仅注释变更,涉及 packages/core/src/core/geminiChat.tspackages/core/src/core/geminiChat.test.ts,提交为 41ad67c771

  • npx vitest run src/core/geminiChat.test.ts -t "sendMessageStream with retries"(在 packages/core 中)—— 79 通过 | 240 跳过(共 319)
  • npx vitest run src/core/geminiChat.test.ts(完整改动文件,在 packages/core 中)—— 319 通过(共 319)
  • 针对发现 2 的变异体检查:在 geminiChat.ts:3052 应用该发现描述的一行变异体后重跑 retry describe —— 1 失败(attributes budget exhaustion correctly when thinking chunks flowed,失败于 retryDecision: 'exhausted' 断言)| 78 通过;还原变异体后重跑 —— 79 通过(变异体被现有测试杀死)
  • npm run build —— 通过
  • npm run typecheck —— 通过
  • npm run lint —— 通过

集成测试:未运行——本次改动仅为注释,且涉及的行为已由上述单元测试完整覆盖;此处不存在仅能通过打包后 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.

Reviewed — no blockers. Suggestions are inline.

Test Plan (not a blocker): src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 1200, 18897, 1502, 481, 3069, 473 passed.

中文说明

已审查——无阻断问题。 建议见行内评论。

Test Plan(非阻断):src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 1200, 18897, 1502, 481, 3069, 473 passed

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

Comment on lines +7417 to +7420
// Shared transport-cut fixtures. `socketCut` is the single producer of
// the retryable socket-failure shape the transport retry gate
// classifies, so a classified-shape change has one place to update
// (non-retryable shapes are constructed inline on purpose).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The new fixture comment's "single producer … one place to update" claim is false: it.each([...RETRYABLE_STREAM_TRANSPORT_CODES]) (~line 9188) constructs the same classified retryable shape inline, parameterized over all 6 allow-listed codes including UND_ERR_SOCKET. The parenthetical holds only for the two genuinely non-retryable inline shapes (4xx at ~9433, ECONNREFUSED at ~9472). In a PR whose explicit purpose is making rationale comments factually correct, this newly written comment is inaccurate. — Failure scenario: if the classified error shape changes (e.g. undici's nesting changes and the classifier is updated), a maintainer trusting this comment updates only socketCut; the inline construction in the drift guard silently goes stale and its six tests fail confusingly, and the parenthetical actively reassures them that any inline shape must be non-retryable — so the drift gets debugged under a wrong mental model.

Suggested change
// Shared transport-cut fixtures. `socketCut` is the single producer of
// the retryable socket-failure shape the transport retry gate
// classifies, so a classified-shape change has one place to update
// (non-retryable shapes are constructed inline on purpose).
// Shared transport-cut fixtures. `socketCut` is the shared producer of the
// canonical `UND_ERR_SOCKET` retryable shape. The per-code drift-guard test
// (`it.each` over the allow-list) constructs parameterized retryable shapes
// inline on purpose; non-retryable shapes stay inline on purpose too.
中文说明

新增的 fixture 注释声称「socketCut 是可重试形状的唯一生产者……只需改一处」,但这不成立:it.each([...RETRYABLE_STREAM_TRANSPORT_CODES])(约第 9188 行)同样以内联方式构造相同的、可被分类的重试形状,并对包括 UND_ERR_SOCKET 在内的全部 6 个白名单 code 参数化。括号里的说明只对两个真正不可重试的内联形状(4xx 约 9433、ECONNREFUSED 约 9472)成立。本 PR 的目的正是让注释中的论证与事实一致,这条新写的注释却不准确。——失败场景:若被分类的错误形状发生变化(例如 undici 的嵌套结构变化、分类器随之更新),信任此注释的维护者只会更新 socketCut;drift-guard 中的内联构造悄然过时,其六个测试以令人困惑的方式失败,而括号说明还会让人误以为所见内联形状一定是不可重试的——于是漂移在错误的心智模型下被排查。

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

Comment on lines +7587 to +7590
// Same catch-and-assert drain as the zero-chunk exhaustion test:
// the rejection settles only after both retry delays elapse.
let caughtError: unknown;
const collecting = (async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This new test copies the 12-line catch-and-drain block byte-identically from the adjacent zero-chunk exhaustion test (~lines 7516-7531), leaving two identical copies in the file (a third variant lives near line 8751). The block hard-codes the settle protocol — collect events, catch the terminal error, advanceTimersByTimeAsync(0) + advanceTimersByTimeAsync(10_000) to cover both retry delays — and the original copy carries a 5-line comment documenting the fake-timer traps the protocol avoids, evidence it is genuinely delicate. — Concrete cost: any change to that protocol (the retry-delay sum moving off 10_000 ms, or the rejection needing a different timer advance) must be applied to both copies in lockstep; missing one makes that test fail, or pass for the wrong reason, silently.

Fix: extract a helper next to cutAfter and call it from both tests, keeping the timer-advance steps explicit at the call sites:

async function drainCollecting(stream: AsyncIterable<StreamEvent>) {
  const events: StreamEvent[] = [];
  let caughtError: unknown;
  try {
    for await (const event of stream) events.push(event);
  } catch (error) {
    caughtError = error;
  }
  return { events, caughtError };
}
中文说明

新测试逐字节复制了相邻零 chunk 耗尽测试(约 7516-7531 行)的 12 行「捕获并排空」块,文件中因此存在两份完全相同的副本(约 8751 行还有第三个变体)。该块硬编码了排空协议——收集事件、捕获终结错误、advanceTimersByTimeAsync(0) + advanceTimersByTimeAsync(10_000) 以覆盖两次重试延迟——且原副本带有 5 行注释说明该协议规避的假定时器陷阱,足见其精细。——具体代价:对该协议的任何修改(重试延迟总和不再是 10_000 ms,或拒绝需要不同的定时器推进)都必须同步应用到两个副本;漏改其一会让该测试静默失败,或以错误的原因通过。

修复方式:在 cutAfter 旁提取一个辅助函数并在两个测试中调用,同时在调用处保留显式的定时器推进步骤(见上方代码)。

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

Comment on lines +7901 to +7906
expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
'Transport stream retry not taken',
expect.objectContaining({
retryDecision: 'skipped_after_content',
}),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This pin cannot discriminate the ternary's real key: the cut here also delivers a functionCall, so both streamYieldedContentChunk and streamYieldedFunctionCall are true at this site. Measured under mutation: re-keying the not-taken ternary (geminiChat.ts:~3051) to streamYieldedFunctionCall survives all 319 tests; a companion assertion that reaches the not-taken branch with content delivered but no functionCall flips the mutant (observed retryDecision: 'exhausted' where 'skipped_after_content' was expected) and passes on the correct key. — Failure scenario: a future re-keying ships silently; for the dominant #7832 shape (plain answer text cut repeatedly, no functionCall ever delivered) the debug log then reports 'exhausted' instead of 'skipped_after_content' — misattributing "unsafe to recover" as "gave up", the exact misattribution this diagnostic exists to prevent.

Fix: extend 'propagates once the continuation budget is exhausted' — a plain-text cut whose continuation budget exhausts reaches the not-taken branch with streamYieldedContentChunk=true, streamYieldedFunctionCall=false — to also assert retryDecision: 'skipped_after_content'. Note: a first-cut plain-text scenario enters the continuation branch, not the not-taken branch, so it cannot host this pin.

中文说明

这个钉住无法区分三元表达式的真实键:此处的切断同时送达了 functionCall,因此该处 streamYieldedContentChunkstreamYieldedFunctionCall 同为 true。变异实测:把 not-taken 三元(geminiChat.ts:~3051)的键改为 streamYieldedFunctionCall 后,全部 319 个测试依然通过;而一个「已送达内容但没有 functionCall」并走到 not-taken 分支的伴随断言能让该变异体失败(实际观察到 retryDecision: 'exhausted',期望 'skipped_after_content'),且在正确的键下通过。——失败场景:未来一次改键会悄无声息地合入;对 #7832 的主要形态(纯文本答复被反复切断、始终没有 functionCall 送达),debug 日志将报告 'exhausted' 而非 'skipped_after_content'——把「无法安全恢复」误报为「放弃重试」,正是该诊断字段要避免的误归因。

修复方式:扩展 'propagates once the continuation budget is exhausted'——该测试中文本被切断且续传预算耗尽,会以 streamYieldedContentChunk=true, streamYieldedFunctionCall=false 走到 not-taken 分支——为其补充 retryDecision: 'skipped_after_content' 断言。注意:首次切断的纯文本场景走的是续传分支而非 not-taken 分支,不能承载该钉住。

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

Comment thread packages/core/src/core/geminiChat.ts Outdated
Comment on lines +152 to +154
* attempt's thoughts are recorded there. It is that the failed
* attempt's accumulated partial turn is discarded wholesale before the
* retry (`popPendingPartialAssistantTurn`), so nothing the caller saw

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The corrected rationale attributes thinking-only replay safety to popPendingPartialAssistantTurn, but on this path the pop is a provable no-op: a mid-stream partial turn is persisted on error only when willPersistToHistory holds, which for streamError !== null reduces to hasToolCall && … (~line 4894; the in-code comment at ~5003 states "Plain-text partial turns (no functionCall yielded) are deliberately NOT persisted"). A yielded functionCall is a non-thought part, so it sets streamYieldedContentChunk, which closes this very gate — therefore whenever the replay branch executes, pendingPartialAssistantTurnIndex is null and the pop early-returns (~1806). The load-bearing mechanism is the non-persistence policy; the pop is defense in depth. The code is correct — but rationale accuracy is exactly the defect class this PR exists to fix, and the gate comment at ~2932 repeats the same misattribution. — Failure scenario: (a) a maintainer pruning retry cleanup sees the pop, observes thinking-only failures never push to history, and removes the "dead" pop — behavior-neutral today, silently wrong the day persistence widens to thought-only partials (JSONL parity is a documented concern at ~4886); (b) someone debugging duplicated output reads this comment and stops at the pop, never reaching the persistence gate that actually does the work.

Fix (both comment sites): restate as the non-persistence policy with the pop as belt-and-braces, e.g.

 * attempt's thoughts are recorded there. It is that a failed attempt that
 * produced only thought parts persists nothing: a mid-stream partial turn is
 * recorded on error only when a functionCall was already delivered, which the
 * replay gate excludes. `popPendingPartialAssistantTurn()` before the retry is
 * defense in depth — it has nothing to pop on this path today, but keeps the
 * replay safe if that persistence policy ever widens.
中文说明

修正后的论证把 thinking-only 重放的安全性归因于 popPendingPartialAssistantTurn,但在该路径上这个 pop 是可证明的空操作:流中错误时只有 willPersistToHistory 成立才会持久化 partial turn,而 streamError !== null 时它退化为 hasToolCall && …(约 4894 行;约 5003 行的代码内注释明确写着「纯文本 partial turn(未送达 functionCall)刻意不持久化」)。送达的 functionCall 是非 thought part,会置位 streamYieldedContentChunk,从而关闭这个门控本身——因此重放分支执行时 pendingPartialAssistantTurnIndex 必为 null,pop 提前返回(约 1806 行)。真正承重的机制是「不持久化」策略;pop 只是纵深防御。代码本身是正确的——但注释论证的准确性正是本 PR 要修复的缺陷类别,且约 2932 行的门控注释重复了同样的误归因。——失败场景:(a) 维护者精简重试清理路径时看到这个 pop,发现 thinking-only 失败从不写入 history,便删掉这个「死代码」——今天行为等价,可一旦持久化策略扩展到 thought-only partial(约 4886 行已把 JSONL 一致性列为关注点),就会悄无声息地出错;(b) 排查重试后输出重复的人读到这条注释,会停在 pop 处,而不到达真正起作用的持久化门控。

修复方式(两处注释同步):改为陈述「不持久化」策略、把 pop 表述为纵深防御(见上方示例措辞)。

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

Comment thread packages/core/src/core/geminiChat.ts Outdated
Comment on lines +2932 to +2934
// Thinking-only output does not block the replay: the
// failed attempt's partial turn is popped wholesale below,
// so nothing the caller saw from that attempt can appear

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Same misattribution as the hasNonThoughtCandidateParts docstring (companion comment at ~line 152): on the thinking-only path nothing was persisted, so "popped wholesale below" discards nothing — the replay is safe because a mid-stream partial turn is recorded on error only when a functionCall was already delivered, which this gate excludes; the pop below is defense in depth. — Failure scenario: same as the companion comment: a future pruning of the seemingly dead pop, or a debugger stopping here instead of at the persistence gate.

Fix: mirror the corrected wording, e.g. "Thinking-only output does not block the replay: such an attempt persists nothing (error-path persistence requires a delivered functionCall, which this gate excludes), and the partial turn is popped wholesale below as defense in depth — so nothing the caller saw from that attempt can appear twice."

中文说明

hasNonThoughtCandidateParts 文档注释(约 152 行的伴生评论)相同的误归因:在 thinking-only 路径上什么都没有持久化,所以下文的「整体 pop」丢弃的其实是空——重放之所以安全,是因为流中错误时只有已送达 functionCall 才会记录 partial turn,而该门控恰好排除了这种情况;下文的 pop 只是纵深防御。——失败场景:同伴生评论:未来有人删掉这个看似无用的 pop,或调试者停在这里而不到达持久化门控。

修复方式:与文档注释保持一致措辞,例如「thinking-only 输出不阻塞重放:这样的 attempt 不会持久化任何内容(错误路径的持久化要求已送达 functionCall,而此门控排除了它),下文的整体 pop 只是纵深防御——因此调用方从该 attempt 看到的任何内容都不可能出现两次。」

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

…enLM#8861)

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

Copy link
Copy Markdown
Collaborator

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

Address-review summary — PR #8861

Feedback addressed: the five inline suggestions from the automated reviewer's
round-4 review. Every finding was verified against the code before acting; all
five are resolved in this commit. No conflicts (--conflict false, no merge
performed).

Feedback points and decisions

1. [Suggestion] Fixture comment's "single producer" claim is false (geminiChat.test.ts:7420)

Verified: it.each([...RETRYABLE_STREAM_TRANSPORT_CODES]) (~line 9188)
constructs the same classified retryable shape inline, parameterized over all
six allow-listed codes including UND_ERR_SOCKET; only the 4xx and
ECONNREFUSED shapes are genuinely non-retryable inline constructions.
Rewrote the fixture comment: socketCut is the shared producer of the
canonical UND_ERR_SOCKET shape, and the per-code drift-guard test and the
non-retryable shapes construct their shapes inline on purpose.
Decision: fixed (comment-only).

2. [Suggestion] Byte-identical 12-line catch-and-drain block (geminiChat.test.ts:7590)

Verified: the zero-chunk exhaustion test and the thinking exhaustion test
carried identical copies of the collect/catch drain block. Extracted a
drainCollecting(stream) helper next to cutAfter that returns
{ events, caughtError }; both tests now call it, with the fake-timer
advances (advanceTimersByTimeAsync(0) + advanceTimersByTimeAsync(10_000))
kept explicit at the call sites. The fake-timer-trap comment stays with the
first call site. Net code shrinks.
Decision: fixed.

3. [Suggestion] Pin cannot discriminate the ternary's real key (geminiChat.test.ts:7906)

Verified: the function-call-cut test delivers a functionCall, so both
streamYieldedContentChunk and streamYieldedFunctionCall are true there and
the not-taken ternary's key cannot be discriminated. As suggested, extended
'propagates once the continuation budget is exhausted' — whose plain-text
cuts reach the not-taken branch with content delivered but no functionCall —
to assert retryDecision: 'skipped_after_content'. Mutation-checked:
temporarily re-keying the ternary to streamYieldedFunctionCall makes this
test fail (observed retryDecision: 'exhausted' where
'skipped_after_content' was expected); with the correct key all tests pass.
Decision: fixed.

4. [Suggestion] Rationale misattribution in the hasNonThoughtCandidateParts docstring (geminiChat.ts:154)

Verified: on the replay path the pop is a provable no-op — error-path
persistence requires hasToolCall (the willPersistToHistory gate), a
delivered functionCall sets streamYieldedContentChunk, which closes the
replay gate, so pendingPartialAssistantTurnIndex is null whenever the replay
branch executes and popPendingPartialAssistantTurn() early-returns. Rewrote
the docstring to ground replay safety in the non-persistence policy, with the
pop described as defense in depth that keeps the replay safe if that policy
ever widens.
Decision: fixed (comment-only).

5. [Suggestion] Same misattribution in the replay-gate comment (geminiChat.ts:2934)

Mirrored the corrected wording at the gate: a thinking-only attempt persists
nothing (error-path persistence requires a delivered functionCall, which the
gate excludes), and the pop below is defense in depth — so nothing the caller
saw from that attempt can appear twice.
Decision: fixed (comment-only).

Not actionable in code: the review body's "Test Plan (not a blocker)" note
about the PR test-plan path — the reviewer itself ran the real test file and
observed it passing, so no change was made for it.

Verification

  • npx vitest run src/core/geminiChat.test.ts (in packages/core) — 319 passed (319)
  • Mutation check for the new pin: re-keyed the ternary to streamYieldedFunctionCall — new assertion failed (mutant killed); reverted — 319 passed (319) again
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
中文说明

处理审查总结 — PR #8861

处理的反馈:自动审查者第 4 轮审查中的 5 条行内建议。每条发现均先对照代码核实,全部 5 条已在本次提交中解决。无冲突(--conflict false,未执行合并)。

反馈点与决定

1. [建议] fixture 注释的「唯一生产者」说法不成立(geminiChat.test.ts:7420

已核实:it.each([...RETRYABLE_STREAM_TRANSPORT_CODES])(约第 9188 行)同样以内联方式构造相同的、可被分类的重试形状,并对包括 UND_ERR_SOCKET 在内的全部 6 个白名单 code 参数化;只有 4xx 和 ECONNREFUSED 形状是真正不可重试的内联构造。已重写 fixture 注释:socketCut 是规范 UND_ERR_SOCKET 形状的共享生产者,按 code 参数化的 drift-guard 测试与不可重试形状则是刻意内联构造。
决定:已修复(仅注释)。

2. [建议] 逐字节重复的 12 行「捕获并排干」块(geminiChat.test.ts:7590

已核实:零 chunk 耗尽测试与 thinking 耗尽测试携带完全相同的收集/捕获排干块。在 cutAfter 旁提取了 drainCollecting(stream) 辅助函数,返回 { events, caughtError };两个测试现在都调用它,且假定时器推进(advanceTimersByTimeAsync(0) + advanceTimersByTimeAsync(10_000))仍显式保留在调用处。假定时器陷阱的注释保留在第一个调用处。代码净减少。
决定:已修复。

3. [建议] 钉住无法区分三元表达式的真实键(geminiChat.test.ts:7906

已核实:functionCall 切断测试送达了 functionCall,因此该处 streamYieldedContentChunkstreamYieldedFunctionCall 同为 true,not-taken 三元表达式的键在那里无法区分。按建议扩展了 'propagates once the continuation budget is exhausted'——其纯文本切断以「已送达内容但没有 functionCall」走到 not-taken 分支——补充 retryDecision: 'skipped_after_content' 断言。已做变异验证:临时把三元键改为 streamYieldedFunctionCall 后该测试失败(观察到 retryDecision: 'exhausted',期望 'skipped_after_content');在正确的键下全部测试通过。
决定:已修复。

4. [建议] hasNonThoughtCandidateParts 文档注释中的论证误归因(geminiChat.ts:154

已核实:在重放路径上该 pop 是可证明的空操作——错误路径的持久化要求 hasToolCallwillPersistToHistory 门控),而送达的 functionCall 会置位 streamYieldedContentChunk、从而关闭重放门控,因此重放分支执行时 pendingPartialAssistantTurnIndex 必为 null,popPendingPartialAssistantTurn() 提前返回。已重写文档注释:把重放安全性归因于「不持久化」策略,并把 pop 表述为纵深防御——一旦该策略放宽,它仍能保住重放安全。
决定:已修复(仅注释)。

5. [建议] 重放门控注释中的相同误归因(geminiChat.ts:2934

在门控处同步了修正后的措辞:thinking-only 的 attempt 不会持久化任何内容(错误路径的持久化要求已送达的 functionCall,而此门控排除了它),下文的 pop 只是纵深防御——因此调用方从该 attempt 看到的任何内容都不可能出现两次。
决定:已修复(仅注释)。

无需代码改动:审查正文中「Test Plan(非阻断)」关于 PR 测试计划路径的提示——审查者自己已运行真实测试文件并观察到通过,故未为此做任何更改。

验证

  • npx vitest run src/core/geminiChat.test.ts(在 packages/core 中)— 319 通过(319)
  • 新钉住的变异验证:把三元键改为 streamYieldedFunctionCall — 新断言失败(变异体被杀死);还原后 — 再次 319 通过(319)
  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过

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.

Reviewed — no blockers. Suggestions are inline.

Test Plan (not a blocker): src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 1200, 18898, 1502, 481, 3069, 473 passed.

中文说明

已审查——无阻断问题。 建议见行内评论。

Test Plan(非阻断):src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 1200, 18898, 1502, 481, 3069, 473 passed

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

Comment on lines +7753 to +7757
// The retry log must record that non-content chunks (the
// thinking) had already flowed — the diagnostic that makes
// thinking-phase replays visible in the debug log.
expect(mockDebugLoggerWarn).toHaveBeenCalledWith(
'Transport stream retry scheduled',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R5-1: The new yieldedNonContentChunks pins leave one reachable mutation alive: swapping the per-attempt flag for the cross-attempt flag (yieldedNonContentChunks: streamYieldedChunkstreamYieldedAnyChunk, geminiChat.ts:2974) passes every current test, because in all existing retry-scheduling scenarios the two flags agree (all attempts yield thought chunks, or none do). — Failure scenario: attempt 1 yields a thought chunk then is socket-cut (retry scheduled — both flags true); attempt 2 is cut immediately with zero chunks — the retry gate is reached again (transportStreamRetryCount 1 < 2, no content delivered, continuation buffer empty since thought text is excluded by isPlainTextPart), and attempt 2's 'Transport stream retry scheduled' log must say yieldedNonContentChunks: false; the mutant logs true. No test asserts that mixed sequence, so a future regression to the cross-attempt flag would ship and misattribute thinking-phase flow in the very diagnostic this PR pins. Impact is confined to the debug log; the code as written is correct today. Measured: with the mutant applied the full geminiChat.test.ts suite passes 319/319; a mixed-attempt probe fails under the mutant and passes on revert. Suggested fix: add a mixed-attempt test (attempt 1: cutAfter([thoughtChunk]), attempt 2: immediate cut, attempt 3: success or exhaustion under fake timers) asserting the second 'Transport stream retry scheduled' call carries yieldedNonContentChunks: false — e.g. filter mockDebugLoggerWarn.mock.calls to the scheduled logs and assert [true, false].

中文说明

新增的 yieldedNonContentChunks 断言留下了一个可达的变异体:把 per-attempt 标志换成跨 attempt 标志(yieldedNonContentChunks: streamYieldedChunkstreamYieldedAnyChunkgeminiChat.ts:2974)能通过当前所有测试,因为在现有的所有重试调度场景中两个标志都一致(要么每次 attempt 都产生 thought chunk,要么都没有)。失败场景:attempt 1 先 yield 一个 thought chunk 再被切断(重试被调度——两个标志均为 true);attempt 2 在零 chunk 时立即被切断——重放门控再次可达(transportStreamRetryCount 1 < 2、未送达内容、续传缓冲区为空,因为 thought 文本被 isPlainTextPart 排除),attempt 2 的 'Transport stream retry scheduled' 日志本应输出 yieldedNonContentChunks: false,变异体却输出 true。没有任何测试断言这个混合序列,因此未来若回归到跨 attempt 标志,将会悄悄把 thinking 阶段的流量错误地记入本 PR 正要钉住的诊断字段。影响仅限于 debug 日志;当前代码本身是正确的。实测:应用该变异体后 geminiChat.test.ts 全量套件 319/319 通过;混合 attempt 探针在变异体下失败、还原后通过。建议修复:新增一个混合 attempt 测试(attempt 1:cutAfter([thoughtChunk]),attempt 2:立即切断,attempt 3:在 fake timers 下成功或耗尽),断言第二次 'Transport stream retry scheduled' 调用携带 yieldedNonContentChunks: false——例如过滤 mockDebugLoggerWarn.mock.calls 中的 scheduled 日志并断言 [true, false]

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

No action required this round (PR #8861)

No actionable feedback was found for this round:

  • Reviews / inline comments / issue-level comments: no new feedback from trusted maintainers or the automated reviewer since the last evaluation (2026-08-10T19:04:42Z). All existing reviews are COMMENTED with no blockers.
  • Checks: all 35 reported checks are SUCCESS (12) or SKIPPED (23); there are no failed or still-red checks.
  • Critical-only mode is active after 5 change-producing rounds. The deferred non-Critical items listed in this round's input (one automated review and inline comment rc:3753253341 at packages/core/src/core/geminiChat.test.ts:7757) are audit records left open for human follow-up. Per the Critical-only rules, no code changes, thread resolutions, or comment replies were made for them.
  • No conflict resolution was requested (--conflict false), and no retry/rejection context was supplied, so no merge or repair work was performed.

The branch remains at commit 6be3799cbe with no changes made or pushed this round.

中文说明

本轮无需采取任何操作(PR #8861

本轮未发现可操作的反馈:

  • 评审 / 行内评论 / 议题级评论:自上次评估(2026-08-10T19:04:42Z)以来,没有来自受信任维护者或自动评审器的新反馈。现有评审均为 COMMENTED 状态,没有阻塞项。
  • 检查:全部 35 项报告的检查均为 SUCCESS(12 项)或 SKIPPED(23 项),没有失败或持续失败的检查。
  • 仅处理 Critical 的模式已生效(此前已完成 5 个产生改动的轮次)。本轮输入中列出的被延后非 Critical 条目(一条自动评审,以及位于 packages/core/src/core/geminiChat.test.ts:7757 的行内评论 rc:3753253341)是留待人工跟进的审计记录。按照仅处理 Critical 的规则,未对其修改代码、解决线程或回复评论。
  • 未请求冲突解决--conflict false),也没有提供重试/拒绝上下文,因此未执行任何合并或修复工作。

分支仍停留在提交 6be3799cbe,本轮没有做出或推送任何改动。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

@wenshao @yiliang114 Gentle ping when you have a moment — this follow-up to your #7938 verification has been green for a while (all checks passing, both findings landed plus the four inline-review items), and branch protection is now only waiting on code-owner approvals. No rush, just flagging it's ready. / 顺手 ping 一下:#7938 验证报告的这个后续已全绿多时(两 finding + 四条 inline 建议均已落地),分支保护只差 code owner approve,方便时请过目,不急。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round result: no action required (PR #8861)

Triage summary

This round's feedback contains no actionable items:

  • Reviews: none.
  • Inline comments: none.
  • Issue-level comments: a single comment (@ComplexSimply) gently pinging the code owners that the PR is green and awaiting code-owner approval. It requests no change and raises no defect — nothing to address.
  • Failed checks: none.
  • Still-red checks: none.
  • Deferred non-Critical feedback: Critical-only mode is active after five change-producing rounds, but no deferred items were listed in this round's input. Deferred items are an audit record rather than work in any case.

Result

No code changes were made this round and nothing was committed. The branch remains at 6be3799cbe with a clean working tree. The PR continues to await human code-owner review and approval.

中文说明

Autofix 轮次结果:无需处理(PR #8861

分类摘要

本轮反馈中没有任何可处理的条目:

  • Reviews: 无。
  • Inline comments: 无。
  • Issue-level comments: 仅有一条评论(@ComplexSimply),内容是向 code owner 发出温和提醒,说明该 PR 已全绿、正等待 code owner approve。该评论未请求任何改动,也未提出任何缺陷——没有需要处理的事项。
  • Failed checks: 无。
  • Still-red checks: 无。
  • Deferred non-Critical feedback: 在 5 个产生改动的轮次后已进入 Critical-only 模式,但本轮输入中未列出任何被延后的条目。且延后条目本身只是审计记录,不属于工作范围。

结果

本轮未做任何代码改动,也没有创建提交。分支仍停留在 6be3799cbe,工作树保持干净。该 PR 继续等待人工 code owner 审阅与批准。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real local stack

Verdict: LGTM, merge-ready. Both findings from the #7938 verification are genuinely closed: the corrected comment is factually true (checked at runtime, not by reading), and both diagnostics are now pinned — every mutant I could write against them survives the suite on main and is killed by this branch.

Environment

Head 6be3799 merged into main 8858d434 — clean merge, 2 files changed, +216 −77
OS / runtime macOS 26.6 (Darwin 25.6.0, arm64), Node v24.18.1
Legs 14 vitest runs (mutation matrix) · 3 runtime probes · 3 end-to-end CLI runs against a socket-cutting mock gateway

1. The source change really is comment-only

Not just "the diff lines start with //" — I compiled both versions with removeComments: true and hashed the emitted JS:

main  emit sha256: 84091d18f3036f07c29e9effa443527fbf9ac17995bff57a0f1e8ba4958faa8f
head  emit sha256: 84091d18f3036f07c29e9effa443527fbf9ac17995bff57a0f1e8ba4958faa8f
emitted JS identical (comments removed): true

Zero runtime behavior change, confirmed mechanically.

2. Suite and gates

check result
vitest run src/core/geminiChat.test.ts (PR arm) 319 / 319 pass
same suite on main 317 / 317 pass (this PR adds 2 tests)
prettier --check on both files clean
eslint on both files exit 0
tsc --noEmit -p packages/core exit 0

3. Mutation matrix — 6 mutants × {main, PR}

This is the load-bearing evidence for finding (b). I mutated geminiChat.ts six ways and ran the full suite on both arms.

mutation matrix

mutant on main on this PR killed by
yieldedNonContentChunks: <flag>false survives 1 fail …after yielding only thinking chunks
yieldedNonContentChunks: <flag>true survives 1 fail …after yielding only tool preparation metadata
'skipped_after_content''skipped_after_chunk' survives 2 fail function call was cut + continuation budget is exhausted
ternary → always 'skipped_after_content' survives 2 fail budget exhaustion … when thinking chunks flowed + retry budget is exhausted
ternary → always 'exhausted' survives 2 fail function call was cut + continuation budget is exhausted
ternary key → streamYieldedFunctionCall survives 1 fail continuation budget is exhausted

6/6 survive on main, 6/6 are killed here. The diagnostics were genuinely unpinned before this PR.

4. Finding (a) — is the corrected rationale actually true?

I did not take the comment's word for it. Three probes driven against the real GeminiChat (probes are mine, not part of the PR):

probes and comment-only proof

  • The old comment was wrong. After a thinking-only cut and a successful retry, history holds [{"text":"RETRY_ATTEMPT_THOUGHT","thought":true}, {"text":"the visible answer"}] — the successful attempt's thought parts are recorded. The failed attempt's thought text is absent, so nothing the caller saw can appear twice.
  • "Persists nothing" checks out. On the thinking-only cut, [PARTIAL_PUSH] count = 0 — popPendingPartialAssistantTurn() has nothing to pop on that path, exactly as the new comment says.
  • "Error-path persistence requires a delivered functionCall" checks out. Cut after a functionCall: [PARTIAL_PUSH] count = 1, and the persisted turn carries the functionCall.

5. Real stack — both diagnostics observed in a production debug log

To confirm these are not test-only fields, I bundled the CLI from this branch (dist/cli.js) and ran it headless against a mock OpenAI-compatible gateway that destroys the SSE socket mid-stream (undici UND_ERR_SOCKET), with QWEN_DEBUG_LOG_FILE=1 and an isolated QWEN_HOME.

real stack

Scenario A — cut while only reasoning_content had streamed (the #7832 shape). The CLI recovered and answered; the real log line is:

[WARN] [QWEN_CODE_CHAT] Transport stream retry scheduled {
  retryDecision: 'retry', attempt: 1, retryDelayMs: 1000,
  yieldedNonContentChunks: true,      <-- the field this PR pins
  transportCode: 'UND_ERR_SOCKET' }

The mock also recorded every upstream body: request #1 is a byte-identical replay of #0 (sha=867fe767f62b both), and carries none of the delivered thinking text — a replay, not a continuation, which is the invariant the corrected comment is arguing for.

Scenario B — visible text delivered then cut, on every attempt. Three continuations, then:

[WARN] [QWEN_CODE_CHAT] Transport stream retry not taken {
  retryDecision: 'skipped_after_content',   <-- the label this PR pins
  attempts: 0, continuationAttempts: 3, maxContinuationRetries: 3 }

Both pinned diagnostics are real, reachable artifacts. The assertions are worth having.


Non-blocking notes (description only — no code change requested)

  1. "301/301 pass" — the final branch merged into current main is 319/319. Worth refreshing before merge.
  2. "each mutant now fails exactly one test" — true when written, but after the later review commits the three ternary mutants each fail two tests. Stronger coverage, stale sentence.
  3. "the one path on current main that still emits skipped_after_content is a cut after a delivered functionCall" — not accurate: a plain-text cut also reaches that log once the continuation budget is exhausted. I reproduced it end to end (scenario B above), and the branch's own added assertion in "propagates once the continuation budget is exhausted" covers it — it's what kills the re-keyed-ternary mutant. Only the prose is wrong; the tests are right.
  4. Context, not a defect: I tried to reproduce the function-call cut through the OpenAI-compatible provider and it replayed instead of blocking — at that point the converter has only pushed tool preparation metadata (preparations.push on the delta.tool_calls branch), so no functionCall part reached the gate and the shape lands on the retryable path already covered by the tool preparation metadata test. The new function-call-cut test drives the ContentGenerator directly, so it covers providers that do stream functionCall parts. Both paths now have a test; nothing to change.

Tested on

OS Status
🍏 macOS ✅ (this report)
🪟 Windows ⚠️
🐧 Linux
中文版本

维护者验证 —— 本地真实环境

结论:LGTM,可以合并。 #7938 验证报告里的两个 finding 都被真正闭合了:修正后的注释在运行时被证实为真(不是靠读代码),两个诊断字段也确实被钉住了 —— 我针对它们写的每一个变异体,在 main 上都能存活,在这个分支上都被杀死。

环境

Head 6be3799 合入 main 8858d434 —— 干净合并,2 files changed, +216 −77
系统 / 运行时 macOS 26.6(Darwin 25.6.0, arm64),Node v24.18.1
验证腿 14 次 vitest 运行(变异矩阵)· 3 个运行时探针 · 3 次真实 CLI 端到端(对接会切断 socket 的 mock 网关)

1. 源码改动确实只有注释

不是只看 diff 行是否以 // 开头 —— 我用 removeComments: true 编译两个版本并对产物取哈希:

main  emit sha256: 84091d18f3036f07c29e9effa443527fbf9ac17995bff57a0f1e8ba4958faa8f
head  emit sha256: 84091d18f3036f07c29e9effa443527fbf9ac17995bff57a0f1e8ba4958faa8f
产物完全一致(去掉注释后): true

运行时行为零变化,机械可证。

2. 测试与门禁

检查 结果
vitest run src/core/geminiChat.test.ts(PR 侧) 319 / 319 通过
同一套件在 main 317 / 317 通过(本 PR 净增 2 个测试)
两个文件 prettier --check 干净
两个文件 eslint exit 0
tsc --noEmit -p packages/core exit 0

3. 变异矩阵 —— 6 个变异体 × {main, PR}

这是 finding (b) 的核心证据。我对 geminiChat.ts 做了六种变异,两侧都跑全量套件。

变异体 main 本 PR 上 被谁杀死
yieldedNonContentChunks: <flag>false 存活 1 失败 …after yielding only thinking chunks
yieldedNonContentChunks: <flag>true 存活 1 失败 …after yielding only tool preparation metadata
'skipped_after_content''skipped_after_chunk' 存活 2 失败 function call was cut + continuation budget is exhausted
三元 → 恒为 'skipped_after_content' 存活 2 失败 budget exhaustion … when thinking chunks flowed + retry budget is exhausted
三元 → 恒为 'exhausted' 存活 2 失败 function call was cut + continuation budget is exhausted
三元判据改为 streamYieldedFunctionCall 存活 1 失败 continuation budget is exhausted

6/6 在 main 上存活,6/6 在这里被杀死。 合并前这些诊断字段确实没有任何测试保护。

4. Finding (a) —— 修正后的论证是不是真的成立?

我没有直接采信注释。三个探针直接驱动真实的 GeminiChat(探针是我加的,不属于本 PR):

  • 旧注释是错的。 thinking-only 被切断、重试成功后,history 里是 [{"text":"RETRY_ATTEMPT_THOUGHT","thought":true}, {"text":"the visible answer"}] —— 成功那次 attempt 的 thought 确实被记录了。失败 attempt 的 thought 文本不在其中,所以调用方看到的东西不会出现两次。
  • 「什么都不持久化」成立。 thinking-only 切断时 [PARTIAL_PUSH] 计数为 0 —— popPendingPartialAssistantTurn() 在这条路径上确实无物可弹,与新注释一致。
  • 「错误路径持久化需要已送达的 functionCall」成立。 functionCall 之后被切断:[PARTIAL_PUSH] 计数为 1,且落盘的 partial turn 里带着那个 functionCall

5. 真实栈 —— 两个诊断都在真实 debug 日志里被观测到

为了确认这不是只存在于测试里的字段,我从本分支打了 CLI bundle(dist/cli.js),headless 跑起来对接一个会在流中途 destroy SSE socket 的 mock OpenAI 网关(undici UND_ERR_SOCKET),开 QWEN_DEBUG_LOG_FILE=1 并隔离 QWEN_HOME

场景 A —— 只流出了 reasoning_content 就被切断#7832 的典型形态)。CLI 恢复并给出了答案,真实日志行:

[WARN] [QWEN_CODE_CHAT] Transport stream retry scheduled {
  retryDecision: 'retry', attempt: 1, retryDelayMs: 1000,
  yieldedNonContentChunks: true,      <-- 本 PR 钉住的字段
  transportCode: 'UND_ERR_SOCKET' }

mock 还记录了每一次上行请求体:request #1 与 #0 逐字节相同(两者 sha=867fe767f62b),且不含任何已送达的 thinking 文本 —— 是重放而非续传,正是修正后的注释所论证的不变量。

场景 B —— 每次都先送出可见文本再切断。 三次续传后:

[WARN] [QWEN_CODE_CHAT] Transport stream retry not taken {
  retryDecision: 'skipped_after_content',   <-- 本 PR 钉住的标签
  attempts: 0, continuationAttempts: 3, maxContinuationRetries: 3 }

两个被钉住的诊断都是真实可达的产物,这些断言值得保留。

不阻塞的说明(只涉及描述,不要求改代码)

  1. 「301/301 pass」 —— 最终分支合入当前 main 后是 319/319,合并前可以更新一下。
  2. 「每个变异体恰好导致一个测试失败」 —— 当时成立,但后续的 review commit 之后,三个三元变异体各会失败两个测试。覆盖更强了,句子过期了。
  3. 「当前 main 上仍会产生 skipped_after_content 的唯一路径是 functionCall 送达后被切断」 —— 不准确:纯文本被切断、续传预算耗尽后同样会走到这条日志。我端到端复现了(上面场景 B),而且分支自己在 "propagates once the continuation budget is exhausted" 里新增的断言正好覆盖它 —— 三元判据被改写的那个变异体就是被它杀死的。错的只有描述文字,测试是对的。
  4. 补充背景,不是缺陷: 我尝试通过 OpenAI 兼容 provider 复现 functionCall 切断,结果发生了重放而不是阻塞 —— 那一刻转换器只在 delta.tool_calls 分支上 preparations.pushtool preparation 元数据,没有 functionCall part 到达门控,所以这种形态落在已被 tool preparation metadata 测试覆盖的可重放路径上。新增的 function-call 切断测试直接驱动 ContentGenerator,覆盖的是会在流中途送出 functionCall part 的 provider。两条路径现在都有测试,无需改动。

测试平台

系统 状态
🍏 macOS ✅(本报告)
🪟 Windows ⚠️
🐧 Linux

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

No action taken — PR #8861

This round's feedback contains no actionable code changes, so no commit was made.

  • No reviews, inline comments, failed checks, or still-red checks were listed for this round.
  • The only issue-level comment is a maintainer verification report (@wenshao) with verdict LGTM, merge-ready. It confirms both original findings are genuinely closed: the source change is comment-only (proven mechanically by hashing removeComments: true output on both arms), and both diagnostics are pinned — all 6 mutants survive on main and are killed by this branch, with both log fields observed in a real end-to-end debug log.
  • The comment's four non-blocking notes concern the PR description text only — a stale test count ("301/301" vs the current 319/319), a stale sentence about per-mutant failure counts, and one inaccurate prose statement about the skipped_after_content path. The maintainer explicitly states "description only — no code change requested," and notes the tests themselves are correct. Refreshing the PR description is a GitHub write operation outside this workflow's allowed actions, so it is left for the maintainer before merge.

No source files were modified; the branch remains at its verified head commit 6be3799cbe with a clean working tree.

中文说明

未采取行动 —— PR #8861

本轮反馈中没有需要处理的代码改动,因此未产生任何提交。

  • 本轮没有列出任何 review、行内评论、失败的检查或仍然失败的检查
  • 唯一的 issue 级评论是一份维护者验证报告(@wenshao),结论为 LGTM,可以合并。它确认最初的两个 finding 都被真正闭合:源码改动仅为注释(通过对两侧 removeComments: true 的编译产物取哈希机械证明),且两个诊断字段均已钉住 —— 全部 6 个变异体在 main 上存活、在本分支上被杀死,两个日志字段也在真实端到端 debug 日志中被观测到。
  • 该评论中的四条不阻塞说明仅涉及 PR 描述文字 —— 过时的测试数(「301/301」,当前为 319/319)、关于每个变异体失败数量的过时句子,以及一句关于 skipped_after_content 路径的不准确描述。维护者明确说明「只涉及描述,不要求改代码」,并指出测试本身是正确的。更新 PR 描述属于 GitHub 写操作,超出了本工作流允许的动作范围,因此留给维护者在合并前处理。

未修改任何源文件;分支保持在已验证的 head commit 6be3799cbe,工作区干净。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

wenshao
wenshao previously approved these changes Aug 12, 2026
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 44 passed · 0 failed · 44 total

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

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

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

Verification report

Verification report — PR #8861

Verdict: merge-ready — 44/44 scripted assertions passed, 0 unexpected failures. No blocking findings; three description-level corrections noted below (the shipped code and comments are accurate in every place I checked). Verified head: 6be3799cbea9c5edf7baff67cd726eb5276ae05e (HEAD^2 of the merge-ref checkout; base HEAD^1 = 187637449bf382d60110530c85a7b00efe81115e).

中文摘要
  • 结论merge-ready。44/44 脚本断言通过,0 个意外失败;无阻塞性 finding,仅 3 处 PR 描述层面的更正(代码与注释本身均准确)。
  • 核心结论(A/B)geminiChat.ts 的源码 diff 确为纯注释——剥离注释后 base 与 head 转译产物 sha256 逐字节相同(见 03-comment-only-proof.png);套件在 base 317/317、head 319/319 全绿,差值恰为新增的 2 个测试(01-ab-baseline-head-vs-base.png)。
  • 变异矩阵:7 个单点变异 × {head, base} 共 16 次 vitest 运行。6 个诊断变异在 head 全部被杀(1–2 个测试失败,杀手与 PR 声称一致),在 base 全部幸存——钉住作用真实存在且为本 PR 所引入;正对照(翻转重放门控)在两侧均杀 15–17 个测试,证明两侧套件都能变红(02-mutation-matrix-head-vs-base.png)。
  • 注释论证核对:12 项脚本检查全过——成功 attempt 的 thought 确实写入 history;错误路径持久化确实以 delivered functionCall 为前提;重试前 popPendingPartialAssistantTurn() 在 thinking-only 路径上确为 no-op(04-rationale-checks.png)。
  • Findings(均不阻塞):PR 正文三处描述与实测不符——(1) 套件计数 "301/301" 已过期(实测 319/319,base 317/317,main 前进所致);(2) "每个变异恰好失败一个测试" 对三元折叠变异不成立(实测 2 个,因 PR 自己新增的 continuation 断言补杀一个);(3) "skipped_after_content 的唯一路径是 functionCall 切断" 不成立——continuation 预算耗尽(PR 自己的 diff 钉住了它)与 inlineData-only 切断(活体探针证实,05-census-inlinedata-sibling-path.png)同样产生该标签。代码行为均正确,问题仅在正文措辞。
  • 未覆盖:浅克隆(depth 2)无法逐 commit 归因(PR 6 个 commit 中本地仅可达 1 个),验证对象为聚合 diff;未跑全仓库测试/CI(仅 geminiChat 套件 + core 包 tsc/eslint/prettier,均含活性证明)。

Central claim and A/B

Central claim: the geminiChat.ts diff is comment-only (zero runtime change), and the new/changed test assertions pin the two retry diagnostics that survived mutation on base. Secondary: the corrected replay-safety rationale matches the code.

Cell Environment Oracle Result
base suite worktree at HEAD^1, node_modules symlinked; import-closure BFS (633 files) shows no @qwen-code/* barrel import reaches the suite, so the control cannot load head code vitest exit + count 317/317 pass
head suite merge-ref checkout same 319/319 pass
delta test count +2 = exactly the two new its
comment-only proof ts.transpileModule with removeComments on both git objects sha256 of emitted JS raw sources differ; emitted JS byte-identical (15f19adb… on both sides)

Witnesses: 01-ab-baseline-head-vs-base.png, 03-comment-only-proof.png.

Mutation A/B matrix (7 mutants × 2 arms: 14 mutant runs + 2 unmutated controls = 16 vitest runs)

Mutant head suite base suite
control (unmutated) 319 pass 317 pass
PC: flip replay gate !streamYieldedContentChunk (positive control) KILLED (17) KILLED (15)
M1 yieldedNonContentChunks: false KILLED (1) — "retries … after yielding only thinking chunks" survives
M2 yieldedNonContentChunks: true KILLED (1) — "retries … after yielding only tool preparation metadata" survives
M3 relabel 'skipped_after_content''skipped_after_chunk' KILLED (2) — new functionCall-cut test + "propagates once the continuation budget is exhausted" survives
M4 collapse ternary → 'exhausted' KILLED (2) — same two as M3 survives
M5 collapse ternary → 'skipped_after_content' KILLED (2) — both budget-exhaustion tests survives
M6 re-key ternary to streamYieldedFunctionCall KILLED (1) — continuation-budget test (exactly the pin the added assertion's comment promises) survives

Every restore verified byte-identical (sha256) after each cell; both trees pristine at the end. Witness: 02-mutation-matrix-head-vs-base.png. The base arm's six survivors reproduce the #7938 verification's finding (diagnostics unpinned); the head arm kills all six — the pinning is real and attributable to this PR's assertions. The positive control is killed on both arms, proving the harness can fail each suite.

Reviewer Test Plan walkthrough

Plan step Result
vitest run src/core/geminiChat.test.ts — "301/301 pass" Suite green, but 319/319 (base 317/317). The 301 count is stale — main advanced via the PR's own merge of main. See correction C1.
M1 ⇒ "1 failed / 300 passed", killed by the thinking-only test Measured 1 failed / 318 passed, same killer. Shape matches; counts stale.
M3 ⇒ "1 failed / 300 passed", killed by the new functionCall-cut test Measured 2 failed / 317 passed: the new test plus the continuation-budget test the second commit also pinned. See correction C2.
Finding (a): check the two corrected comments against the code Verified by 12 scripted checks, all PASS (04-rationale-checks.png): success-path history.push prepends thoughtContentPart (thoughts ARE recorded); on streamError the persistence gate reduces to hasToolCall && (…) (error-path persistence requires a delivered functionCall); popPendingPartialAssistantTurn() precedes the scheduled log and is a no-op on the thinking-only path (nothing is pushed there); the stale "never recorded" rationale is gone from source and test header and present on base.

Corrections (description-level, not code-change requests)

  • C1 — stale test count. The body's "301/301" (and the mutation table's "/300") predate main advancing; at the verified head the suite is 319 tests (base 317). Evidence: 01-ab-baseline-head-vs-base.png.
  • C2 — "each mutant fails exactly one test" overstates for the ternary collapses. Commit 2's message says hardcoding-true and collapsing the ternary each fail exactly one test. Measured: hardcode-true kills 1 (✓), but each ternary collapse kills 2 (the two tests pinning the opposite arm's label), and the label rename kills 2. Directionally correct (nothing survives); the "exactly one" detail is wrong for M3/M4/M5. Evidence: 02-mutation-matrix-head-vs-base.png.
  • C3 — the "one path" uniqueness claim for skipped_after_content is false. The body says the only path on current main that still emits skipped_after_content is a cut after a delivered functionCall. Census of the gates (scripted checks B1–B3 + live probe) shows three shapes: (1) functionCall cut (continuation gate closed by !streamYieldedFunctionCall) — the PR's new test; (2) content cut with the continuation budget exhausted (the PR's own added assertion in "propagates once the continuation budget is exhausted" pins this, so the body contradicts its own diff); (3) a cut after non-text content with no text ever delivered — an inlineData-only cut, proven live by a scratch probe (1 attempt, 0 RETRY events, not-taken log with skipped_after_content), 05-census-inlinedata-sibling-path.png. The shipped code and its comments never make the uniqueness claim and handle all three shapes correctly; only the PR body's wording is incomplete.

Findings

No blocking findings. C1–C3 above are the complete finding list; all are description-level (the code, comments, and pins are correct and load-bearing).

Not covered

  • Per-commit attribution: the checkout is depth 2; only 1 of the PR's 6 commits is locally reachable (git rev-list HEAD^1..HEAD^2 = 1 vs 6 commits in the metadata snapshot). All measurements are against the aggregate HEAD^1..HEAD diff; per-commit claims were not individually exercised.
  • Repo-wide gates: only the affected surface was gated — geminiChat.test.ts suite (both arms), tsc --noEmit in packages/core, eslint + prettier --check on both changed files. The PR's own CI covers the rest; I did not re-run it. Mutation runs used --coverage.enabled=false for speed (coverage is orthogonal to pass/fail counts).
  • Runtime E2E: none — unnecessary for a comment-only source change proven byte-identical after comment stripping; the suite exercises the retry machinery through the real (unmocked) GeminiChat code path.
  • The census probe was run as a scratch copy of the suite (deleted afterwards); it is a demonstration fixture, not a test this PR ships.

Methodology

Environment: CI verify container (node:22-bookworm), merge-ref checkout (HEAD = merge, HEAD^1 = base tip, HEAD^2 = verified head). Base control: git worktree add tmp/base-tree HEAD^1 with root + packages/core node_modules symlinked; a BFS over the test's 633-file import closure showed no @qwen-code/* self-import, and readlink -f node_modules/@qwen-code/qwen-code-core was asserted (it resolves into the head tree but is unreachable from this suite), so the control loads base code only. Harnesses (matrix-runner.mjs, ab-witness.mjs, comment-only-proof.mjs, rationale-checks.mjs, matrix-table.mjs, parse-matrix-logs.mjs) live in this artifact dir; raw per-cell vitest logs in logs/head-matrix/ and logs/base-matrix/ (16 runs: 14 mutants + 2 controls), plus logs/ab-witness.log, logs/rationale-checks.log, logs/comment-only-proof.log. Every mutation cell restored the source byte-identically (sha256-verified), and both trees were pristine at the end. Captures rendered with scripts/verify-capture.mjs.

Evidence images

01-ab-baseline-head-vs-base

02-mutation-matrix-head-vs-base

03-comment-only-proof

04-rationale-checks

05-census-inlinedata-sibling-path

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

No action taken — PR #8861

This round's feedback contains no actionable code changes, so no commit was made.

  • No reviews requesting changes, no inline comments, no failed checks, and no still-red checks were listed for this round. Critical-only mode is active, and the actionable sections are empty.
  • The two reviews newer than the last evaluation are both APPROVED (@wenshao and the automated reviewer). The maintainer verification report returns LGTM, merge-ready, confirming both original findings are genuinely closed: the source change is comment-only (proven mechanically by hashing removeComments: true output on both arms), and both diagnostics are pinned — all mutants survive on main and are killed by this branch, with both log fields observed in a real end-to-end debug log.
  • The only deferred item is a transient "sandboxed verification is running" status marker from the CI bot. It is non-Critical and is excluded from this round's actionable sections per Critical-only mode, so no code change, thread resolution, or reply was made for it.
  • The earlier non-blocking notes concern the PR description text only ("301/301" vs the current 319/319, a stale per-mutant failure sentence, and one inaccurate prose statement about the skipped_after_content path). The maintainer explicitly stated "description only — no code change requested," and the tests themselves are correct. Refreshing the PR description is a GitHub write operation outside this workflow's allowed actions, so it is left for the maintainer before merge.

No source files were modified; the branch remains at its verified head commit 6be3799cbe with a clean working tree and no conflict with main.

中文说明

未采取行动 —— PR #8861

本轮反馈中没有需要处理的代码改动,因此未产生任何提交。

  • 本轮没有列出任何要求改动的 review、行内评论、失败的检查或仍然失败的检查。当前处于仅处理 Critical 的模式,可执行区域为空。
  • 晚于上次评估的两个 review 均为 APPROVED@wenshao 与自动审阅者)。维护者验证报告结论为 LGTM,可以合并,确认最初的两个 finding 都被真正闭合:源码改动仅为注释(通过对两侧 removeComments: true 的编译产物取哈希机械证明),且两个诊断字段均已钉住 —— 全部变异体在 main 上存活、在本分支上被杀死,两个日志字段也在真实端到端 debug 日志中被观测到。
  • 唯一的延后条目是 CI 机器人发布的「沙箱验证正在运行」临时状态标记。它属于非 Critical,按仅处理 Critical 模式被排除在本轮可执行区域之外,因此未对其做任何代码改动、线程解决或回复。
  • 较早的非阻塞说明仅涉及 PR 描述文字(「301/301」,当前为 319/319、关于每个变异体失败数量的过时句子,以及一句关于 skipped_after_content 路径的不准确描述)。维护者明确说明「只涉及描述,不要求改代码」,且测试本身是正确的。更新 PR 描述属于 GitHub 写操作,超出了本工作流允许的动作范围,因此留给维护者在合并前处理。

未修改任何源文件;分支保持在已验证的 head commit 6be3799cbe,工作区干净,与 main 无冲突。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

…iagnostics-followup

# Conflicts:
#	packages/core/src/core/geminiChat.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #8861 (address-review)

Feedback triage

This round ran in Critical-only mode (5 change-producing rounds complete).
The actionable sections were empty: no reviews, no inline comments, no
issue-level comments, no failed checks, and no still-red checks. The
Deferred non-Critical feedback section is an audit record only — per the
Critical-only rules it was not touched, and those items remain open for
human follow-up.

The round's work was therefore the requested base-conflict resolution
(--conflict true): merging origin/main into the PR branch.

Conflict resolution

git merge origin/main produced exactly one conflict, in
packages/core/src/core/geminiChat.ts (the test file auto-merged cleanly):

  • Our side (PR test(core): pin transport retry diagnostics and correct the replay-safety comment #8861) changed no code in this region — it corrected two
    rationale comments (the hasNonThoughtCandidateParts docstring and the
    transport-replay gate comment) to ground the thinking-replay safety
    argument in the partial-turn discard rather than the incorrect
    "thoughts are never recorded" claim.
  • origin/main refactored the region: the local
    const isRetryableStreamTransportError predicate was extracted to
    isRetryableStreamTransportError() in stream-transport-retry.ts, and
    the three call sites now invoke the function.

Resolution: keep our corrected comment block and adopt main's code shape
(delete the local const; the gate calls the imported function). Verified
before resolving:

  1. The extracted function is semantically identical to the removed const
    (kind === 'transport' && transportCode !== undefined && RETRYABLE_STREAM_TRANSPORT_CODES.has(transportCode)).
  2. Our comment's claims still hold in main's evolved code:
    popPendingPartialAssistantTurn() runs immediately inside the retry
    branch ("popped wholesale below"), and a delivered functionCall sets
    streamYieldedContentChunk via hasNonThoughtCandidateParts, so the
    replay gate excludes it ("error-path persistence requires a delivered
    functionCall, which this gate excludes").
  3. The other auto-merged hunk (the hasNonThoughtCandidateParts docstring)
    landed intact, and the remaining three predicate call sites all use the
    function form.
  4. A diff of the resolved file against origin/main shows only the two
    PR comment corrections and no code delta — the merge introduces nothing
    else.

Other notes

  • The pre-commit hook (lint-staged → prettier --write) reformatted three
    files at commit time because the installed prettier 3.6.1 (declared
    ^3.5.3) wraps union types differently from main's 3.5-era formatting.
    Those unstaged reformats (in geminiChat.ts, agent-core.ts,
    config-session-env.test.ts — the latter two outside this PR's
    footprint) were discarded so the merge commit stays a faithful union of
    its parents and inside the PR footprint. This formatting drift is
    pre-existing repo-wide and unrelated to test(core): pin transport retry diagnostics and correct the replay-safety comment #8861.
  • No new guard, branch, or behavior was added this round (comments plus a
    merge adopting main's existing code), so no mutation probe applies.
  • Additive history only: one merge commit c87e5110d8 with parents
    6be3799cbe (prior PR tip) and 22bb5e8b9f (origin/main).

Verification

  • git merge origin/main — one conflict (geminiChat.ts), resolved as above
  • npx vitest run src/core/geminiChat.test.ts (packages/core) — 370 passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npm run build — passed
  • git status --short — clean after the merge commit; no unintended files
  • Post-merge fidelity checks: resolved file vs origin/main = the two comment
    corrections only; agent-core.ts / config-session-env.test.ts identical
    to origin/main
中文说明

Autofix 轮次总结 — PR #8861(address-review)

反馈分诊

本轮运行于 仅 Critical 模式(已完成 5 个产生改动的轮次)。可执行区域为空:没有评审、没有行内评论、没有 issue 级评论、没有失败的检查,也没有持续失败的检查。Deferred non-Critical feedback(已延期的非 Critical 反馈)部分仅为审计记录——按照仅 Critical 模式的规则,未对其做任何处理,这些条目保持开放,留待人工跟进。

因此本轮的工作是所请求的与 base 的冲突解决(--conflict true):将 origin/main 合并进 PR 分支。

冲突解决

git merge origin/main 只产生了一处冲突,位于 packages/core/src/core/geminiChat.ts(测试文件自动合并成功):

  • 我方(PR test(core): pin transport retry diagnostics and correct the replay-safety comment #8861 在该区域没有改动任何代码——它修正了两处理由注释(hasNonThoughtCandidateParts 的文档字符串和传输重放门槛注释),将 thinking 重放安全性的论证基础从不正确的"thought 不会被记录"改为部分回合丢弃(partial-turn discard)。
  • origin/main 重构了该区域:将本地 const isRetryableStreamTransportError 谓词提取为 stream-transport-retry.ts 中的 isRetryableStreamTransportError() 函数,三个调用点改为调用该函数。

解决方式:保留我方修正后的注释块,同时采用 main 的代码形态(删除本地 const,门槛改为调用导入的函数)。解决前已验证:

  1. 提取出的函数与被删除的 const 语义完全一致(kind === 'transport' && transportCode !== undefined && RETRYABLE_STREAM_TRANSPORT_CODES.has(transportCode))。
  2. 我方注释中的论断在 main 演进后的代码中依然成立:popPendingPartialAssistantTurn() 紧跟在重试分支内执行("下方整体弹出");已送达的 functionCall 会通过 hasNonThoughtCandidateParts 置位 streamYieldedContentChunk,因此重放门槛会将其排除("错误路径持久化需要已送达的 functionCall,而该门槛排除了它")。
  3. 另一个自动合并的 hunk(hasNonThoughtCandidateParts 文档字符串)完整落位,其余三个谓词调用点均为函数调用形态。
  4. 解决后的文件与 origin/main 的 diff 仅包含 PR 的两处注释修正,无任何代码差异——合并不引入其他内容。

其他说明

  • 提交时 pre-commit 钩子(lint-staged → prettier --write)重新格式化了三个文件,原因是已安装的 prettier 3.6.1(声明为 ^3.5.3)对联合类型的换行方式与 main 上 3.5 时代的格式化结果不同。这些未暂存的重新格式化改动(涉及 geminiChat.tsagent-core.tsconfig-session-env.test.ts——后两者在本 PR 足迹之外)已被丢弃,以保证合并提交忠实保持两个父提交的并集且不超出 PR 足迹。该格式化漂移是仓库范围内早已存在的问题,与 test(core): pin transport retry diagnostics and correct the replay-safety comment #8861 无关。
  • 本轮未新增任何守卫、分支或行为(仅注释加上采用 main 既有代码的合并),因此不适用变异探针(mutation probe)。
  • 仅追加式历史:单个合并提交 c87e5110d8,父提交为 6be3799cbe(PR 先前顶端)与 22bb5e8b9f(origin/main)。

验证

  • git merge origin/main — 一处冲突(geminiChat.ts),按上述方式解决
  • npx vitest run src/core/geminiChat.test.ts(packages/core)— 370 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npm run build — 通过
  • git status --short — 合并提交后工作区干净,无计划外文件
  • 合并后一致性检查:解决后的文件相对 origin/main 仅有两处注释修正;agent-core.ts / config-session-env.test.ts 与 origin/main 完全一致

🧭 Gate advisory — this round modified areas outside the PR footprint (machine-measured, not agent-authored):

  • docs
    Review the expansion deliberately; the footprint gate is in advisory mode. · 本轮改动了 PR 足迹之外的区域(门自动测量,非 agent 文本),当前足迹门为 advisory 模式,请有意识地审阅该扩张。

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 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.

No blocking issues. LGTM! ✅

Not explored to full depth (tool budget reached): "agent 3b": executed the new/modified transport-retry tests to confirm they pass (worktree has no node_modules; npm ci + build not run).

Test Plan (not a blocker): src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 21249, 1689, 23736, 1658, 601, 4227, 626 passed.

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

  • packages/core/src/core/geminiChat.test.ts:9270 — [probe] R5-1 (round-5 ledger, still stands): yieldedNonContentChunks pins leave the per-attempt→cross-attempt flag-swap mutation alive
  • packages/core/src/core/geminiChat.test.ts:8970 — [review] socketCut dedup sweep missed this test — sole remaining inline UND_ERR_SOCKET copy
  • packages/core/src/core/geminiChat.test.ts:10244 — [review] inline drain-and-catch IIFE survives beside the new drainCollecting helper in an edited test
中文说明

无阻断问题。LGTM!✅

未探索到全部深度(达到工具调用预算):"agent 3b"executed the new/modified transport-retry tests to confirm they pass (worktree has no node_modules; npm ci + build not run)

Test Plan(非阻断):src/core/geminiChat.test.tsno such file or directory; 300 passed — this review observed 21249, 1689, 23736, 1658, 601, 4227, 626 passed

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

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

@wenshao
wenshao added this pull request to the merge queue Aug 25, 2026
Merged via the queue into QwenLM:main with commit 4a492bc Aug 25, 2026
58 checks passed
@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