Skip to content

fix(sdk): Surface daemon JSON-RPC error details - #10571

Merged
doudouOUC merged 3 commits into
QwenLM:mainfrom
doudouOUC:fix/10570-daemon-http-error-details
Aug 31, 2026
Merged

fix(sdk): Surface daemon JSON-RPC error details#10571
doudouOUC merged 3 commits into
QwenLM:mainfrom
doudouOUC:fix/10570-daemon-http-error-details

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

When a daemon HTTP 5xx response uses the opaque JSON-RPC message Internal error, the TypeScript SDK now uses the first non-empty string available from data.details, data.message, or string data. The original HTTP status and parsed response body remain unchanged, and existing top-level error fallbacks are preserved.

Why it's needed

A failed Web Shell model switch can already carry the actionable provider reason in its HTTP response body, but the SDK previously discarded that reason while building DaemonHttpError.message. Users therefore saw only Set model failed: POST /session/:id/model: Internal error, which made configuration and provider failures indistinguishable from a daemon defect. The extraction is deliberately limited to 5xx responses with the exact generic top-level error and a numeric JSON-RPC code so specific errors, client errors, and non-JSON-RPC responses retain their existing display behavior.

Issue #10564 is related but covers the separate failed-turn event path and nested data.error.message; this PR fixes the daemon HTTP client path described by #10570.

Reviewer Test Plan

How to verify

Send setSessionModel a mocked HTTP 500 response shaped as { error: "Internal error", code: -32603, data: { details: "Missing credentials" } } and confirm the resulting DaemonHttpError.message ends in Missing credentials while its status remains 500 and its body remains unchanged. Repeat with data.message and string data. Then confirm that an empty or non-string detail, a specific top-level error, a non-numeric code, and an HTTP 400 response all retain the previous top-level message.

Automated verification completed with the full DaemonClient.test.ts suite (385 passed), focused setSessionModel tests on the latest main (11 passed), SDK type checking, SDK build, and git diff --check.

Evidence (Before & After)

Before: POST /session/:id/model: Internal error

After: POST /session/:id/model: Missing credentials

These messages were captured by a deterministic in-memory SDK reproduction using the exact origin/main source and the same mocked daemon response. A live provider configuration was not mutated for reproduction.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Node.js 22.22.3 on macOS; Qwen Code 0.22.3 Web Shell baseline; SDK tests use a mocked fetch response.

Risk & Scope

  • Main risk or tradeoff: The shared SDK HTTP error formatter now exposes string diagnostic data for every matching daemon 5xx JSON-RPC response, not only model switching. The exact top-level error, HTTP status, numeric-code, and string-value gates limit that expansion, and boundary tests cover each gate.
  • Not validated / out of scope: A live Web Shell provider-failure run was not performed because it would require mutating provider credentials. Duplicate HTTP/SSE notices, the separate [object Object] toast, nested data.error.message turn failures from bug(serve): Web Shell shows generic "Internal error" for failed turns, hiding the provider's actual error message #10564, and automatic retries are out of scope. The repository root build remains blocked in this worktree by the pre-existing local Ink type mismatch (selectable and selection frame exports); the affected SDK package build and type check pass.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #10570

Related: #10564 covers a different failed-turn event path and is not fixed by this PR.

中文说明

本 PR 做了什么

当 daemon HTTP 5xx 响应使用不透明的 JSON-RPC 消息 Internal error 时,TypeScript SDK 现在会依次使用 data.detailsdata.message 或字符串形式 data 中第一个非空字符串。原始 HTTP 状态码和解析后的响应体保持不变,现有顶层错误回退行为也予以保留。

为什么需要这个改动

Web Shell 模型切换失败时,HTTP 响应体中可能已经带有可操作的 provider 原因,但 SDK 之前在构造 DaemonHttpError.message 时丢弃了该原因。用户因此只能看到 Set model failed: POST /session/:id/model: Internal error,无法区分配置或 provider 故障与 daemon 自身缺陷。提取逻辑被有意限制为:HTTP 5xx、顶层错误严格等于通用 Internal error、且 JSON-RPC code 为数值;因此具体错误、客户端错误和非 JSON-RPC 响应仍保持原有展示行为。

Issue #10564 与此相关,但它处理的是另一条失败 turn 事件链路以及嵌套的 data.error.message;本 PR 修复的是 #10570 描述的 daemon HTTP 客户端链路。

Reviewer 测试计划

如何验证

