Skip to content

fix: add structured error code to SessionNotFoundError for session-closing retry - #8884

Merged
yiliang114 merged 8 commits into
QwenLM:mainfrom
yiliang114:fix/session-closing-structured-code
Aug 11, 2026
Merged

fix: add structured error code to SessionNotFoundError for session-closing retry#8884
yiliang114 merged 8 commits into
QwenLM:mainfrom
yiliang114:fix/session-closing-structured-code

Conversation

@yiliang114

@yiliang114 yiliang114 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds an explicit structured error code for the daemon's session-closing state, updates WebUI retry handling to consume that code, and makes Java session close treat an already-closing session as an idempotent terminal result.

Why it's needed

The follow-up retry behavior from #8864 depended on matching English error text. A stable error code keeps clients correct when messages change and distinguishes an unknown session from a concurrent close without changing existing unknown-session behavior.

Reviewer Test Plan

How to verify

  1. Trigger a session load while the target session is closing and confirm the HTTP 404 body contains code: "session_closing"; confirm WebUI retries this response.
  2. Request an actually unknown session and confirm the body still contains code: "session_not_found" and WebUI does not retry it.
  3. Issue concurrent Java close requests and confirm a matching session_closing response is accepted as successful completion.

Evidence (Before & After)

N/A — protocol and client behavior only; no user-visible UI change.

Tested on

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

Environment (optional)

Focused WebUI and CLI Vitest suites passed on macOS. The Java regression test was added but not executed locally because no JRE/JDK is installed.

Risk & Scope

  • Main risk or tradeoff: Closing-state producers must pass the explicit code; existing SessionNotFoundError callers keep the session_not_found default.
  • Not validated / out of scope: Java runtime execution and unrelated daemon error types.
  • Breaking changes / migration notes: None. The response only adds a structured field and preserves existing status codes and messages.

Linked Issues

Follow-up to #8864.

中文说明

本 PR 做了什么

为守护进程的 session closing 状态增加显式结构化错误码,更新 WebUI 的重试判断以使用该错误码,并让 Java 客户端在关闭 session 时把“已经在关闭中”视为幂等终态。

为什么需要

#8864 的后续重试行为依赖匹配英文错误文本。稳定的错误码可在错误消息变化时保持客户端行为正确,并区分“session 不存在”和“并发关闭”,同时不改变原有的未知 session 行为。

Reviewer 测试计划

验证方式

  1. 在目标 session 正在关闭时触发 session load,确认 HTTP 404 响应包含 code: "session_closing",并确认 WebUI 会重试该响应。
  2. 请求确实不存在的 session,确认响应仍包含 code: "session_not_found",且 WebUI 不会重试。
  3. 并发发起 Java close 请求,确认匹配 session 的 session_closing 响应会被视为成功完成。

前后证据

不适用——仅涉及协议和客户端行为,没有用户可见 UI 变化。

测试平台

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

环境

已在 macOS 通过聚焦的 WebUI 和 CLI Vitest 测试。已增加 Java 回归测试,但本地未安装 JRE/JDK,因此未执行。

风险与范围

  • 主要风险或取舍:closing 状态的生产方必须显式传入错误码;其他 SessionNotFoundError 调用方继续使用 session_not_found 默认值。
  • 未验证或范围外:Java 运行时执行和无关的守护进程错误类型。
  • 破坏性变更或迁移说明:无。响应只新增结构化字段,并保留原有状态码和消息。

关联问题

#8864 的后续修复。

PR QwenLM#8864 retried session switches while the target session is closing,
but relied on fragile string matching against the daemon's error message.
This commit:

1. Adds a `code` property to `SessionNotFoundError` — automatically set
   to `'session_closing'` when the extra message mentions "closing",
   otherwise `'session_not_found'`.

2. Includes `code` in the HTTP JSON response body so clients can
   distinguish closing (transient) from genuinely missing sessions
   without depending on error message text.

3. Updates the WebUI retry check in `DaemonSessionProvider` to use
   `errorBody.code === 'session_closing'` instead of matching
   `endsWith('The session is closing; retry after close completes')`.

4. Fixes an inconsistent error message in `rewindSession` that used the
   short `'The session is closing'` without the retry suffix.

Closes: QwenLM#8864 (follow-up)
@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-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.

Thanks for the PR, @yiliang114 — this is the planned follow-up from the #8864 review thread (the structured-code suggestion deferred there), and the direction is uncontroversial, but the PR body doesn't follow the PR template, so I have to pause it here. This is a formatting gate, not a code concern.

None of the template's required sections are present (the body currently uses a custom ## Summary / ## Changes structure):

  • What this PR does — prose description of the change; note the template asks for prose, not the per-component rundown the current ## Changes list is
  • Why it's needed — the motivation is already in your summary (the fragile string matching landed by #8864); it helps to also note this was deferred from that review thread
  • Reviewer Test Plan, with its three subsections:
    • How to verify — the behaviors a reviewer should confirm and what to expect: e.g. the closing-retry path now keys off the structured code field instead of the message suffix, the rewind path returns the consistent retry message, and which suites pin that (the updated DaemonSessionProvider.test.tsx fixtures)
    • Evidence (Before & After) — this is an internal daemon/Web Shell protocol change rather than a user-visible one, so N/A is fine here per the template, with commands and output under How to verify
    • Tested on — the OS matrix (🍏/🪟/🐧 with ✅/⚠️/N/A); right now it's unclear where your verification ran
  • Risk & Scope — the three bullets: main risk or tradeoff / not validated / breaking changes
  • Linked Issues — reference #8864 without a closing keyword, since this is a follow-up
  • The <details> Chinese translation of the body

Could you restructure the body to follow the template? The content you already wrote is good — most of it can be moved into the right sections as-is. Please keep each paragraph or list item as one long line (the template notes that GitHub renders single newlines as <br>, so hard-wrapped text displays as a narrow column).

Once the body is updated, a maintainer can re-run triage with @qwen-code /triage to continue.

中文说明

感谢提交 PR,@yiliang114 —— 这是 #8864 评审线程中约定的后续 PR(当时搁置的结构化 code 建议),方向没有争议,但 PR 正文没有遵循 PR 模板,所以需要先停在这里。这是一次格式上的拦截,而不是对代码的质疑。

