fix(serve): surface provider error detail in turn_error messages - #10569
Conversation
Daemon turns that failed because the model provider rejected the request published a generic "Internal error" as the turn_error message, hiding the upstream reason (e.g. engine_overloaded_error) from the Web Shell. Teach the bridge's error-message extractor to read the nested data.error shape the ACP SDK produces when the agent's error message is itself a JSON string, with the same precedence the desktop client uses. Fixes QwenLM#10564
E2E test reportBaseline (before the fix) — the new checks fail by showing the gap:
After the fix (verified on the PR branch, which is based on current
Pre-existing failures observed while validating (not caused by this PR):
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 18 passed — this review observed 1822 passed; 1684 passed — this review observed 1822 passed.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally。
Test Plan(非阻断):18 passed — this review observed 1822 passed; 1684 passed — this review observed 1822 passed。
— qwen3.8-max via Qwen Code /review (v0.22.3)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Addressed review round 1 in c9d1a8b.
Verification: |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): 18 passed — this review observed 1825, 1721, 481, 5178, 94 passed; 1684 passed — this review observed 1825, 1721, 481, 5178, 94 passed.
中文说明
Test Plan(非阻断):18 passed — this review observed 1825, 1721, 481, 5178, 94 passed; 1684 passed — this review observed 1825, 1721, 481, 5178, 94 passed。
— qwen3.8-max via Qwen Code /review (v0.22.3)
|
@qwen-code /triage |
chiga0
left a comment
There was a problem hiding this comment.
Tier: Standard — 7 files, daemon bridge bug fix (+385/−11).
CI — HEAD c9d1a8b7: ✅ Test (ubuntu-latest) · ✅ Integration Tests (no-AK, No Sandbox) · ✅ web-shell E2E Smoke · ✅ Desktop Shell (ubuntu / windows). The previous "CI failing" comment was against an earlier commit; current HEAD is green.
What I checked:
Core fix (extractJsonRpcErrorDetail / extractNestedErrorDetail in bridge.ts):
- Precedence is preserved:
data.details→data.message→data.error(new) → fallback toerr.message. Top-level fields continue to win over the new nested branch. - Empty-string guards:
error.length > 0andmessage.length > 0inextractNestedErrorDetailprevent empty strings from leaking through. Confirmed by the "empty string data.error" and "empty string data.error.message" test cases that now exercise theRequestErrorpath. - Null safety:
error !== nullcheck inextractNestedErrorDetailcorrectly rejects null. Arrays also fall through safely ((array as Record)['message']isundefined, returningundefined→ caller falls toerr.message). classifyTurnErrorKind: exact-matchesterminated, which arrives viadata.details(higher priority than the new branch). The new code cannot affect that classification. ✓- Eight call sites: all are display/log/event surfaces; no behavioral check on the extracted message beyond
terminated. ✓
Test coverage:
- Unit: nested
error.message, stringerror, precedence over top-level keys, no-usable-string fallback, empty stringerror, empty stringerror.message. All branches covered. - Integration: fake OpenAI server emits a single
error_finishchunk with a JSON provider error body; asserts the session SSEturn_error.messagecarries the provider text and that exactly one model request was made (no retry on code-less body). - fake-openai-server self-test: pins the same-chunk invariant (
error_finishanddelta.contenton the same SSE frame).
Cross-check vs. existing reviews — all addressed:
- R1-1 (design-doc call-site count wrong): fixed ✓ — design now lists all 8 call sites including the two
model_switch_failedpublishers, and correctly distinguishes the separatetranscript-replay.tslocal helper. - R1-2 (empty-string guards untested): fixed ✓ — two
RequestError-based test cases added in HEAD. - R1-3 (integration tests outside npm workspaces): no code change needed — CI job
99287677995ran and passed bothfake-openai-server.test.tsandcli/qwen-serve-streaming.test.ts.
One Suggestion from my audit (not a blocker): extractNestedErrorDetail does not explicitly check !Array.isArray(error) the way extractJsonRpcErrorDetail checks the outer data. An array input would reach (array as Record)['message'], get undefined, and return undefined — correct behavior, but inconsistent with the surrounding pattern. Worth aligning if the function is extended.
No blockers.
Approving.
|
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: 38 passed · 0 failed · 38 total Flakiness gate: 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:38 通过 · 0 失败 · 38 总计 抖动门: Verification reportPR #10569 — fix(serve): surface provider error detail in turn_error messagesVerdict: 中文摘要
Central claim and A/B proofCentral claim: when a daemon-hosted turn fails because the gateway reports an upstream rejection as an The claim chain was verified link-by-link from third-party and repo source before the A/B:
The control design reverts the hunk in place rather than in a base worktree, so every Reviewer Test Plan walkthrough: step 1 (unit command) — head green, base red, as the plan predicts; step 2 (build+bundle) — executed for both arms; step 3 (integration command) — head green, base red with the plan's exact Mutation / vacuity matrix
Single mechanism, so no combination row applies. The 7 new tests are not vacuous: the revert fails the intended assertions with the behavioral mismatch the tests exist to catch (not an import/compile break). Shape ladder (compiled dist, wired to the real SDK)
Sibling sweep: the extractor's 8 call sites match the design note's enumeration exactly (turn_error broadcast, 2 quarantine logs, 2 FindingsNo blocking findings. Non-blocking observations, for the record:
Not covered
MethodologyCI merge-ref checkout ( Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
Thanks for the PR! Template looks good ✓ Problem: observed bug, well evidenced. Linked issue #10564 reports four real turn failures in one session all displayed as bare Direction: clearly aligned — this is error-message fidelity on the daemon path, and the merged sibling #10571 just did the same for the SDK surface. Claude Code's changelog shows steady investment in exactly this area (e.g. client-generated error messages rendering with their real detail instead of generic text), though no direct Size: small and appropriately scoped — 15 production lines in Approach: right-sized. The alternative (a new additive Risk: no elevated risk signals — no high-risk paths matched (checked against the revert-correlated file list), and all eight Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测到的 bug,证据充分。关联 issue #10564 记录了同一会话中 4 次真实的 turn 失败,UI 全部显示笼统的 方向:明确对齐——这是 daemon 路径上的错误消息保真问题,刚合并的姊妹 PR #10571 已在 SDK 面做了同样的事。Claude Code 的 changelog 在同一领域持续投入(如客户端生成的错误消息展示真实详情而非笼统文本),虽然没有 规模:小而聚焦—— 方案:规模合理。替代方案(新增 风险:无升级风险信号——未命中高风险路径(对照 revert 相关文件列表检查),且 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewReviewed against my own baseline proposal (extend the bridge's JSON-RPC detail extractor with the nested The production change is the minimal one.
Test coverage is thorough: seven new unit cases (nested object, plain string, both precedence directions, three fallback edges), a fake-server self-test pinning the same-chunk invariant (error body and No blockers, no convention violations. The two judgment calls the author flagged — no new length bound (parity with the already-unbounded Testing evidence — the PR's own CI (unattended run; PR code not executed here)All
Not verified: the before/after claim that the new integration case fails on the base commit is the author's reported result (their pasted
Sandboxed verification would settle the one open item: 中文说明代码审查以我自己的基线方案(扩展 bridge 的 JSON-RPC 详情提取器以识别 ACP SDK 产生的嵌套 生产改动是最小改动。
测试覆盖充分:7 个新单测用例(嵌套对象、纯字符串、两个优先级方向、三个兜底边界)、一个 fake-server 自测(固定"错误体与 无阻塞项,无规范违规。作者标明的两个取舍——不设新的长度上限(与本就无上限的 测试证据——来自 PR 自身 CI(无人值守运行;此处未执行 PR 代码)审查提交上所有
未验证:新集成用例在基线提交上失败这一 before/after 说法是作者的自述结果(其粘贴的 CI 结果表见上方英文部分的机器可读区域。 沙箱验证可以了结唯一悬而未决的问题: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean across every stage; the only reservation is that the base-red half of the before/after rests on the author's pasted run, which I could only corroborate statically. Stepping back: this is what a well-formed fix looks like. The problem was observed in production, confirmed by independent issue triage, and the root cause is a single missing branch in one extractor — which is exactly what the PR changes, in 15 production lines. My own baseline proposal for this problem was substantively identical; I looked for the cheaper path and didn't find one (the additive-wire-field alternative costs four packages of churn for the same user-visible text, and the design note says so explicitly). The parts that earned my confidence:
Reservations, none blocking:
Approving, pinned to the reviewed commit. ✅ 中文说明置信度:4/5 —— 各阶段均干净;唯一的保留是 before/after 中"基线为红"的一半来自作者粘贴的运行结果,我只能静态佐证。 退一步看:这是一个规范的 fix 该有的样子。问题在生产环境观测到、经 issue 分诊独立确认,根因是一个提取器里缺失的单个分支——而 PR 改的正是这一处,生产代码 15 行。我自己的基线方案与之实质相同;我找过更便宜的路径,没有找到(新增 wire 字段的替代方案要用四个包的改动换同样的用户可见文本,设计文档也明确这么说了)。 让我建立信心的部分:
保留意见(均不阻塞):
批准,锚定在已审查的提交上。✅ — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
One post-review correction to the PR description, for the record — it doesn't change the approval: The description says the nested read uses "the same precedence the desktop client already uses for ACP internal-error data", but I couldn't find that precedent in the client code. The actual client-side extractors — This is a description inaccuracy, not a code issue: the PR's own precedence ( 中文说明审查后对 PR 描述做一处更正,留档备查——不影响已给出的批准: 描述中说嵌套读取采用"desktop 客户端处理 ACP internal-error data 已有的相同优先级",但在客户端代码中找不到这个先例。实际的客户端提取逻辑—— 这是描述不准确,不是代码问题:PR 自身的优先级( — Qwen Code · qwen3.8-max Reviewed at |
|
Released in v0.23.0. |





What this PR does
When a daemon-hosted prompt turn fails because the model provider rejected the request, the daemon's
turn_errorevent now carries the provider's own error message (for exampleThe engine is currently overloaded, please try again later) instead of the generic JSON-RPCInternal error. The Web Shell transcript error block, the live-state summary, refresh replay, and SDK consumers all surface the real reason with no wire schema change.Why it's needed
Today the Web Shell shows a bare
Internal errorfor this whole class of failures. In one observed session a turn failed four times with no visible distinction, while the daemon logs showed the true cause every time — the upstream engine returningengine_overloaded_error. From the user's seat a transient provider overload is indistinguishable from a daemon bug.The detail was present on the wire all along and was dropped at one extraction point. When the agent throws an error whose message is itself a JSON string (a provider error body surfaced as stream content), the ACP SDK's catch-all ships
JSON.parse(message)as the errordata, nesting the provider text atdata.error.message. The daemon's error-message extractor already readdata.detailsanddata.messagebut not that nested shape, so it fell back to the genericInternal error. This PR teaches the extractor the nesteddata.errorshape (a plain string or an object'smessage), with the same precedence the desktop client already uses for ACP internal-error data. A design note rides along underdocs/design/, and theturn_errorrow of the daemon event-schema doc now records the message semantics.Reviewer Test Plan
How to verify
The new integration case reproduces the production failure shape end to end (real daemon + real
qwen --acpchild + a fake OpenAI server that answers with a singleerror_finishchunk whose content is the provider JSON error body): it asserts the session SSEturn_errorcarries the provider text, and that exactly one model request was made (the JSON body carries no numeric code, so no rate-limit retry fires). Unit cases cover the nested-object shape, a plain-stringdata.error, precedence over nested values, and the no-usable-string fallback.What a reviewer should confirm: on the base commit both fail with
Internal errorwhere the provider text is expected; on this branch they pass.Evidence (Before & After)
Before (base commit):
AssertionError: expected 'Internal error' to be 'The engine is currently overloaded, please try again later'— theturn_errorevent message was the generic JSON-RPC text.After (this branch): the integration case passes; the daemon
turn_errorcarriesThe engine is currently overloaded, please try again later. Unit run: 18 passed in theextractErrorMessagedescribe. Fullpackages/acp-bridgesuite: 1684 passed.Tested on
Environment (optional)
Local
npm run build && npm run bundle; integration run withQWEN_SANDBOX=false.Risk & Scope
datacarries a nestederrorstring orerror.messagenow surfaces that text instead of the generic message. That is the intent; every call site of the extractor is a display or log surface (enumerated in the design note), and structured fields (errorKind,code,loopType) are extracted independently and unchanged.engine_overloaded_error-class failures should be retried automatically (tracked separately); the non-daemon TUI error path, which does not traverse this extractor; macOS/Windows runs left to CI.Linked Issues
Closes #10564
中文说明
本 PR 做了什么
当 daemon 托管的 prompt turn 因模型 provider 拒绝请求而失败时,daemon 的
turn_error事件现在携带 provider 自己的错误消息(例如The engine is currently overloaded, please try again later),而不是笼统的 JSON-RPCInternal error。Web Shell transcript 错误块、live-state 摘要、刷新重放和 SDK 消费方都能看到真实原因,且没有 wire schema 变更。为什么需要
目前 Web Shell 对这一类失败只显示
Internal error。在一个真实会话中同一 turn 失败了 4 次且毫无区分度,而 daemon 日志显示每次都是上游引擎返回engine_overloaded_error。用户无法区分"上游暂时过载"和"daemon 出 bug"。详情其实一直在 wire 上,只是在一个提取点被丢弃了。当 agent 抛出的错误消息本身是 JSON 字符串(provider 错误体以流内容形式出现)时,ACP SDK 的兜底逻辑会把
JSON.parse(message)放进错误的data,provider 文本被套在data.error.message。daemon 的错误消息提取逻辑此前读取data.details和data.message,但不读这个嵌套形状,于是回退到笼统的Internal error。本 PR 让提取逻辑认识嵌套的data.error形状(纯字符串或对象的message),优先级与 desktop 客户端处理 ACP internal-error data 的既有顺序一致。设计说明随 PR 提交在docs/design/下,daemon 事件 schema 文档的turn_error行也补充了 message 语义。评审者测试计划
如何验证
新增集成用例端到端复现生产故障形状(真实 daemon + 真实
qwen --acp子进程 + 返回单个error_finishchunk(内容为 provider JSON 错误体)的 fake OpenAI server):断言会话 SSE 的turn_error携带 provider 文本,且只产生一次模型请求(JSON 错误体没有数字错误码,不会触发限流重试)。单元用例覆盖嵌套对象、纯字符串data.error、顶层优先级和不可用兜底。证据(Before & After)
修复前(基线提交):
AssertionError: expected 'Internal error' to be 'The engine is currently overloaded, please try again later'。修复后(本分支):集成用例通过,turn_error携带 provider 文案;extractErrorMessage单测 18 个全部通过;packages/acp-bridge全量 1684 个测试通过。风险与范围
data中带有嵌套error字符串或error.message的 agent 错误,现在会展示该文本而非笼统消息。这正是目的;提取器的所有调用点都是展示或日志面(设计文档中已枚举),结构化字段(errorKind、code、loopType)的提取路径独立且未变。engine_overloaded_error类失败是否应自动重试(另行跟踪);非 daemon 的 TUI 错误路径不经过此提取器;macOS/Windows 交给 CI。关联 Issue
Closes #10564