setSessionModel 接收一个形如 { error: "Internal error", code: -32603, data: { details: "Missing credentials" } } 的模拟 HTTP 500 响应,确认生成的 DaemonHttpError.messageMissing credentials 结尾,同时状态码仍为 500、响应体保持不变。再分别验证 data.message 和字符串形式 data。随后确认空或非字符串详情、具体顶层错误、非数值 code 和 HTTP 400 响应都继续使用原有顶层消息。

自动化验证已完成:完整 DaemonClient.test.ts 测试套件通过 385 项;在最新 main 上聚焦的 setSessionModel 测试通过 11 项;SDK 类型检查、SDK 构建和 git diff --check 均通过。

证据(改动前后)

改动前:POST /session/:id/model: Internal error

改动后:POST /session/:id/model: Missing credentials

这些消息来自确定性的内存 SDK 复现:分别加载精确的 origin/main 源码和当前改动,并使用相同的模拟 daemon 响应。复现过程中没有修改真实 provider 配置。

测试平台

操作系统 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ⚠️ 未测试

环境(可选)

macOS 上的 Node.js 22.22.3;Qwen Code 0.22.3 Web Shell 基线;SDK 测试使用模拟 fetch 响应。

风险与范围

  • 主要风险或取舍:共享的 SDK HTTP 错误格式化逻辑现在会为所有匹配的 daemon 5xx JSON-RPC 响应展示字符串诊断数据,而不只影响模型切换。顶层错误精确匹配、HTTP 状态、数值 code 和字符串值四道门槛限制了影响范围,边界测试覆盖了每一道门槛。
  • 未验证 / 超出范围:没有执行真实 Web Shell provider 故障,因为这需要修改 provider 凭据。重复的 HTTP/SSE 通知、单独的 [object Object] toast、bug(serve): Web Shell shows generic "Internal error" for failed turns, hiding the provider's actual error message #10564 中嵌套 data.error.message 的 turn 失败以及自动重试均不在本 PR 范围内。该 worktree 的仓库根构建仍被既有的本地 Ink 类型不匹配阻断(缺失 selectable 和 selection frame exports);受影响 SDK 包的构建和类型检查均通过。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

修复 #10570

相关:#10564 处理另一条失败 turn 事件链路,本 PR 不修复该问题。

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

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run against 394c4d745e — the two commits since the last pass (3241aae, 394c4d7) are both test-only responses to the two review rounds; the production change is unchanged.

Template looks good ✓

Problem: observed bug with evidence — linked issue #10570 documents the opaque Set model failed: POST /session/:id/model: Internal error message, with a deterministic reproduction against origin/main (a mocked daemon 500 response pushed through setSessionModel), originally seen on a local Web Shell with a Team API provider configuration.

Direction: aligned. The actionable reason (e.g. missing credentials) is already present in the HTTP response body; the SDK just discards it while building the display message. CHANGELOG has no direct reference to JSON-RPC detail surfacing, but this is a straightforward internal bugfix and the area is clearly relevant.

Size: not applicable — packages/sdk-typescript/src/daemon/ is not a protected core path. Small diff: 31 production lines (28 additions / 3 deletions) plus 168 test lines.