模板要求的章节全部缺失(目前正文使用了自定义的 ## Summary / ## Changes 结构):

  • What this PR does —— 用散文描述改动;注意模板要求散文描述,而不是目前 ## Changes 那样按组件罗列
  • Why it's needed —— 动机在你的摘要里已经写了(#8864 引入的脆弱字符串匹配);最好同时说明这是从那个评审线程搁置下来的后续工作
  • Reviewer Test Plan,包含三个子章节:
    • How to verify —— 评审者应确认的行为和预期结果:例如 closing 重试路径现在依赖结构化 code 字段而不是消息后缀、rewind 路径返回一致的重试消息,以及哪些测试套件固定了这些行为(更新的 DaemonSessionProvider.test.tsx fixtures)
    • Evidence (Before & After) —— 这是 daemon/Web Shell 内部协议改动,而非用户可见改动,按模板写 N/A 即可,命令与输出放在 How to verify 下
    • Tested on —— 操作系统矩阵(🍏/🪟/🐧 加 ✅/⚠️/N/A);目前无法判断你的验证是在哪个平台上进行的
  • Risk & Scope —— 三个要点:主要风险或权衡 / 未验证项 / 破坏性变更
  • Linked Issues —— 引用 #8864(不使用关闭关键字,因为这是后续 PR)
  • 正文的 <details> 中文翻译

能否按模板重构正文?你已经写好的内容大部分是好的 —— 多数可以直接挪到对应章节。请保持每个段落或列表项为一长行(模板注明 GitHub 会把单个换行渲染成 <br>,硬换行的文字会显示成窄列)。

正文更新后,维护者可以用 @qwen-code /triage 重新触发 triage 继续流程。

Qwen Code · qwen3.8-max

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head afd1833. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 1 render-shaping file:

  • packages/webui/src/daemon/session/DaemonSessionProvider.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head afd1833, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not explored to full depth (tool budget reached): PR #8884 (QwenLM/qwen-code) adds a structured code fiel...: did not trace every call path that can surface inside the WebUI event-loop catch (e.g., whether a closeSessionImpl double-close 404 can ever reach that catch …; PR #8884 (QwenLM/qwen-code) adds a structured code fiel...: none — all checks above completed within budget.; PR #8884 (QwenLM/qwen-code) adds a structured code fiel...: none — all checks I started were completed within budget.; PR #8884 (QwenLM/qwen-code) adds a structured code fiel...: none — all checks I started were completed within budget..

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

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

Comment thread packages/cli/src/serve/server/error-response.ts
Comment thread packages/acp-bridge/src/bridgeErrors.ts Outdated
Comment thread packages/acp-bridge/src/bridgeErrors.ts Outdated
Comment thread packages/cli/src/serve/server/error-response.ts
Comment thread packages/cli/src/serve/server/error-response.ts
Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.tsx Outdated
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout: replaced message-derived closing classification with explicit producer codes, preserved Java DELETE idempotence for session_closing, added CLI/WebUI/Java regressions, documented the wire codes, and repaired the PR body to the repository template. Verified 204 WebUI tests and 7 CLI error-response tests; Java execution remains unverified because no JRE/JDK is installed locally. The pre-existing export mapper was intentionally left unchanged because that route cannot produce a closing code. All active threads are closed; exact-head CI/review is pending.

中文摘要已改为显式 closing 错误码、保留 Java DELETE 幂等语义、补充测试与协议文档并修复 PR 模板;204 个 WebUI 和 7 个 CLI 测试通过,本地无 JRE/JDK,等待 CI。

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Verified this end-to-end against the installed Desktop 0.1.1 / Qwen Code 0.21.9 candidate. I rapidly switched alpha → beta → alpha → beta; the real load responses were 200, 200, 404 { code: "session_closing" }, 200. The client retried the structured closing response, attached the final beta session, restored its transcript, and showed no error notice.

@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. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

Not explored to full depth (tool budget reached): This change adds a structured session_closing error cod...: none — all checks above completed within budget.; This change adds a structured session_closing error cod...: none — all checks above completed within budget.; This change adds a structured session_closing error cod...: Java unit test not executed — no JVM tooling ( java / javac / mvn ) exists on this machine; the Java change was verified by close reading only..

中文说明

已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。

未探索到全部深度(达到工具调用预算):This change adds a structured session_closing error cod...:none — all checks above completed within budget.;This change adds a structured session_closing error cod...:none — all checks above completed within budget.;This change adds a structured session_closing error cod...:Java unit test not executed — no JVM tooling ( java / javac / mvn ) exists on this machine; the Java change was verified by close reading only.

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

Comment thread packages/acp-bridge/src/bridge.ts
Comment thread docs/developers/qwen-serve-protocol.md
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/acp-bridge/src/bridgeErrors.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Packaged Desktop regression result

The final local Desktop 0.1.1 candidate at integration commit 51c9d48b contains this PR head f7aa1689 together with the client timeout cleanup in #8883.

Two concurrent sessions were both admitted with 202 and completed independently. I then clicked their real sidebar entries Alpha → Beta → Alpha → Beta: the four /load responses were all 200, the final session was correct, and the UI showed no session_closing, No session with id, or missing-session error.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Changed: corrected the canonical protocol error shape to include session_not_found and document the concurrent session_closing code.

Verified: Prettier and git diff --check passed; no executable behavior changed.

Intentionally not changed: additional producer tests and helper extraction.

Pending: exact-head CI and automatic review.

中文摘要

已统一协议文档中的标准错误结构并补充 session_closing;无运行时代码改动,等待 CI。

@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. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

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): Change summary: This PR adds a structured code field ('...: did not dynamically confirm how often the child's sessionClose RPC actually returns a definitive refusal in production traffic (mechanism verified from source…; Change summary: This PR adds a structured code field ('...: none — all planned checks completed (~20 tool calls).; You are review agent reverse-audit — Reverse audit agen...: none — I finished within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget..

中文说明

已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。

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

未探索到全部深度(达到工具调用预算):Change summary: This PR adds a structured code field ('...:did not dynamically confirm how often the child's sessionClose RPC actually returns a definitive refusal in production traffic (mechanism verified from source…;Change summary: This PR adds a structured code field ('...:none — all planned checks completed (~20 tool calls).;You are review agent reverse-audit — Reverse audit agen...:none — I finished within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.

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

Comment thread packages/acp-bridge/src/bridge.ts
Comment thread docs/developers/qwen-serve-protocol.md
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Resolved the current review feedback on afd18336907: the post-await restore re-check now preserves session_closing, with the existing race test strengthened to pin the code. The Java abstraction and organization-route envelope suggestions were left unchanged because they are inert or require a separate public API decision. Prettier and git diff --check pass; the focused ACP test is blocked during collection because the local linked workspace has no built @qwen-code/qwen-code-core entry. Exact-head CI is running.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR has merge conflicts, so refs/pull/8884/merge is unavailable — resolve conflicts and re-run.

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR has merge conflicts, so refs/pull/8884/merge is unavailable — resolve conflicts and re-run。

Qwen Code · sandboxed verification

doudouOUC
doudouOUC previously approved these changes Aug 11, 2026

@doudouOUC doudouOUC 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 exact head afd183369070ec0ea79b8859dd4044b8b3f544e5. The structured closing code is explicit at every closing-state producer, including the post-await restore re-check; it is serialized on REST 404s, drives only the pending WebUI load retry, and preserves Java DELETE idempotence while validating the session id. Unknown sessions retain session_not_found. Exact-head Node, Web Shell, Serve A/B, Java matrix, and real-daemon E2E checks pass, and the prior Critical findings are fixed. No blocking findings.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run against 54f9fbced23c — the recovered head. Gate findings unchanged, and the branch state that blocked the last two runs is resolved.

  • Template: ✓ all required sections present.
  • Problem: real, not theoretical — this is the structured-code follow-up deferred from the [webshell]retry session switch while closing #8864 review thread; the merged parent is the evidence the retry path exists and currently keys off English error text.
  • Direction: aligned. Structured codes are already the convention on this surface — sendBridgeError emits code: 'session_archived' on the branch right next to the one this PR changes, and the error-taxonomy doc this PR updates is the canonical reference.
  • Size: cross-package (acp-bridge / cli / sdk-java / webui + docs) but small — 58 production lines, 77 test lines, 58 doc lines. Far below every threshold; author is a maintainer (admin), so the two-tier core gate is exempt regardless.
  • Approach: minimal and unchanged — one code field on SessionNotFoundError, serialized in sendBridgeError, consumed by the WebUI retry predicate and Java close idempotence, plus docs. Nothing to cut. The post-fix(webui): Make cross-session switching transactional #8882 conflict resolution adds one judgment call worth naming: the legacy text fallback is now gated on the daemon advertising client_identity (one caveat on that in the review comment).
  • Risk: no high-risk-path matches; no elevated risk signals.

The branch state that blocked the last two runs is cleared: the rebase onto 32812e9 landed, and while GitHub was still computing mergeability at review time, the pull_request CI runs are building the merge ref on this head — which they can only do when the merge is clean.

Moving on to code review. 🔍

中文说明

针对恢复后的 head 54f9fbced23c 重跑。门禁结论不变,阻挡前两轮的分支状态已解决。

  • 模板:✓ 所有必需章节齐全。
  • 问题:真实存在,不是理论性的 —— 这是 [webshell]retry session switch while closing #8864 评审中约定后续跟进的结构化错误码改造;已合并的父 PR 就是重试路径存在、且目前依赖英文错误文本的证据。
  • 方向:对齐。结构化错误码已是该接口的惯例 —— sendBridgeError 紧邻本 PR 修改的分支就返回 code: 'session_archived',本 PR 更新的错误分类文档正是该接口的规范参考。
  • 规模:跨包(acp-bridge / cli / sdk-java / webui + docs)但很小 —— 生产 58 行、测试 77 行、文档 58 行。远低于所有阈值;作者是维护者(admin),双层核心门禁本就豁免。
  • 方案:最小且不变 —— SessionNotFoundError 加一个 code 字段,在 sendBridgeError 序列化,由 WebUI 重试判断与 Java close 幂等消费,外加文档。没有可砍的部分。fix(webui): Make cross-session switching transactional #8882 之后的冲突解决新增了一个值得点名的判断:旧文本回退现在以守护进程声明 client_identity 能力为门槛(相关保留意见见评审评论)。
  • 风险:未命中高风险路径;无升级风险信号。

阻挡前两轮的分支状态已清除:到 32812e9 的 rebase 已落地;评审时 GitHub 仍在计算可合并性,但 pull_request CI 已能在该 head 上构建 merge ref —— 只有合并干净时才会如此。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 54f9fbced23c8dd6ce9ea6c763865a50535ef5be · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Everything the last run asked for is verifiably on this head — I re-checked each item against the diff and current main, not the closeout:

  • Merge recovered. The rebase onto 32812e9 carries the fix(webui): Make cross-session switching transactional #8882 resolution: isClosingSessionLoadError() checks body?.['code'] === 'session_closing' first, and the WebUI conflict was resolved exactly the way the discarded 2d9e9d1 had done it once.
  • Fixture mechanism-pin restored. The positive closing-retry fixture's body text is shortened ("...The session is closing"), so the legacy endsWith matcher can't match it — that test now passes only via code. The new negative test lands too: a session_not_found 404 does not retry.
  • Class pin + taxonomy row: both present, carried over unchanged.
  • Producer coverage is complete. I audited every SessionNotFoundError construction site in bridge.ts against main: the six closing-message producers plus the post-await re-check in restoreSession (previously a bare not-found throw, now correctly split into not-found vs closing) all carry session_closing; every other site is a genuine unknown-session default, including the two agent-crash throws that look similar but aren't closing states. No closing producer was missed.
  • The new piece this round is the legacy fallback gate: isClosingSessionLoadError(error, allowLegacyMessage) keeps the text match only when the daemon doesn't advertise client_identity. I checked both call sites: the SSE restore loop reads the gate from the daemon capabilities snapshot (fail-open to legacy if undefined), and the cross-session transition site keeps the code-only default — correct by construction, since that path only exists for daemons that advertise client_identity (fix(webui): Make cross-session switching transactional #8882 routes older ones to the legacy flow). [webshell]retry session switch while closing #8864's own retains legacy closing-session retries without client identity test still passes by construction. One caveat, non-blocking: client_identity has shipped since May (feat(serve): add daemon-stamped client identity #4231) while code ships with this PR, so a newer WebUI against an already-released daemon (anything through v0.21.9) would surface the closing race as an error instead of an auto-retry. That window closes once this ships — daemons with client_identity and the code field go out together — and the failure direction is benign (manual retry works, nothing lost), so I'm recording it rather than blocking on it.
  • Java scope confirmed unchanged and correctly used: isCurrentSessionNotFound is consulted only by destroySession()'s DELETE, and code == null still covers old daemons. The two earlier deferred suggestions (shared Java predicate, code on the organization route) have reasoned rejections on record — fine to leave.

Test evidence — PR's own CI at 54f9fbc, fetched once via API

Check Conclusion
Test (ubuntu-latest, Node 22.x) ⏳ in progress
Serve A/B (ubuntu-latest, Node 22.x) ⏳ in progress
Capture web-shell visuals (ubuntu-latest, Node 22.x) ⏳ in progress
ubuntu-latest / Java 11, Java 17, Java 21 ✅ success
macos-latest / Java 21, windows-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
Desktop Shell (ubuntu-22.04), Desktop Shell (windows-2022) ✅ success
Test (macos-latest / windows-latest, Node 22.x) ⏭️ skipped (merge-queue-only jobs, by design)
Integration Tests (CLI, No Sandbox) ⏭️ skipped

The ubuntu suite — which covers every TS package this PR touches (acp-bridge, cli, webui) — is still running, with nothing red so far. Two notes on the shape of this table: the macOS/Windows Node tests are merge-queue-only in ci.yml (event_name == 'merge_group'), so their skip is CI policy, not a coverage gap; and the Java suites settle the one gap the author flagged (no local JDK) — the new destroyAcceptsAlreadyClosingForCurrentSession regression test ran green across the full matrix plus the real-daemon E2E.

Sandboxed verification would settle the last claim static review can't: @qwen-code /verify — that a real daemon under a genuine closing race emits code: "session_closing" on the wire and the WebUI retries it, and that the suites fail with the code check removed. A verify run for this head is already in flight per the lifecycle comment. Real-scenario testing: not driven — unattended CI run; the packaged-Desktop results earlier in the thread remain the author's claim, not independently re-run here.

中文说明

代码审查

上一轮要求的每一项都已确认落在当前 head 上 —— 逐项对照 diff 与当前 main 核验,不是照抄 closeout:

  • 合并已找回。32812e9 的 rebase 带有 fix(webui): Make cross-session switching transactional #8882 的解决方案:isClosingSessionLoadError() 优先检查 body?.['code'] === 'session_closing',WebUI 冲突的解决方式与被丢弃的 2d9e9d1 当时所做完全一致。
  • 夹具机制钉已恢复。 正向 closing 重试夹具的 body 文本已截短("...The session is closing"),旧的 endsWith 匹配器无法命中 —— 该测试只能靠 code 通过。新增负向测试也已落地:session_not_found 404 不重试。
  • 类钉 + 分类表行:均在,原样保留。
  • 抛出点覆盖完整。 对照 main 审计了 bridge.ts 中全部 SessionNotFoundError 构造点:六处 closing 消息抛出点,加上 restoreSession 的 post-await 复查(此前是裸 not-found,现在正确拆分为 not-found 与 closing),全部带 session_closing;其余均为真正的未知 session 默认值,包括两处形似但并非 closing 状态的 agent 崩溃抛出。没有遗漏的 closing 抛出点。
  • 本轮新增部分是旧回退的门槛:isClosingSessionLoadError(error, allowLegacyMessage) 仅在守护进程未声明 client_identity 时保留文本匹配。两个调用点都核过:SSE 恢复循环从守护进程能力快照读取门槛(undefined 时向旧匹配敞开);跨 session 切换点保持仅 code 的默认值 —— 结构上正确,因为该路径只对声明 client_identity 的守护进程存在(fix(webui): Make cross-session switching transactional #8882 把更旧的守护进程路由到旧流程)。[webshell]retry session switch while closing #8864 自带的 retains legacy closing-session retries without client identity 测试也因此在构造上仍然通过。一个非阻塞保留意见:client_identity 自 5 月起已随版本发布(feat(serve): add daemon-stamped client identity #4231),而 code 随本 PR 才发布,因此新版 WebUI 对接已发布的守护进程(v0.21.9 及以前)时,closing 竞态会显示为错误而不是自动重试。该窗口随本次发布关闭 —— 带 client_identitycode 字段的守护进程将同时发出 —— 且失败方向良性(手动重试可用,无损失),故记录在案、不作为阻塞。
  • Java 范围确认不变且使用正确:isCurrentSessionNotFound 只被 destroySession() 的 DELETE 使用,code == null 仍兼容旧守护进程。更早两条延期建议(Java 谓词共享、organization 路由加 code)均有书面理由 —— 维持不动。

测试证据 —— 54f9fbc 自身 CI,经 API 一次性获取

(机器可读表格见英文部分。)ubuntu 套件 —— 覆盖本 PR 触及的全部 TS 包(acp-bridge、cli、webui)—— 仍在运行,目前无红。两点说明:macOS/Windows Node 测试在 ci.yml 中仅限 merge queue(event_name == 'merge_group'),跳过是 CI 策略而非覆盖缺口;Java 套件解决了作者标注的唯一缺口(本地无 JDK)—— 新增的 destroyAcceptsAlreadyClosingForCurrentSession 回归测试在完整矩阵加真实守护进程 E2E 中全绿。

沙箱验证可以了结静态审查无法证明的最后一项:@qwen-code /verify —— 真实守护进程在真实关闭竞态下是否在线上发出 code: "session_closing" 并被 WebUI 重试,以及移除 code 检查后套件是否失败。针对该 head 的 verify 运行已按生命周期评论在途。真实场景测试:未驱动 —— 无人值守 CI 运行;线程前段的打包 Desktop 结果仍是作者自述,未在此独立复跑。

Qwen Code · qwen3.8-max

Reviewed at 54f9fbced23c8dd6ce9ea6c763865a50535ef5be · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — every prior-round demand is verifiably landed and the change is exactly the minimal follow-up the merged parent deferred; the last fifth is CI finishing on this head.

Stepping back: my independent take on this problem — one additive wire field on the existing error class, default preserved, every closing producer tagged, consumers switched to the code with a compatibility path for older daemons — is what this PR does, almost line for line. The one place it goes beyond the naive version is the client_identity gate on the legacy text fallback, and it's the right call: #8882's transactional switching already uses that feature as the modern/legacy daemon divide, so the retry predicate now follows the same convention instead of keeping an unconditional text match. The skew-window caveat (released daemons advertise client_identity but won't carry code until this ships) is recorded in the review comment — benign failure direction, closes on release, not worth holding for.

The branch saga from the last two runs is over: the merge that was pushed and then lost in a force-push is back, resolved the same way, and this time it is the head itself. Recovery steps 1–4 from my last comment are all verifiable in the diff. The Java tests the author couldn't run locally ran green in CI across the full matrix plus the real-daemon E2E. A maintainer approval already stands on this exact commit; the bot's own is the one still missing, and CI is the only thing in front of it.

CI: two pull_request runs still in flight on 54f9fbc (the ubuntu suite covering every touched TS package, and Serve A/B), nothing red so far. Approval is deferred until they land green — the marker below carries it; nothing for the author to do. If the web-shell vitest OOM from 2d9e9d1's run reappears, that read stands (a package this PR doesn't touch) — but only after everything else is green.

(Housekeeping: the bot's two Aug-10 template-gate CHANGES_REQUESTED reviews are still on record against the first head; the deferred approval — once CI is green — comes from the same account and supersedes them in the merge decision.)

中文说明

Confidence: 4/5 —— 前几轮提出的每一项要求都已可核验地落地,改动正是已合并父 PR 延后的那个最小后续;最后五分留给当前 head 的 CI 跑完。

整体看:我对这个问题的独立方案 —— 在既有错误类上加一个可加成的 wire 字段、默认值保留、所有 closing 抛出点打标、消费方切换到 code 并为旧守护进程保留兼容路径 —— 与本 PR 几乎逐行一致。唯一超出朴素方案之处是旧文本回退的 client_identity 门槛,而这是正确选择:#8882 的事务式切换已经用该能力作为新旧守护进程的分界,重试谓词现在沿用同一惯例,而不是保留无条件的文本匹配。版本窗口期的保留意见(已发布守护进程声明 client_identity 但在本次发布前不会有 code)已记录在评审评论 —— 失败方向良性、随发布关闭,不值得为此压住。

前两轮的分支波折到此结束:那次推上去又在 force-push 中丢失的合并回来了、以同样方式解决、且这次就是 head 本身。上条评论的恢复步骤 1–4 均可在 diff 中核验。作者本地无法运行的 Java 测试已在 CI 完整矩阵加真实守护进程 E2E 中全绿。一位维护者的批准已落在同一提交上;缺的只是机器人自己的那张票,而 CI 是它前面唯一的障碍。

CI:54f9fbc 上还有两个 pull_request 运行在途(覆盖所有触及 TS 包的 ubuntu 套件,以及 Serve A/B),目前无红。批准推迟到它们全绿 —— 由下方标记承接;作者无需操作。如果 2d9e9d1 那次运行中的 web-shell vitest 内存溢出再现,维持环境性判断(本 PR 未触及该包)—— 但要在其余全绿之后。

(事务性说明:机器人 8 月 10 日针对最初 head 的两条模板门禁 CHANGES_REQUESTED 评审仍在记录中;CI 全绿后的延迟批准与之同账号,将在合并裁决中覆盖它们。)

Qwen Code · qwen3.8-max

Reviewed at 54f9fbced23c8dd6ce9ea6c763865a50535ef5be · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on afd183369070ec0ea79b8859dd4044b8b3f544e5 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 afd183369070ec0ea79b8859dd4044b8b3f544e5既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@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 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): PR #8884 (QwenLM/qwen-code) adds a structured code fiel...: none — all planned checks completed. Note verification was by reading and tracing, not by executing the test suites.; PR #8884 (QwenLM/qwen-code) adds a structured code fiel...: none — all checks above completed within budget.; PR #8884 (QwenLM/qwen-code) adds a structured code fiel...: none — all checks above completed within budget..

中文说明

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

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

未探索到全部深度(达到工具调用预算):PR #8884 (QwenLM/qwen-code) adds a structured code fiel...:none — all planned checks completed. Note verification was by reading and tracing, not by executing the test suites.;PR #8884 (QwenLM/qwen-code) adds a structured code fiel...:none — all checks above completed within budget.;PR #8884 (QwenLM/qwen-code) adds a structured code fiel...:none — all checks above completed within budget.

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

Comment thread packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx
Comment thread docs/developers/qwen-serve-protocol.md
Comment thread packages/acp-bridge/src/bridge.test.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Resolved all three current review suggestions in 2d9e9d19180:

  • the positive WebUI retry fixtures now prove matching is based on code: session_closing, independent of the legacy error text;
  • the ACP close-race test now pins both SessionNotFoundError and code: session_closing;
  • the daemon error taxonomy now documents the distinct session_not_found and session_closing cases and remediation.

I also merged the latest main and resolved the overlapping WebUI retry helper in favor of the shared structured-code check.

Verification: core and ACP builds; WebUI provider tests (232); ACP bridge tests (544); focused ESLint; Prettier; git diff --check.

@yiliang114
yiliang114 requested a review from doudouOUC August 11, 2026 11:24
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review --timeout=360

@github-actions

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is queued in workflow run.

- Add toBeInstanceOf(SessionNotFoundError) alongside toMatchObject
  to preserve the envelope type assertion
- Document session_closing code in 18-error-taxonomy.md
@yiliang114
yiliang114 force-pushed the fix/session-closing-structured-code branch from 2d9e9d1 to c6f5da5 Compare August 11, 2026 11:28
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR has merge conflicts, so refs/pull/8884/merge is unavailable — resolve conflicts and re-run.

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR has merge conflicts, so refs/pull/8884/merge is unavailable — resolve conflicts and re-run。

Qwen Code · sandboxed verification

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewed exact head c6f5da57b4e9e023dc08cd5a6c47495353b9e6ce. I did not find a new code issue in the structured-code flow; the focused WebUI (204) and ACP bridge (522) suites pass. The remaining blocker is the current merge conflict with main in DaemonSessionProvider.tsx; please resolve that shared-helper overlap and rerun exact-head CI before merge.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on c6f5da57b4e9e023dc08cd5a6c47495353b9e6ce — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 c6f5da57b4e9e023dc08cd5a6c47495353b9e6ce既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Resolved the conflict against the latest main (32812e9). The WebUI merge keeps structured session_closing matching for daemons with client_identity, while preserving the legacy message fallback only for older daemons without that capability.

Focused verification passed: WebUI DaemonSessionProvider (232 tests), ACP bridge (544 tests), CLI error response (7 tests), ESLint, Prettier, and the isolated npm ci prepare/build. Maven is unavailable in this environment, so the Java test remains deferred to CI. The two remaining review threads were outdated and are now resolved.

@qwen-code /review

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

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

Approve. The structured code field on SessionNotFoundError is the right direction — replacing fragile English-text matching with a stable protocol field. Backward compatibility is preserved (code == null branch in Java client covers old servers). The split of the combined if condition into separate session_not_found vs session_closing checks is semantically clearer.

Note: This PR overlaps significantly with #8896 (same files: bridge.ts, bridgeErrors.ts, error-response.ts, Java SDK). Recommend merging this one first (smaller and more focused), then rebasing #8896 on top.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

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

Scripted assertions: 61 passed · 0 failed · 61 total

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

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

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

Verification report

PR #8884 Deep Verification — structured code on SessionNotFoundError

Verdict: findings — 61 scripted assertions executed, 61 pass / 0 fail. The central claim is proven load-bearing by A/B against a rebuilt base control. All findings are minor / non-blocking notes (a cross-version compatibility narrowing worth consciously accepting, plus test-plan reachability and coverage-gap observations). No regressions reproduced.

Verified head: 54f9fbced23c8dd6ce9ea6c763865a50535ef5be (merge checkout 22cc2fd, base tip 32812e9)

中文摘要

结论:findings(61 项脚本化断言全部通过,0 失败;findings 均为轻微/非阻塞提醒)。

  • A/B 结论:中心主张成立。真实守护进程(head 与重建的 base 控制版)通过环回 HTTP 驱动:未知 session 的 load 在 head 返回 404 + code:"session_not_found",在 base 无 code 字段;closing 错误经真实 express + 编译产物序列化,head 输出 code:"session_closing",base 无。WebUI 重试判定函数从两棵树逐字提取后对固定 fixture 矩阵求值:仅带 code 的 closing 响应在 base 不会重试(短消息无后缀),在 head 会重试——即本次变更是承重件。
  • Findings(均轻微):① 兼容门控选用 client_identity 能力位,但所有既有 daemon(含本 PR 之前的所有版本)都已通告该位(实测 109 项特性中包含它),而 code 字段是本 PR 才新增——因此"新 WebUI + 旧 daemon"的跨版本组合会丢失 closing 重试(base 靠文本匹配仍会重试);同版本部署不受影响。建议为 code 字段增设专用能力标签(仓库已有此模式)或在文档中明确该取舍。② Reviewer Test Plan 的步骤 1/3 无法用纯 REST 互相竞争复现(archive coordinator 对 REST-vs-REST 竞争返回 409 session_archiving,closing 404 只在非协调器关闭路径/跨传输场景出现)——是计划可达性问题,非代码缺陷;步骤 2 已实测符合描述。③ 7 个 closing 产生点仅 1 个(restore 等待后的复检)被测试钉住;rewind 的消息+code 修复无测试。④ Java 改动仅静态审查(容器无 JDK,与作者环境一致)。
  • 未覆盖:Java 运行时执行、逐 commit 归因(depth-2 浅克隆仅含 3 个 commit)、ACP-HTTP/agent 崩溃等关闭路径的 closing 窗口实机采样、整库测试套件。

Central claim and A/B proof

Central claim: the daemon's 404 SessionNotFoundError responses carry a structured codesession_closing for a concurrent close, session_not_found otherwise — and the WebUI retries on that code instead of matching English error text.

Secondary claims: (1) Java destroySession accepts a matching session_closing 404 as idempotent terminal success; (2) the rewindSession closing error gains the full retry message + code.

Wire A/B — real daemons, head vs rebuilt base control

Base control: git worktree at HEAD^1, rebuilt (npm run build inside the worktree with workspace links re-pointed into it; readlink -f node_modules/@qwen-code/qwen-code-core…/tmp/base-tree/packages/core asserted before trusting the control). Harness ab-daemon.mjs boots node <tree>/packages/cli/dist/index.js serve --port 0 per arm and drives it over loopback. Witness: 01-ab-daemon-head-code-on-404.png, 02-ab-daemon-base-no-code-on-404.png.

Cell Head Base (control)
Load unknown session id 404 {error, code:"session_not_found", sessionId} 404 {error, sessionId}no code
Prompt burst while a REST close is in progress 409 session_archiving (see finding 2) identical 409
16 concurrent DELETE racers 1×204 + 15×409 session_archiving identical histogram
Load after close (session persisted by warmup prompt) 200 restore 200 restore
First DELETE completes 204 204

Wire A/B — closing-error serialization (express oracle)

The closing branch of the A/B cannot be raced over REST-vs-REST (the session-archive coordinator rejects contenders with 409 before they reach the bridge's closing check — pre-existing on both arms, see finding 2), so the closing serialization is proven one level down with no mocks of the unit under test: ab-express.mjs imports each tree's compiled error-response.js + bridgeErrors.js (module identity across the instanceof boundary verified behaviorally — a wrong-tree class would fall through to 500, observed 404), wires them into a real express server on loopback, and constructs errors exactly as bridge.ts closing-state producers do. Witness: 03-ab-express-serialization-flip.png.

Cell Head Base (control)
Closing error → sendBridgeError 404 {"error":"…The session is closing; retry after close completes","code":"session_closing","sessionId":"…"} 404 same body minus code
Unknown-session error 404 …"code":"session_not_found"… 404 minus code

The flip: 0/2 bodies carry code on base, 2/2 on head. Combined with the daemon cells, every SessionNotFoundError 404 gains its structured field end to end.

WebUI retry gate A/B

webui-retry-ab.mjs extracts isClosingSessionLoadError verbatim from each tree's source (TS annotations stripped mechanically, source echoed in the log), and drives 7 fixtures × the capability axis the call site passes (allowLegacyMessage = daemon lacks client_identity). 14/14 head, 7/7 base. Witness: 04-webui-retry-matrix-head-vs-base.png.

Fixture Base decision Head decision
F1 code:"session_closing" + short message (no retry suffix) no retry (text never matched) retry (code-driven, message-independent)
F2 code + full legacy message retry retry
F3 legacy body, daemon without client_identity retry retry (legacy fallback allowed)
F3 legacy body, daemon with client_identity retry no retry (see finding 1)
F4/F5 not-found bodies, F6 non-HTTP, F7 wrong status no retry no retry

F1 is the load-bearing flip: the retry decision no longer depends on the daemon's exact English phrasing (which the rewindSession fix in this PR demonstrably violated before).

Mutation matrix (vacuity)

Positive controls: every unmutated gate is green (cli error-response.test.ts 7/7, bridge.test.ts -t closing 2/2, DaemonSessionProvider.test.tsx -t closing 5/5 and -t retry 7/7 — two tests match both filters). Witness for row 4: 05-bridge-mutation-kills-closing-test.png.

# Mutation (scratch, restored after) Suite Outcome
1 sendBridgeError: drop code: err.code from the 404 json cli error-response.test.ts killedserializes the structured session-closing code fails on expected-vs-actual json (6 others green)
2 WebUI predicate + call site reverted to base text-match webui -t closing killedretries a session switch while the target session is closing fails (phase: "failed" instead of connected)
3 WebUI call site only: capability arg removed (code logic kept) webui -t closing killedretains legacy closing-session retries without client identity fails (expected "spy" to be called 2 times, but got 1 times)
4 bridgeErrors.ts: this.code = 'session_not_found' (interface-preserving) bridge -t closing killedrejects a bounded refresh when the session starts closing fails on toMatchObject({code:'session_closing'})

No mutant regressed any other test. Survivors (closing-state producers with no pinning test, verified by grep for session_closing across all test files): pre-await attach check (bridge.ts ~5100), close-coalescer (~5622), closeSession "already closing" (~5861), spawn-attach during close (~6271), sendPrompt during close (~6539), rewindSession (~9439). All are coverage gaps, not dead code — each site is reachable in its own flow (the prompt/close paths were exercised live in the daemon harness and answered 409 upstream of the bridge), and each passes a literal 'session_closing' to the now-verified constructor. Completeness reporting, not merge conditions.

Sibling sweep

  • All 133 new SessionNotFoundError( sites enumerated: the 7 closing-state producers all pass the code; the two other multi-arg sites (bridge.ts ~5213, ~6372) are agent-crash "retry to restore/spawn" errors and correctly keep the default session_not_found.
  • The Java client's second predicate isCurrentPermissionNotFound still excludes session_closing — verified safe: the permission routes never throw a closing code (their 404s are hand-built or bare SessionNotFoundError), so no behavior change there.
  • Web-shell's WorkspaceSessionProvider uses client_identity for an unrelated purpose and has no closing-text matching — no sibling consumer needed updating.
  • code: 'session_not_found' already existed at several route-level sites pre-PR (session.ts, sse-events.ts, session-runtime.ts); this PR closes the sendBridgeError gap and makes the field total on SessionNotFoundError.

Findings (ordered by severity; all minor)

1. Legacy-retry fallback is gated on client_identity, which every daemon already advertises — cross-version retry narrows vs base

isClosingSessionLoadError(error, !capabilities?.features.includes(CLIENT_IDENTITY_FEATURE)) allows the legacy text match only when the daemon lacks client_identity. Measured live against a head daemon: /capabilities advertises 109 features including client_identity, and the registry entry is since: 'v1' on base as well — i.e. every daemon release, including all pre-PR ones, advertises it, while the code field exists only from this PR on. Consequence (fixture F3 in the matrix): a head WebUI paired with any pre-PR daemon gets allowLegacyMessage=false and no code in the 404 body → closing retry is lost, whereas base retried on text. Same-version deployments (the daemon serves its own WebUI) are unaffected. The repo's established pattern for exactly this situation is a dedicated capability tag ("older daemons without it leave them on the legacy client-fallback behavior" — see the session_mid_turn_message_query registry comment).

Repro: node tmp/pr8884-verify-20260811-131625/webui-retry-ab.mjs --tree <head> --arm head — F3 row flips true → false between the two capability axes.

Suggested direction (not applied)

Either advertise a new capability tag for the structured code (e.g. session_closing_code) and gate the fallback on its absence, or document the cross-version narrowing as an accepted tradeoff in the protocol doc. No behavior change is needed for same-version pairings.

2. Reviewer Test Plan steps 1 and 3 are not reachable through pure REST contention

Step 1 ("load while the session is closing ⇒ 404 session_closing") and step 3 ("concurrent Java close requests ⇒ matching session_closing accepted") presuppose a 404 during a REST-originated close. Measured on both arms: while a REST close holds the archive coordinator's exclusive lease, every racing REST mutation (prompt, load, second DELETE) gets 409 session_archiving with Retry-After: 5 (pre-existing mapping, present on base; racer histogram 1×204 + 15×409 on both arms). The closing 404 branch is reachable only via close originators that bypass the coordinator — another transport's close (ACP-HTTP), agent-death teardown, or the intra-bridge re-checks — so the Java/WebUI acceptance logic is defensive-but-correct rather than dead, and step 2 (unknown session ⇒ session_not_found, no retry) verifies exactly as written. This is a plan-reachability note, not a code defect; the doc wording ("a concurrent close may return code: "session_closing"") is accurate.

3. Only one of seven closing producers is pinned by a test

See the mutation matrix survivors. Sharp edges: the rewindSession change is a real message change ('The session is closing' → full retry suffix + code) with no test, and the post-restore re-check split (the fix(acp) commit) is pinned only by the one added assertion. If a follow-up touches those sites, nothing currently fails.

4. Java change verified statically only

No JRE/JDK/Maven in this container (same constraint the author reported). Static review: the new destroyAcceptsAlreadyClosingForCurrentSession test and the widened predicate keep the sessionId equality guard (a 404 for a different session is still not treated as already-deleted, consistent with the existing destroyDoesNotTreatGenericNotFoundAsAlreadyDeleted), and the daemon-side mapping the test assumes is the one proven by the express oracle. Compilation and test execution remain unverified here.

Not covered

  • Java runtime execution — no JDK in container (finding 4); the new test never ran anywhere yet.
  • Per-commit attribution — depth-2 checkout: metadata lists 8 commits, only merge/base/head are reachable locally (git rev-list HEAD^1..HEAD^2 = 1). Verified the aggregate HEAD^1..HEAD diff.
  • Live closing-window sampling over the wire — REST-vs-REST is coordinator-masked (finding 2); I did not drive the ACP-HTTP transport or agent-kill paths to observe a live session_closing 404 from the full daemon. The serialization is proven by the express oracle instead; call this the wire shape proof, not an end-to-end trigger reproduction.
  • Full workspace suites (only the four affected suites/files, filtered), repo-wide lint/typecheck (CI-covered), docs lint, WebUI rendering.
  • A harness-level caveat for the record: an early -t "retry" vitest filter silently skipped the retries… tests ("retries" does not contain "retry"); the first webui mutation run was redone under -t "closing" and the counts above are from the corrected runs.

Methodology

Environment: CI verify container (node:22-bookworm), merge-ref checkout at depth 2; npm ci + npm run build pre-run at head. Head daemon driven from packages/cli/dist/index.js serve --port 0 --workspace <scratch> --no-web with an isolated HOME; base control rebuilt in tmp/base-tree with all @qwen-code workspace links re-pointed into the base tree (realpath-asserted) and all other deps symlinked from the root install (lockfile untouched by the PR, so the shared third-party deps are a clean control). Harnesses (ab-daemon.mjs, ab-express.mjs, webui-retry-ab.mjs) live in this artifact dir with raw logs under logs/; every assertion is expectation-encoded per arm (a base cell showing the old code-less shape is a PASSING control). Vitest gates ran from within each package per repo convention. Mutations were applied to scratch copies via scripted replace-with-assert, run, and restored; git status clean after every cycle. Evidence captures rendered by scripts/verify-capture.mjs.

Evidence images

01-ab-daemon-head-code-on-404

02-ab-daemon-base-no-code-on-404

03-ab-express-serialization-flip

04-webui-retry-matrix-head-vs-base

05-bridge-mutation-kills-closing-test

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-review-bot qwen-code-review-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. Reviewed exact head 54f9fbced23c8dd6ce9ea6c763865a50535ef5be.

The structured code field on SessionNotFoundError cleanly replaces the fragile English-text matching from #8864. All 7 closing-state throw sites in bridge.ts explicitly pass 'session_closing', the post-await re-check is correctly separated (byId.get !== existing vs isClosingOrAuthorizingClose), and error-response.ts serializes code in the 404 JSON body. The WebUI retry logic uses the structured code with a legacy fallback gated on daemon capability, and the Java client treats session_closing as idempotent success for DELETE. Tests cover the new serialization, session_not_found non-retry path, and Java idempotent close.

No blocking findings. The prior 4 rounds of automated review S-level findings have been addressed through subsequent commits, and both human reviewers (doudouOUC, yiliang114) confirmed no blocking issues on the latest head.

@yiliang114
yiliang114 added this pull request to the merge queue Aug 11, 2026
Merged via the queue into QwenLM:main with commit 8970856 Aug 11, 2026
58 of 59 checks passed

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

Independent Deep Review — PR #8884

Reviewed blind first (metadata + diff + full-file context, existing reviews deliberately withheld), then cross-validated against the 4 rounds of qwen-code-ci-bot findings and the two approvals.

Note on timing: this review was produced against head 54f9fbce, which the PR reached only via the final merge-resolution commits. The PR merged (8970856) while the review was in flight. Everything below still applies to main and is filed as post-merge follow-up rather than as a merge gate.

Verdict

The core mechanism is sound and well-executed: SessionNotFoundError now carries a 'session_not_found' | 'session_closing' discriminator with a safe default, all 7 closing throw sites in bridge.ts opt in explicitly, sendBridgeError serializes it, and the Java SDK's destroySession idempotence is preserved. Replacing English-substring matching with a stable code is the right call and the docs were updated to match.

However — the change that landed is not the change that was reviewed. The allowLegacyMessage back-compat path in DaemonSessionProvider.tsx was introduced in the merge commit 85a8c36b, is present in no reviewed commit (15f09a04..c6f5da57) and in no main commit (86a474ba had the unconditional message matcher). It carries the one substantive defect below and has zero test coverage.

Findings: 1 Major, 3 Minor, 1 Nit (2 filed inline).


1. [Major] The allowLegacyMessage gate is unreachable for exactly the daemons it targets

packages/webui/src/daemon/session/DaemonSessionProvider.tsx:2769

isClosingSessionLoadError(
  error,
  !capabilities?.features.includes(CLIENT_IDENTITY_FEATURE),
)

client_identity is a baseline, unconditional capability: packages/cli/src/serve/capabilities.ts:78 registers it as { since: 'v1' } and it is not listed in CONDITIONAL_SERVE_FEATURES, so getAdvertisedServeFeatures() emits it for every daemon. It was introduced by 4d9cbe49c (feat(serve): add daemon-stamped client identity, #4231) on 2026-05-17.

The daemons that actually need the legacy fallback are those built from PR #8864 (merged 2026-08-10) up to this PR: they emit '...retry after close completes' but no code. Every one of them advertises client_identity, so the flag evaluates to false and the message branch never executes.

Consequences:

  • The branch is dead code in every realistic deployment, while reading as deliberate back-compat.
  • Against an 8864-era daemon, a WebUI built from this commit loses the closing-session retry that worked before this PR — the code is absent and the message match is suppressed. That is a narrow but real regression window, not a graceful degradation.
  • capabilities is always populated before this catch block runs (assigned at :1293, awaited during connect), so the ?. short-circuit doesn't rescue the intended case either.
  • Even if the flag were true, the fallback would still be incomplete: closeSessionImpl (bridge.ts:5861) throws 'The session is already closing', which .endsWith('The session is closing; retry after close completes') does not match. Message matching was never a complete predicate — further reason not to keep a half-working version of it.

Options, roughly in order of preference:

  1. Drop the parameter, match code only. The skew window is one day and the daemon/WebUI ship together; this is the Simplicity-First answer and matches what was actually reviewed at c6f5da57.
  2. Match code === 'session_closing' || message.endsWith(...) unconditionally — no capability probe. Costs one extra string compare on an error path and is correct for every daemon.
  3. If a probe is genuinely wanted, gate on a real signal: add a session_closing_code tag to SERVE_CAPABILITY_REGISTRY. Piggybacking on an unrelated three-month-old tag as a version proxy will silently rot again.

2. [Minor] The legacy branch has no test coverage, so CI cannot see finding #1

All four WebUI cases that exercise this predicate (DaemonSessionProvider.test.tsx:7409, :7570, :7585, :7647) put code: 'session_closing' in the body. Every one of them passes unchanged if the entire allowLegacyMessage branch is deleted — classic mutation-survivor. There is no case constructing { error: '...retry after close completes' } without code, which is the only input shape the branch exists to handle.

Test (ubuntu-latest, Node 22.x) is green at 54f9fbce, but that green says nothing about this code. Worth noting that Serve A/B and web-shell E2E Smoke were still pending at merge time.

3. [Minor] The two retry sites now disagree about daemon compatibility

DaemonSessionProvider.tsx:3501 — the cross-session transition pump — calls isClosingSessionLoadError(error) with the default allowLegacyMessage = false, i.e. code-only, no legacy tolerance whatsoever. :2769 (the SSE restore loop) tries to pass a flag. Two call sites detecting the same daemon condition now have divergent compatibility semantics, with no comment explaining the asymmetry. Whichever resolution is chosen for #1, these two should agree.

4. [Minor] ACP transports drop the structured code entirely

packages/cli/src/serve/acp-http/dispatch.ts:859

case 'SessionNotFoundError':
case 'InvalidSessionScopeError':
case 'WorkspaceMismatchError':
case 'InvalidClientIdError':
  return { code: RPC.INVALID_PARAMS, message: errMsg(err) };

No data — unlike its immediate neighbours SessionConflictError and SessionArchivingError, which both carry data.errorKind precisely so the client can recover the classification. jsonRpcErrorToHttpStatusWithData (packages/sdk-typescript/src/daemon/acpTransportUtils.ts:73) then maps -32602 → 400.

So over AcpHttpTransport / AcpWsTransport a closing session surfaces as HTTP 400 with no code, and isClosingSessionLoadError bails at its error.status !== 404 guard before it ever looks at the body. The feature is REST-only.

This is pre-existing, not introduced here — but it directly qualifies the PR's premise that a stable code keeps clients correct, and DaemonClient takes a pluggable transport. One-line fix in the spirit of the sibling cases:

case 'SessionNotFoundError':
  return {
    code: RPC.INVALID_PARAMS,
    message: errMsg(err),
    data: {
      errorKind: (err as { code?: string }).code ?? 'session_not_found',
      sessionId: (err as { sessionId?: unknown }).sessionId,
      httpStatus: 404,
    },
  };

5. [Nit] The protocol doc now over-promises the envelope

docs/developers/qwen-serve-protocol.md:67 presents code as part of the canonical SessionNotFoundError envelope. packages/cli/src/serve/routes/session.ts:3987 (handleSessionOrganizationUpdate) still returns { error, sessionId } with no code. The other three hand-written 404 sites (:2041, :2401, :2453) were updated, so this one is now the odd one out.

This is the gap the bot raised as R3-3 and you declined — reasonable at the time, but the doc edit converts an internal inconsistency into a documented-contract mismatch. Either add the field or scope the doc sentence to sendBridgeError-backed routes. (Note the test pin at multi-workspace-sessions.test.ts:4920 uses toEqual, so adding the field requires touching that assertion.)


Cross-Validation Matrix

# Finding Status Assessment
R1-1 Java isCurrentSessionNotFound would flip closing-DELETE to a hard failure Overlap Correctly identified as Critical; resolved in this PR by accepting session_closing, pinned by destroyAcceptsAlreadyClosingForCurrentSession. Verified the predicate is reachable only from the destroySession 404 path (:224) — no blast radius elsewhere. Agree.
R3-1 restoreSession post-await re-check conflated "replaced" with "closing" Overlap Fixed in afd18336; verified the split at bridge.ts:5118-5127. A replaced entry now yields a plain session_not_found and only a genuinely closing entry yields session_closing. Correct — retrying a replaced id would have been wrong.
R4-2 / R4-3 Error-taxonomy doc row + bridge test class pin Overlap Both landed in c6f5da57. The toMatchObject({code:'session_closing'}) addition at bridge.test.ts:4489 is the right shape — it pins the code without loosening the existing toBeInstanceOf assertion.
R1-5 Export route hardcodes session_not_found Overlap → I agree with your decline session.ts:873 resolves from persisted storage where no live entry exists, so session_closing is not a reachable state. Hardcoding is correct, not an oversight.
R2-1 Extract a factory for the closing error Overlap → I agree with your decline Seven call sites each already read unambiguously; a factory would add indirection for no invariant. Consistent with AGENTS.md Simplicity First.
R3-2 isCurrentPermissionNotFound left asymmetric Overlap → I agree with your decline Deliberate and correct: a closing session must not make a permission lookup resolve as idempotent success. The asymmetry encodes a real semantic difference. A short comment there would prevent the next reviewer re-raising it.
R2-3 Test pinning the default code value Overlap → mild disagree You declined as low-value and that is defensible, but the default is the one thing that keeps ~40 unmodified SessionNotFoundError call sites on the old contract. One assertion would make that a pinned invariant rather than an implicit one. Non-blocking.
R3-3 Organization route 404 has no code Overlap → mild disagree, escalated See Nit #5 — the protocol-doc edit in this PR changed the calculus after you declined it.
allowLegacyMessage gate unreachable (Major) Unique — mine Landed in merge commit 85a8c36b, after every bot round. No reviewer saw it.
Legacy branch untested / mutation-survivor Unique — mine Same cause.
Two retry sites with divergent compat semantics Unique — mine :2769 vs :3501.
ACP transport loses the code Unique — mine Pre-existing gap, but it bounds the PR's stated guarantee.
ytahdn Overlaps with #8896, land 8884 first Unique — theirs Verified: #8896's transactional switching landed in main and its retry site (:3501) is the one flagged in Minor #3. The ordering advice was right; the interaction between the two is what produced the asymmetry.
CONFLICTING mergeable state Overlap (author-acknowledged) Resolved via 85a8c36b/1b9f37a9. The resolution itself is where the Major came in — worth a habit of re-requesting review after a non-trivial conflict resolution in shared client logic.

Additional Audit Coverage (no findings)

  • All 7 closing throw sites in bridge.ts (:5103, :5122, :5625, :5864, :6274, :6542, :9439) verified to pass 'session_closing'; no closing path left on the default. Message at :9439 was correctly normalized to the canonical string.
  • All hand-written res.status(404) bodies across packages/cli/src/serve audited for code presence — only session.ts:3987 is missing it (Nit #5).
  • TypeScript SDK: DaemonClient.closeSession treats 204 and any 404 as success, so it is unaffected by the code change in either direction. No parity gap.
  • Python SDK: no session_not_found string coupling; unaffected.
  • Default-parameter safety: every other new SessionNotFoundError(...) call site relies on the 'session_not_found' default and none of them represent a closing session — the default is correctly chosen.
  • WebUI transport: DaemonClient is constructed with the default REST transport (workspaceClientRef.current ?? new DaemonClient({baseUrl, token})), so the retry does work in the shipped configuration — which is why Minor #4 is scoped as a bound on the guarantee rather than a live break.
  • Java: destroySession / detach 404 paths traced; only destroySession consumes the widened predicate.
  • CI at 54f9fbce: Test (ubuntu-latest), Java 11/17/21 matrix, Desktop Shell, Real daemon E2E all green; Serve A/B and web-shell E2E Smoke still pending at merge.

Suggested follow-up

A small follow-up PR resolving #1 (preferably by deleting the branch) plus #3, with one test that feeds a code-less closing body, would close this out. #4 and #5 are independent and can be batched with other protocol cleanup.


This review was generated by QoderWork AI

isClosingSessionLoadError(error)
isClosingSessionLoadError(
error,
!capabilities?.features.includes(CLIENT_IDENTITY_FEATURE),

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.

[Major] This gate is false for every daemon that needs the fallback.

client_identity is a baseline capability — capabilities.ts:78 declares it { since: 'v1' } and it is absent from CONDITIONAL_SERVE_FEATURES, so getAdvertisedServeFeatures() returns it unconditionally. It shipped in 4d9cbe49c (#4231) on 2026-05-17.

The daemons this fallback exists for are the ones built between #8864 (merged 2026-08-10) and this PR: they emit the closing message but not the code. All of them advertise client_identity, so allowLegacyMessage resolves to false and the legacy branch never runs.

Two consequences: the branch is dead code that reads as intentional back-compat, and against an 8864-era daemon this WebUI actually loses a retry that worked before this PR (no code in the body, message match suppressed).

capabilities is already populated by the time this catch block runs — it's assigned at :1293 during connect — so the ?. short-circuit doesn't cover the intended case either.

Simplest correct fix is to drop the second argument and match on code alone; if back-compat is genuinely wanted, match code-or-message unconditionally, or introduce a real session_closing_code capability tag rather than reusing an unrelated three-month-old one as a version proxy.


This review was generated by QoderWork AI

function isClosingSessionLoadError(error: unknown): boolean {
function isClosingSessionLoadError(
error: unknown,
allowLegacyMessage = false,

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.

[Minor] This parameter is untested and was never reviewed.

allowLegacyMessage first appears in the merge commit 85a8c36b. It is in none of the reviewed commits (15f09a04..c6f5da57) and not in main either — 86a474ba had the unconditional message matcher. It entered as conflict-resolution logic and went straight to merge.

No test covers it. All four cases touching this predicate (DaemonSessionProvider.test.tsx:7409, :7570, :7585, :7647) supply code: 'session_closing', so every one of them still passes if this entire branch is deleted. There is no case with the message but no code — the only input shape the branch handles.

Separately, the other caller at :3501 (the cross-session transition pump from #8896) uses the default false, so the two retry paths for the same daemon condition now have different compatibility semantics with nothing explaining why.

Also worth noting the message predicate was never complete: closeSessionImpl (bridge.ts:5861) throws 'The session is already closing', which .endsWith('The session is closing; retry after close completes') does not match. That is an argument for removing the branch rather than fixing its gate.


This review was generated by QoderWork AI

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 54f9fbced23c8dd6ce9ea6c763865a50535ef5be — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 54f9fbced23c8dd6ce9ea6c763865a50535ef5be既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.10.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants