fix(core): allow transport stream retry during the thinking-only phase - #7938
Conversation
The transport stream retry was gated on !streamYieldedChunk, which any thinking/reasoning chunk trips within seconds for thinking models. A gateway closing the long-lived SSE connection minutes into the thinking phase (UND_ERR_SOCKET) then propagated as a fatal error even though zero user-visible content had been produced — making large generations in YOLO/headless mode effectively impossible (QwenLM#7832). Track a separate streamYieldedContentChunk that is only set when a chunk carries a non-thought candidate part (text, functionCall, …) and gate the replay on it instead. Thought parts are ephemeral — never recorded as the assistant's response in history — so replaying after thinking-only output cannot duplicate visible content. Content-phase failures keep the existing behavior: once a non-thought part has flowed, the retry stays blocked and the error propagates. The not-taken log now reports skipped_after_content, and the retry log gains yieldedNonContentChunks so thinking-phase replays are visible in diagnostics. Fixes QwenLM#7832
|
@qwen-code /takeover |
1 similar comment
|
@qwen-code /takeover |
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR! Template looks good ✓ — all substantive sections are present and thorough. Problem: Observed bug with strong evidence. Issue #7832 is a P1 with a 5/5 reproduction matrix — YOLO mode large code generation consistently fails with Direction: Aligned. Transport stream retry is core reliability infrastructure, and thinking models are the primary use case for qwen-code. The retry was already classified as safe ( Size: Core paths touched ( Approach: The scope is tight — one new flag, one helper, one gate condition change, plus log field updates. No unrelated changes, no drive-by refactors. The PR explicitly defers the issue's suggestion (2) (partial-output continuation) as out of scope, which is the right call. I don't see a simpler path: distinguishing thought-only output from visible content is exactly the right granularity for the retry gate. Risk: Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 所有实质性章节齐全且详尽。 问题: 已观测到的 bug,证据充分。Issue #7832 是 P1 级别,有 5/5 的复现矩阵 —— YOLO 模式下大型代码生成在思考阶段因 DashScope 网关关闭空闲 SSE 连接而持续失败( 方向: 对齐。传输流重试是核心可靠性基础设施,思考模型是 qwen-code 的主要使用场景。重试已被分类为安全( 规模: 触及核心路径( 方案: 范围紧凑 —— 一个新标志、一个辅助函数、一个门控条件变更,加上日志字段更新。无无关改动,无顺手重构。PR 明确将 issue 的建议 (2)(部分输出续传)标记为超出范围,这是正确的决定。 风险: 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code ReviewIndependent proposal: Given the problem (retry gate trips on thinking chunks), I would add a second boolean that only tracks non-thought candidate parts, swap the gate condition to use it, and add a small helper to keep the check readable. The thought/visible-content distinction already exists in the codebase ( Comparison with the diff: The PR does exactly this, and does it cleanly.
No correctness bugs, no security concerns, no convention violations. The tests are well-structured: the thinking-retry test uses fake timers and asserts the full recovery path (2 API calls, 1 RETRY event, recovered content), and the thinking-then-content test pins the accumulation invariant (1 API call, 0 RETRY). The mutation testing described in the PR body (reverting the gate fails exactly 1/262; neutering the content check fails 2/262) is a good signal that the tests are load-bearing. No findings. ✅ TestingCI evidence (fetched via API for Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The ubuntu unit test suite is still running. macOS/Windows tests and integration tests were skipped (likely gated on the ubuntu result). The Real-scenario testing: N/A — this is a CI run ( 中文说明代码审查独立方案: 给定问题(重试门控在思考 chunk 上误触发),我会添加第二个布尔值仅跟踪非思考候选部分,将门控条件切换为使用它,并添加一个小辅助函数保持检查的可读性。思考/可见内容的区分在代码库中已经存在( 与 diff 的比较: PR 完全这样做了,而且做得很干净。
无正确性 bug,无安全问题,无规范违反。测试结构良好,mutation testing 信号良好。 无发现 ✅ 测试CI 证据:ubuntu 单元测试仍在运行中。macOS/Windows 测试和集成测试被跳过。 真实场景测试:不适用 —— 这是 CI 运行。作者无写入权限,沙箱通道不可用。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 5/5 — Clean across every stage; would merge without hesitation once CI lands. This is a textbook minimal fix for a real, well-evidenced problem. The PR does one thing — relaxes the transport retry gate from "no chunks yielded" to "no user-visible content yielded" — and does it with a single flag, a small helper that formalizes an existing invariant, and two well-targeted tests. The thought/visible-content distinction was already load-bearing in the history-recording path ( The independent proposal I wrote before reading the diff was essentially identical to what the PR does. I didn't find a simpler path. Every line in the diff earns its place — no drive-by changes, no scope creep, and the explicit deferral of partial-output continuation (suggestion 2 from the issue) is the right scoping call. The high-risk path flag on Approval deferred until CI lands green on 中文说明置信度:5/5 —— 每个阶段都很干净;CI 通过后会毫不犹豫地合并。 这是一个针对真实的、有充分证据的问题的教科书式最小修复。PR 只做一件事 —— 将传输重试门控从"未产生任何 chunk"放宽为"未产生用户可见内容" —— 用一个标志、一个将现有不变量形式化的小辅助函数,以及两个精准测试来完成。思考/可见内容的区分在历史记录路径中已经是承重的( diff 中的每一行都有其存在价值 —— 无顺手改动,无范围蔓延。高风险路径标记是合理的一般性信号,但这个具体变更风险很低:它在一个明确定义的类别(仅思考输出)上扩展了现有的重试窗口,没有触及重试机制本身、错误分类或内容管道。 批准推迟到 CI 在 — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
|
🤝 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. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No action takenThis review round contained no actionable feedback:
No code changes were made. The PR is ready for human review. 中文说明未采取任何操作本轮审查没有包含可操作的反馈:
未进行任何代码变更。该 PR 已准备好接受人工审查。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
Maintainer verification — built and tested locallyI re-verified this PR at head Verdict: merge-ready. Two non-blocking nits at the end. 1. The gap I wanted to close: are the fixtures real?The PR's own tests feed hand-written So I wrote an independent harness that removes the mock entirely: a local HTTP server speaking OpenAI-compatible SSE with genuine The path holds up:
Scenario A is the reporter's bug, reproduced and then fixed end-to-end on the real transport. Scenario B is the important one for confidence: content-phase behavior is proven unchanged rather than merely asserted. On the safety argument specifically — I checked the recorded history after the replay rather than trusting the rationale: Attempt 1's thinking is absent from history, and the answer occurs exactly once. No duplication, in the UI stream or in persisted history. Scenario C is worth calling out because the PR flags it as its own riskiest edge. It behaves safely: replay is now permitted after a usage-only chunk, and the recovered output is emitted exactly once with no stale thinking carried over. 2. Are the new tests load-bearing?Seven mutations, each proven to actually land (no-op edits rejected — a silent no-op would masquerade as a survivor), suite re-run, file restored.
M1 and M2 reproduce the PR's own stated mutation numbers exactly (1/262 and 2/262) — the self-report checks out. M4 is not a test gap: Independent of the mutation matrix, the single-variable A/B — the PR's test file run against BASE source, the PR touching only these two files — fails exactly 1 of 262, and it fails with 3. Regression and gates (PR arm)
Two local-environment items, both A/A-controlled so you can discount them: 4. Two non-blocking findings(a) The stated rationale is slightly imprecise. The PR says thought parts "are never recorded as the assistant's final response in history." Empirically they are recorded — the successful attempt's (b) The two diagnostic changes are untested (M6, M7). The PR body presents Neither blocks. The fix is minimal, load-bearing on the real transport path, and the content-phase guard is genuinely intact. Good PR. 中文说明维护者验证 —— 本地真实构建与测试我在两个隔离的 worktree 中对 head 结论:可以合并。 文末有两个不阻塞的小问题。 1. 我想堵住的缺口:fixture 是真实的吗?PR 自带的测试是把手写的 因此我写了一个独立的测试装置,彻底去掉 mock:一个本地 HTTP 服务器用 OpenAI 兼容 SSE 发送真实的 这条路径成立:
场景 A 就是报告者的 bug,在真实传输层被复现并修复。场景 B 对信心更关键:内容阶段的行为是被证明未变,而非仅仅被断言未变。 关于安全性论证,我没有采信说法,而是检查了 replay 之后真实记录的 history:attempt 1 的思考内容不在 history 中,答案恰好出现一次。无论 UI 流还是持久化 history 都没有重复。 场景 C 值得单独说明,因为 PR 自己把它标为最大风险点。实测是安全的:usage-only chunk 之后现在允许 replay,恢复的输出恰好发出一次,且没有残留旧的思考内容。 2. 新增测试是否承重?七个变异,每个都验证过确实生效(拒绝空改动 —— 一个静默的 no-op 会伪装成"存活"),重跑测试后还原文件。
M1 与 M2 精确复现了 PR 自述的变异数据(1/262 与 2/262),自我报告可信。 M4 不是测试缺口: 此外,单变量 A/B(用 PR 的测试文件跑 BASE 源码;PR 只改了这两个文件)恰好失败 1/262,且失败信息为 3. 回归与门禁(PR 侧)
两处本地环境问题,均已做 A/A 对照可排除: 4. 两个不阻塞的问题(a) 论证表述略有偏差。 PR 说 thought part「从不作为 assistant 的最终响应记录进 history」。实测它们确实被记录 —— 成功那次的 (b) 两处诊断改动无测试覆盖(M6、M7)。PR 正文把 两点都不阻塞。修复足够小、在真实传输路径上确实承重、内容阶段的保护完好。是个不错的 PR。 |
|
Released in v0.21.2. |
…e replay Merge main into fix/mid-stream-transport-continuation. Main's QwenLM#7938 split the transport retry gate into a from-zero replay for thinking-only output (!streamYieldedContentChunk); this PR adds a continuation retry for text/thought partials (streamYieldedChunk + pendingPartialTextParts). The replay block runs first and continues, so thinking-only failures replay from zero while content-phase failures continue. The not-taken log keeps main's skipped_after_content discriminator.
After the merge with QwenLM#7938, a transport failure after thinking chunks plus visible content takes the continuation path (blocked from replaying from zero), so the old no-retry assertion contradicts the merged behavior. Assert the continuation instead: one retry marked isContinuation and the coalesced visible text preserved.
|
@wenshao Thank you for the verification — the real-wire differential (a genuine OpenAI-compatible SSE server emitting real Both findings are now addressed in follow-up #8861 (the fixes were ready the same day, but lost the race to the merge by about an hour): (a) Both comment sites now state the actual invariant: the successful attempt's thoughts are recorded in history; the replay is safe because the failed attempt's accumulated partial turn is discarded wholesale ( (b) Both diagnostics are pinned: On M4: your equivalence proof matches my reading — 中文@wenshao 感谢验证——真实链路差分(真正的 OpenAI 兼容 SSE 服务器发送真实 两个 finding 已在后续 PR #8861 落地(修复当天就已完成,但比合并晚了约一小时): (a) 两处注释现在陈述真正的不变量:成功 attempt 的 thought 会进入 history;重放之所以安全,是因为失败 attempt 累积的 partial turn 会被整体丢弃( (b) 两个诊断字段均已钉住: 关于 M4:你的等价性证明与我的理解一致—— |
…fety comment (QwenLM#8861) * test(core): pin transport retry diagnostics and correct the replay-safety 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. * test(core): close the review's four gaps on the retry diagnostics 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. * test(core): unify socket-cut fixtures and pin retry exhaustion with thinking (QwenLM#8861) * docs(core): ground thinking-replay safety in the partial-turn discard (QwenLM#8861) * fix(core): ground replay rationale and pin retry-decision ternary (QwenLM#8861) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: ComplexSimply <rudy.arrowsong@gmail.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>


What
Implements the thinking-phase retry refinement scoped by @doudouOUC in #7832 (comment) (the
welcome-prdirection): track a separatestreamYieldedContentChunkthat is only set when a chunk carries a non-thought candidate part (text,functionCall,inlineData, …), and gate the transport stream replay on it instead ofstreamYieldedChunk.hasNonThoughtCandidateParts()next toisToolCallPreparationOnly()documents the invariant the gate relies on.retryDecisionbecomesskipped_after_content(wasskipped_after_chunk; single producer, no other consumers — verified by grep), and the retry-scheduled log gainsyieldedNonContentChunksso thinking-phase replays are visible in diagnostics.Why
streamYieldedChunktrips on the first thinking chunk — within seconds for thinking models — while the model can then spend minutes reasoning. That is exactly when gateways close long-lived SSE connections (UND_ERR_SOCKET, reporter's 5/5 repro; same server-side cause as #7831). The retry was already classified as safe (UND_ERR_SOCKET∈RETRYABLE_STREAM_TRANSPORT_CODES) but locked out even though zero user-visible content existed.Replaying after thinking-only output is safe because thought parts are ephemeral — they are never recorded as the assistant's final response in history (
stripThoughtPartsFromContentand the model-parts filter droppart.thoughtbefore history is recorded), so a replay cannot duplicate visible output. The UI does re-render the new attempt's thinking, which is the same behavior as the existing pre-chunk retry path.Reviewer Test Plan
vitest run src/core/geminiChat.test.ts→ 262 passing. Fullsrc/core/slice: 53 files / 2492 passing;src/utils/slice: 117 files / 4117 passing.tsc --noEmit,eslint --max-warnings 0,prettier --checkclean.{ thought: true }part then throwsUND_ERR_SOCKET; asserts 2 API calls, exactly 1 RETRY event, and the recovered content arrives.!streamYieldedChunkfails exactly the thinking-retry test (1/262); neutering the content check fails exactly the two blocking tests (2/262).Not reproduced against a live DashScope gateway: the failure needs a server-side idle close minutes into a real SSE stream. The deterministic unit tests above pin the decision logic itself; the reporter's repro matrix covers the environment side.
Risk & Scope
One flag + one gate condition + log fields in
sendMessageStream's retry loop; no API or event-shape changes. The riskiest edge — a chunk with no parts (usage-only / finishReason-only) — now allows replay where it previously blocked; both are pre-content states where replay is safe for the same history-based reason.Linked Issues
Fixes #7832. Related: #7831 (same gateway idle-close, higher-context variant). The issue's suggestion (2) — partial-output continuation for content-phase failures — is explicitly out of scope per the triage discussion; it needs API-level continuation semantics and a design doc.