Approach: scope still right. The unwrap is confined to the exact shape the issue describes (5xx + top-level "Internal error" + numeric JSON-RPC code) inside failOnError, and preserves status and parsed body. Both follow-up pushes added boundary tests only — no scope creep. The sibling XHR/SSE formatting sites remain intentionally untouched (#10564 owns the event path).

Risk: no elevated risk signals — no high-risk path hits.

Moving on to code review. 🔍

中文说明

针对 394c4d745e 重新运行 —— 上次通过后的两个提交(3241aae394c4d7)均为回应两轮 review 的纯测试提交,生产代码改动未变。

模板完整 ✓

问题:已观测到的 bug,且有证据 —— 关联 issue #10570 记录了不透明的 Set model failed: POST /session/:id/model: Internal error 消息,并提供了针对 origin/main 的确定性复现(通过 setSessionModel 发送 mock 的 daemon 500 响应),最初见于使用 Team API provider 配置的本地 Web Shell。

方向:对齐。可操作的原因(如缺少凭据)本就在 HTTP 响应体中,SDK 在构建展示消息时把它丢掉了。CHANGELOG 中没有 JSON-RPC detail 透出的直接条目,但这是一个直接的内部 bugfix,领域明显相关。

规模:不适用 —— packages/sdk-typescript/src/daemon/ 不在核心保护路径内。diff 很小:生产代码 31 行(28 增 / 3 删),测试 168 行。

方案:范围依然合理。unwrap 限定在 issue 描述的确切形状(5xx + 顶层 "Internal error" + 数字 JSON-RPC code),位于 failOnError 内,保留 status 与解析后的 body。后续两次推送只增加了边界测试,无范围蔓延。相邻的 XHR/SSE 格式化位置仍有意未改(事件路径归 #10564)。

风险:无升级风险信号 —— 未命中高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 394c4d745edd51a6834d9a8fbbb571bb9b156154 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run against 394c4d745e. The production code is unchanged from the commit the last pass reviewed — both pushes since (test(sdk): address PR review feedback, test(sdk): cover HTTP error detail boundaries) add tests only, responding to the two review rounds.

Code review

Independent proposal, re-derived before reading the diff: the unwrap belongs in failOnError — the single site that builds these DaemonHttpErrors — gated to the exact opaque shape (5xx + top-level "Internal error" + numeric JSON-RPC code), taking the first non-empty string from string data / data.details / data.message, leaving status/body untouched, one test per gate. The PR does exactly this; I found no simpler path it missed.

Re-verified against the base tree:

  • The gate is exact and defensive: an Array.isArray guard on the body, a strict string match on "Internal error", typeof code === 'number', and a non-empty string check before every substitution. No any, and nothing changes outside the gated branch.
  • status and body are untouched (the tests pin both), and the 503 prompt_queue_full branch below reads body directly and is unaffected.
  • Consumers checked again on current main: standalone-sessions.ts, DaemonAuthFlow.ts, DaemonClient.ts, DaemonSessionClient.ts, and DaemonHttpError.ts all branch on status or body.code — nothing branches on .message text, so the change is display-only by construction.
  • New tests since the last pass: twelve parametrized boundary cases — details precedence over message, fallback from empty details/message/string data, non-string details, a specific top-level error, a non-numeric code, a non-internal numeric code (-32000 still unwraps — the gate is "numeric", matching the issue text), null data, and a 400 client error. Every case pins status + body + exact message.

Standing observation, unchanged and non-blocking: the same top-level-only formatting still lives at the two sibling sites this PR deliberately leaves alone — the XHR workspace-upload path in DaemonClient.ts (~L2112) and the SSE connect-error paths in RestSseTransport.ts (~L183, ~L199). That is consistent with the issue's scoping; a shared helper unifying them would be a natural follow-up if maintainers want it.

Testing

Unattended CI run — the evidence here is the PR's own CI on the reviewed commit, fetched through the API; no PR code was built or executed locally.

Everything substantive is green on 394c4d745e after the CI re-run: the unit suite, the no-AK integration run, both desktop shells, and the security checks. The initial run's Integration Tests failure (flagged by the earlier review round) was re-run and passed. One caveat: web-shell E2E Smoke shows cancelled on the re-run attempt, but it passed on the first attempt for this exact commit (22:35–22:42 UTC); the retry died mid browser-smoke step on the shared-runner pool — an infrastructure interruption, not a PR defect (a display-only SDK message change cannot fail a browser smoke that already succeeded on the same SHA, and main's recent CI serialization commit exists precisely because these shared runners contend). The main ruleset carries no required status checks.

Check Conclusion
web-shell E2E Smoke (ubuntu-latest, Node 22.x) 🚫 cancelled (re-run attempt; ✅ success on the first attempt for this commit)
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success

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

Sandboxed verification is already in flight on this head — it A/Bs the one claim the unit tests cannot reach: that the unwrapped detail actually surfaces during a live Web Shell model switch, not just in the mocked SDK reproduction. Its report will land in the verification thread on this PR.

中文说明

针对 394c4d745e 重新运行。生产代码与上次审查的提交完全一致 —— 此后两次推送(test(sdk): address PR review feedbacktest(sdk): cover HTTP error detail boundaries)均为回应两轮 review 的纯测试提交。

代码审查:读 diff 前重新独立推导的方案是——把 unwrap 放在 failOnError(构造这些 DaemonHttpError 的唯一位置),限定为确切的不透明形状(5xx + 顶层 "Internal error" + 数字 JSON-RPC code),依次取字符串 data / data.details / data.message 中第一个非空字符串,status/body 保持不变,每个门槛一个测试。PR 的实现与此完全一致,没有找到它遗漏的更简路径。

已在基线代码树上复核:门槛精确且防御性强(Array.isArray 防护、严格匹配 "Internal error"typeof code === 'number'、每次替换前检查非空字符串);无 any,门槛分支之外行为不变;statusbody 未被改动(测试均已固定),下方 503 prompt_queue_full 分支直接读 body、不受影响;在当前 main 上再次核查全部消费方(standalone-sessions.tsDaemonAuthFlow.tsDaemonClient.tsDaemonSessionClient.tsDaemonHttpError.ts)均按 statusbody.code 分支,没有任何调用方依赖 .message 文本——该改动在构造上仅影响展示。上次通过之后新增 12 个参数化边界用例:details 优先于 message、空 details/message/字符串 data 的回退、非字符串 details、具体顶层错误、非数字 code、非 -32603 的数字 code(-32000 仍会 unwrap——门槛是"数字",与 issue 描述一致)、null data、400 客户端错误;每个用例都固定 status + body + 完整消息文本。

一贯的非阻塞观察:同样的"仅取顶层 error"格式仍存在于本 PR 有意未改的两处相邻位置——DaemonClient.ts 的 XHR 工作区上传路径(约 L2112)与 RestSseTransport.ts 的 SSE 连接错误路径(约 L183、L199)。这与 issue 的范围划分一致;若维护者需要,用共享 helper 统一是自然的后续工作。

测试:无人值守 CI 运行 —— 以下证据为被审提交自身 CI 经 API 抓取的结果,未在本地构建或执行任何 PR 代码。重新运行 CI 后,394c4d745e 上所有实质性检查均绿:单测套件、no-AK 集成、两个桌面 shell、安全检查。初次运行中 Integration Tests 的失败(早前 review 轮次曾指出)经重跑已通过。一个说明:web-shell E2E Smoke 在重跑尝试中显示 cancelled,但它在该提交的首次尝试中已经通过(UTC 22:35–22:42);重试是在共享 runner 池的浏览器步骤中途被中断的——属于基础设施问题,而非 PR 缺陷(一个仅影响展示文案的 SDK 改动不可能让一个在同一 SHA 上已通过的浏览器冒烟失败,main 上近期的 CI 串行化提交正是因为这些共享 runner 存在竞争)。main 的规则集未设置必需状态检查。端到端部分(Web Shell 实际切换模型时细节是否真的显示)由已在途的沙箱验证覆盖,报告会发布在本 PR 的验证线程中。

Qwen Code · qwen3.8-max

Reviewed at 394c4d745edd51a6834d9a8fbbb571bb9b156154 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — same read as the last pass, reinforced: a small, precisely gated fix whose production change was already reviewed clean, and whose two follow-up pushes added nothing but boundary tests pinning every gate. Remaining nits are unchanged — the sibling XHR/SSE formatting sites are intentionally left alone, and the live Web Shell leg rests on the mocked reproduction until the in-flight sandbox verification reports.

Reflection: I re-derived the approach independently and the PR still matches it — same location (failOnError), same gates, no simpler path missed. What changed since the last pass is test coverage only: the author responded to both review rounds with test-only commits, now thirteen cases pinning the exact message, status, and body across every gate, including precedence, empty-value fallbacks, non-internal numeric codes, and null data. No consumer anywhere in the SDK branches on .message text, so the blast radius is display-only by construction. CI is green on this commit — the one cancelled check passed on the first attempt for this same SHA and died mid-retry on shared-runner infrastructure. Across two review rounds the author was precise and honest about scope (declining the two out-of-scope expansions with reasons rather than silently ballooning the diff), and the PR stays exactly where the issue drew the line. Approving; the sandbox verification report will land separately and reads as confirmation, not a gate.

中文说明

置信度:4/5 —— 与上次结论一致且更有把握:一个小而精确、门槛严格的修复,生产代码在上轮审查中已确认干净,此后两次推送只增加了钉住每道门槛的边界测试。遗留的小瑕疵不变——相邻的 XHR/SSE 格式化位置有意未改;Web Shell 实际显示效果在沙箱验证报告落地前仍只有 mock 复现支撑。

反思:我重新独立推导了方案,PR 依然与之吻合——同样的位置(failOnError)、同样的门槛、没有遗漏更简路径。上次通过后变化的只有测试覆盖:作者对两轮 review 均以纯测试提交回应,现在共 13 个用例固定了每条路径的确切消息、status 与 body,包括优先级、空值回退、非 -32603 数字 code 和 null data。SDK 中没有任何消费方依赖 .message 文本,影响范围在构造上仅限展示。该提交的 CI 全绿——唯一 cancelled 的检查在同一 SHA 的首次尝试中已通过,重试是在共享 runner 基础设施上中途被中断。两轮 review 中作者精确且诚实:对两处超出范围的建议都给出了理由明确拒绝,而不是悄悄扩大 diff;PR 严格停留在 issue 划定的范围内。批准;沙箱验证报告将另行发布,作为确认而非门槛。

Qwen Code · qwen3.8-max

Reviewed at 394c4d745edd51a6834d9a8fbbb571bb9b156154 · re-run with @qwen-code /triage

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 30, 2026

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent 3c": verifying the daemon-side error serialization for GET /acp (whether AcpHttpTransport 's connect path can receive the JSON-RPC envelope) — I stopped at the tr….

中文说明

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

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):"agent 3c"verifying the daemon-side error serialization for GET /acp (whether AcpHttpTransport 's connect path can receive the JSON-RPC envelope) — I stopped at the tr…

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

Comment thread packages/sdk-typescript/src/daemon/DaemonClient.ts
Comment thread packages/sdk-typescript/test/unit/DaemonClient.test.ts
Comment thread packages/sdk-typescript/src/daemon/DaemonClient.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Review follow-up for 3241aae9bd:

Item Action Rationale
R1-1 Not taken The reproduced model-switch path uses failOnError; changing latent XHR/SSE/ACP stream paths would expand the observed issue and declared PR scope.
R1-2 Fixed Added four cases pinning details precedence, fallback from empty details, empty string data, and empty message. All three requested mutants are killed.
R1-3 Not taken The daemon forwarding behavior predates this SDK-only fix; adding a CLI package contract test would create an unrelated cross-package expansion.

Verification: full DaemonClient.test.ts suite passed (389/389), SDK typecheck passed, SDK build passed, git diff --check passed, and independent isolated mutation checks killed all three requested mutants.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": runtime confirmation that a live AcpHttpTransport/AcpWsTransport-configured DaemonClient actually receives the code-less body (verified structurally from both t…; "agent 5": running the new vitest cases to confirm they are green — vitest was intermittently absent from the shared worktree's node_modules across calls (concurrent workt…; "agent 1a": executing the new vitest cases in packages/sdk-typescript/test/unit/DaemonClient.test.ts (worktree had no node_modules; npm ci failed with an npm script error….

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Test Plan (not a blocker): 385 passed — this review observed 1733, 481, 5173, 94 passed; 11 passed — this review observed 1733, 481, 5173, 94 passed.

中文说明

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

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"runtime confirmation that a live AcpHttpTransport/AcpWsTransport-configured DaemonClient actually receives the code-less body (verified structurally from both t…"agent 5"running the new vitest cases to confirm they are green — vitest was intermittently absent from the shared worktree's node_modules across calls (concurrent workt…"agent 1a"executing the new vitest cases in packages/sdk-typescript/test/unit/DaemonClient.test.ts (worktree had no node_modules; npm ci failed with an npm script error…

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

Test Plan(非阻断):385 passed — this review observed 1733, 481, 5173, 94 passed; 11 passed — this review observed 1733, 481, 5173, 94 passed

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

Comment thread packages/sdk-typescript/test/unit/DaemonClient.test.ts
Comment thread packages/sdk-typescript/test/unit/DaemonClient.test.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Review round 2 summary (394c4d7)

Item Decision Action
R2-1 Declined Kept the numeric JSON-RPC code gate; code-less ACP transport parity requires a separate redaction and producer-contract decision outside #10570.
R2-2 Fixed Added coverage for a non--32603 numeric code and null data, with both requested mutation checks.

Verification: Prettier check, SDK typecheck, SDK build, and DaemonClient.test.ts (391/391) passed.

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

⚠️ Downgraded from Approve to Comment: CI failing: Integration Tests (no-AK, No Sandbox). Reviewed.

中文说明

⚠️ 已从批准降级为评论:CI failing: Integration Tests (no-AK, No Sandbox)。 已审查。

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

@doudouOUC doudouOUC self-assigned this Aug 31, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 31, 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: 2471 passed · 0 failed · 2471 total

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

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

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

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

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

Verification report

PR #10571 verification — fix(sdk): Surface daemon JSON-RPC error details

Verdict: merge-ready — 2471/2471 scripted assertions passed, 0 unexpected failures.
Verified head: 394c4d745edd51a6834d9a8fbbb571bb9b156154 (merge ref HEAD^2); base: 40889bad75935d5da3e29bff32a48b2044c5ad57 (HEAD^1 of the merge-ref checkout; the snapshot's baseRefOid 3aa1b14… is stale main, superseded by the merge ref).

中文摘要
  • 结论: merge-ready。2471 项脚本化断言全部通过,0 项意外失败。
  • A/B 结论: 对真实 loopback daemon 驱动 16 个脚本化 5xx/边界 cell:head 构建 67/67(通用 Internal error 的 5xx JSON-RPC 响应正确展开为 data.details/data.message/字符串 data,status 与 body 逐字节保留);base 构建 67/67(同类响应保持 Internal error)。6 个 cell 在两侧翻转,其中 4 个是 PR 声称的形状。把 PR 的新测试文件放到 base 源码上运行,恰好 6 个依赖新行为的测试以预期的 expected-vs-actual 消息失败(385 passed | 6 failed),head 上 391/391 全绿 —— 测试非空转、改动承重。
  • 突变矩阵: 5 个门槛各自单独还原后均被命名的测试杀死(400 gate、精确 Internal error 匹配、数值 code、details/message 优先级、非空 details);两个 Array.isArray 守卫还原后套件仍 391/391 且 16-cell 线级探针仍 67/67,归类为冗余防御(JSON 解析产物不可能触发),非覆盖缺口。
  • Findings(均不阻塞): ① XHR 上传与 SSE 订阅两处同构的错误格式化器仍是旧逻辑,同样会把 Internal error 的细节丢弃(既有行为,PR 明确限定范围); ② 纯空白 data.details 会被原样展示(与 bridge 既有 extractJsonRpcErrorDetail 先例一致); ③ details 无截断,20k 字符原样进入 message(仅展示面,body 本就携带)。
  • 未覆盖: 逐 commit 归因(shallow depth-2,仅聚合 diff 可达);真实 provider 故障的端到端运行(线级 harness 复现的是 wire 形状而非 provider 触发);bug(serve): Web Shell shows generic "Internal error" for failed turns, hiding the provider's actual error message #10564data.error.message 路径;全仓 gate(仅 SDK workspace 套件 + typecheck)。

Central claim + A/B proof

Central claim: for daemon HTTP 5xx responses whose top-level error is exactly Internal error with a numeric JSON-RPC code, DaemonHttpError.message ends with the first non-empty string among string data, data.details, data.message, while status and body stay unchanged; all non-matching shapes keep the old message.

Oracle: ab-harness.mjs boots a real loopback HTTP daemon (node:http, scripted per-cell responses, no fetch injection) and drives client.setSessionModel() from the compiled dist of each build over real sockets; 67 scripted assertions per arm (per cell: instanceof DaemonHttpError, status, exact message, body verbatim; plus wire-side checks that the daemon saw exactly 16 POST /session/s-1/model with body {modelId} and no stray routes). Controls: base dist realpath asserted inside the base worktree with zero error==="Internal error" unwrap markers (no symlink bleed into head code); the diff touches no package.json/lockfile, so the shared root node_modules is not a confound.

# daemon answers (status, body) base message suffix head message suffix flip?
01 500 {error:'Internal error',code:-32603,data:{details:'Missing credentials'}} Internal error Missing credentials yes
02 500 data:{message:'Provider is throttled'} Internal error Provider is throttled yes
03 500 data:'Agent stopped' Internal error Agent stopped yes
04 500 data:{details:'',message:'Provider is throttled'} Internal error Provider is throttled yes
05 400 + details Internal error Internal error (gate holds) no
06 500 error:'Model is unavailable' + details Model is unavailable same (specific errors untouched) no
07 500 code:'internal_error' (string) Internal error Internal error no
08 500 data:{details:{reason:'private'}} Internal error Internal error (non-string not leaked) no
09 500 body [1,2,3] HTTP 500 HTTP 500 no
10 500 data:null Internal error Internal error no
11 500 no data Internal error Internal error no
12 500 non-JSON text body HTTP 500 HTTP 500 no
13 500 data:{details:' '} Internal error ' ' (surfaced; see F2) yes
14 500 data:['x','y'] Internal error Internal error no
15 500 error:{nested:true} [object Object] [object Object] (pre-existing quirk unchanged) no
16 500 data:{details:'x'×20000} Internal error 20000 chars, untruncated (see F3) yes

Head arm 67/67, base arm 67/67 — 6/16 cells flip generic→detail, and every boundary cell is identical on both arms (controls run on both sides). Witnesses: 01-ab-wire-head.png, 02-ab-wire-base.png.

Production-shape trace (mock shapes are real): the ACP SDK's dispatch catch converts any handler error with a string message into RequestError.internalError({details: error.message}){code:-32603, message:'Internal error', data:{details:...}} (node_modules/@​agentclientprotocol/sdk dist, internalError); the daemon's errorPayload (packages/cli/src/serve/server/error-response.ts) forwards code/data verbatim with error: errorMessage(err) = Internal error. So a config.switchModel credential failure produces exactly cell 01's envelope. The bridge already unwraps the same three shapes with the same precedence for the model_switch_failed SSE event (extractJsonRpcErrorDetail, packages/acp-bridge/src/bridge.ts:1786), so the HTTP path now agrees with what SSE subscribers already see. Note the exact-equality gate is safe against Internal error: <extra> (the SDK's additionalMessage form): those carry the reason in the top-level string already, so not unwrapping them loses nothing.

Vacuity (tests vs unfixed source)

PR test file run against base source: 6 failed | 385 passed (391); the 6 failures are exactly the head-dependent tests (surfaces JSON-RPC details…, provider message, details wins over message, string data, empty details falls back to message, non-internal numeric code), failing on the intended assertion (expected "message": "POST /session/:id/model: Missing credentials" vs received Internal error). At head the file is 391/391 (06-focused-suite-head.png); whole package suite 1735/1735 across 37 files; npm run typecheck exit 0. Witness: 03-vacuity-new-tests-on-base.png.

Mutation matrix (guards pinned?)

Each mutant = one-point edit to head source, full 391-test file run, file restored (witness 04-mutation-matrix.png; survivors classified by wire probe, 05-survivor-classification.png):

mutant predicted result killed by (attribution)
M0 unmutated green 391/391 — (green control)
M1 drop res.status >= 500 killed 1 failed formats 'client error' safely (400)
M2 === 'Internal error' → any string killed 1 failed formats 'specific top-level error' safely
M3 typeof code === 'number'!== undefined killed 1 failed formats 'non-JSON-RPC code' safely
M4 drop !Array.isArray(body) survives 391/391 + wire 67/67 redundant defence: JSON.parse can never yield an array with an 'error' key, so the clause decides no outcome
M4b drop !Array.isArray(data) survives 391/391 + wire 67/67 redundant defence: array data has no details/message properties
M5 swap details/message precedence killed 1 failed formats 'details wins over message' safely
M6 drop details.length > 0 killed 2 failed empty details + empty details falls back to message
whole-hunk revert (= base arm) new tests red 6 failed the vacuity run above

Positive controls: five mutants killed by exactly the named tests; unmutated control green. Survivors M4/M4b are not coverage gaps — the full 16-cell wire harness passes 67/67 with each guard removed (and with head unmutated), proving no observable behavior depends on them; they mirror the bridge's own (equally redundant) array handling. No combination row needed: each guard closes a distinct dimension, none defend one hazard twice.

Corrections

  • The PR description states "the full DaemonClient.test.ts suite (385 passed)". At the verified head the file contains 391 tests, all passing; 385 is the pass count when the new test file runs against base source (391 − the 6 head-behavior tests). The code is correct; only the description's number understates the suite.

Findings (non-blocking)

  1. Suggestion — sibling formatters keep the old opaque behavior. The XHR upload path (DaemonClient.ts:2133-2137) and the SSE subscribe path (RestSseTransport.ts:179-187) build DaemonHttpError with the pre-PR String(body.error) logic, so a 5xx Internal error on uploadWorkspaceFile or GET /session/:id/events still discards data.details. Pre-existing, and the PR explicitly scopes to the failOnError HTTP path (Risk & Scope says so); filed as a follow-up candidate, not a defect of this diff.
  2. Nit — whitespace-only data.details surfaces verbatim (length > 0 admits ' '; cell 13). Identical to the bridge's extractJsonRpcErrorDetail precedent, so consistent; only reachable when the daemon emits whitespace-only detail.
  3. Nit — no truncation of surfaced detail (cell 16: 20k chars enter message unmodified). Display-only; the same string already travels in error.body, and the daemon is a local trusted process, so no new exposure — purely a UI-length consideration.

Not covered

  • Per-commit attribution: checkout is depth-2 (rev-list HEAD^1..HEAD^2 = 1 locally vs 3 commits in the metadata snapshot); verified the aggregate HEAD^1..HEAD diff only.
  • Live provider failure E2E: the harness reproduces the exact wire shape the production chain emits (traced in the A/B section), not the provider-side trigger; a live credential-mutation run was not performed (same limitation the PR declares).
  • bug(serve): Web Shell shows generic "Internal error" for failed turns, hiding the provider's actual error message #10564's data.error.message turn-error path, [object Object] toast, duplicate HTTP/SSE notices, retries — declared out of scope by the PR; not exercised.
  • Sibling formatters (F1) verified by code reading only, not behavioral runs.
  • Repo-wide gates: only the @qwen-code/sdk workspace suite + typecheck ran (the PR's own CI covers the rest).
  • Author's macOS/Windows rows: pure-JS logic, not re-tested per-OS.

Methodology

Environment: CI verify container, npm ci + npm run build pre-run at head; base and mutant builds in scratch worktrees tmp/base-tree (HEAD^1) and tmp/mutant-tree (HEAD) under the repo, sharing the root node_modules (lockfile untouched by the PR; base dist realpath asserted to prevent symlink bleed). Harnesses (ab-harness.mjs, mutation-matrix2.sh, survivor-probe.sh in this artifact dir) drive the compiled dist/daemon/index.js of each build over a real loopback HTTP server; mutant bundles were produced with the package's own esbuild recipe. Raw logs: head-harness.log, base-harness.log, base-vacuity.log, mutant-M*.log, harness-M4*.log, sdk-head-full-suite.log, sdk-head-typecheck2.log. Assertion accounting: 335 wire assertions (5 runs × 67) + 1735 head suite tests + 385 base-run passing tests + 6 encoded expected-failure assertions (vacuity) + 8 matrix-row predictions + 2 gates (typecheck, base build) = 2471, 0 unexpected failures.

Flakiness gate log

rounds=5 files=1 skipped=0
file packages/sdk-typescript/test/unit/DaemonClient.test.ts: (cd packages/sdk-typescript) npx --no-install vitest run ./test/unit/DaemonClient.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/sdk-typescript/test/unit/DaemonClient.test.ts: PPPPP

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

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 2 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 3 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 4 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 5 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)

Evidence images

01-ab-wire-head

02-ab-wire-base

03-vacuity-new-tests-on-base

04-mutation-matrix

05-survivor-classification

06-focused-suite-head

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

@doudouOUC
doudouOUC enabled auto-merge August 31, 2026 02:30

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

Tier: Standard — 2 files, SDK bug fix (+196/−3).

CI — 30 checks; 29 completed / skipped, 1 route: success. No build or integration-test job ran on the HEAD SHA. The PR description states 391 DaemonClient tests passed locally plus SDK typecheck and build; those results are not machine-verified by CI on this commit.

What I checked:

Logic correctness in failOnError:

  • Array safety: !Array.isArray(body) and !Array.isArray(data) are both guarded — neither a body array nor a data array is mistaken for a plain object.
  • Null safety: data && typeof data === 'object' correctly rejects null (null is falsy). Confirmed by the "null data" test case.
  • Gate correctness: res.status >= 500, errorBody?.['error'] === 'Internal error', and typeof errorBody['code'] === 'number' together limit the unwrap to opaque generic 5xx JSON-RPC responses. Client errors (400), specific top-level error strings, and non-numeric codes fall through to the existing display behavior.
  • Precedence: string datadata.detailsdata.message → top-level error, each gated by .length > 0.
  • data: undefined: unreachable by the guards (fails typeof data === 'string' and data && typeof data === 'object'); detail stays at top-level error. No test needed.

Tests: The 14-row parametric table covers all declared boundary cases. One observation:

The test named 'non-internal numeric code' uses code: -32000 and expects expected: 'Server is draining' — i.e., it verifies that a non--32603 numeric code does trigger unwrapping. The name says "non-internal" but the outcome is "yes, unwraps". The test body is correct; the name is inverted relative to the expectation and will mislead future readers. (Suggestion — not a blocker.)

Cross-check vs. existing reviews:

  • R1-1 (extend to XHR upload path): author declined — out of scope of the reproduced model-switch path. Accepted.
  • R1-2 (precedence + empty-string guard tests): fixed ✓ in earlier commit.
  • R1-3 (daemon-side contract pin): author declined — cross-package scope. Accepted.
  • R2-1 (ACP transport path): author declined — intentional scope boundary. Accepted.
  • R2-2 (null data + non-internal numeric code boundary rows): fixed ✓ in HEAD commit (394c4d7).

No blockers. All suggestions addressed or intentionally declined with clear justification.

Approving.

@doudouOUC
doudouOUC added this pull request to the merge queue Aug 31, 2026
Merged via the queue into QwenLM:main with commit f6cca89 Aug 31, 2026
163 of 167 checks passed
@doudouOUC
doudouOUC deleted the fix/10570-daemon-http-error-details branch August 31, 2026 02:53
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.0.

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

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(sdk): Daemon HTTP errors drop JSON-RPC details

3 participants