fix(serve): Coordinate caller-supplied session IDs - #8415
Conversation
Verification reportValidated commit Automated checks
Manual reviewTwo consecutive open-ended diff audits completed after the rebase. The audits covered runtime ownership, draining generation visibility, pending-state identity and reference counts, generation-change rollback, orphan cleanup, ACP/REST error mapping, capability gates, SDK response verification, public documentation, and test assertions. Both final passes were clean. Not run locallyWindows and Linux local runs, plus authenticated real-model prompting through every client surface, were not run. The affected transport, persistence, concurrency, and shared-child failure paths are covered by focused tests; platform CI remains the final cross-platform check. |
🩺 serve daemon A/BBuilt the PR base vs this PR head
|
| field | PR base (before) | this PR (after) |
|---|---|---|
features[] |
— | "session_id_override" |
— Qwen Code · serve A/B
|
Re-run at
Moving on to code review. 🔍 中文说明在
进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
|
Code review of The normalization commit, reviewed line-by-lineSemantics are right. Boundary coverage is complete. I enumerated every session-map lookup and admission call site at this head:
Follow-on operations stay on the canonical ID — the transport test restores with a mixed-case ID, then drives No new Criticals in the delta. The two standing Suggestion-grade items from earlier passes remain open and non-blocking: the Sequence: create with a caller-supplied IDsequenceDiagram
participant P1 as SDK client
participant P2 as REST or ACP route
participant P3 as Admission coordinator
participant P4 as Bridge generation
P1->>P2: create with sessionId (capability-gated)
P2->>P3: reserveCreate with normalized id
P3->>P4: scan live owners across all generations
P3->>P3: scan pinned persistence targets
P3-->>P2: claim installed synchronously
P2->>P4: spawn with forced thread scope
P4-->>P2: session id
P2->>P2: verify honored, roll back orphan on mismatch
P2->>P3: release claim
P2-->>P1: lowercase id, attached false
Restore (load/resume) takes the same coordinator via Testing — the PR's own CI at
|
| Check | Conclusion |
|---|---|
| Qwen Code CI / Test (ubuntu-latest, Node 22.x) | ✅ success |
| Serve A/B (ubuntu-latest, Node 22.x) | ✅ success |
| Real daemon E2E / Java 11 | ✅ success |
| SDK Java / ubuntu-latest Java 11, 17, 21 | ✅ success |
| SDK Java / macos-latest Java 21 | ✅ success |
| SDK Java / windows-latest Java 21 | ✅ success |
| Desktop Shell (ubuntu-22.04, windows-2022) | ✅ success |
| web-shell E2E Smoke (ubuntu-latest, Node 22.x) | ✅ success |
The only in-flight check at this head is review-pr, a bot orchestration job — not PR CI. The green suite includes the new normalization tests (parser, admission claim identity, REST route param, wire-level ACP load/resume with follow-on prompt, stdio load/resume with follow-on cancel).
Sandboxed verification would settle the one open behavioural question at this head: the prior /verify merge-ready verdicts (2814/2814, then 2798/2798 assertions) were against pre-normalization heads, so whether the normalization commit preserves the full A/B gap is what @qwen-code /verify — already running for this head as part of this triage run, report to land in this thread — is set up to prove. Until that report lands, the delta's behavioural backing is the new regression tests plus CI, not an independent A/B round.
中文说明
对 5a166aad 的代码评审。head 自上次背书(ed122b5a)以来已移动,因此本轮评审增量而非重翻旧账:rebase 到当前 main(d91c661),冲突以叠加方式解决;以及一个新提交——fix(serve): normalize restored session IDs——它正是此前各轮建议的 R1-1 后续修复,且带真实覆盖。无人值守运行:此处未构建或执行任何 PR 代码。
归一化提交逐行评审: 语义正确——normalizeSessionIdForLookup(位于共享的 config/session-id.ts)只对严格匹配调用方 UUID 正则(v1-v5、RFC variant、大小写不敏感匹配)的值转小写;内部 -agent-* Arena ID 与遗留非 UUID ID 原样保留拼写,有直接单测钉住——这一点很重要,因为这些 ID 在别处按精确拼写比较。parseCallerSuppliedSessionId 现委托同一 helper,创建时与查找时的归一化不会漂移。边界覆盖完整:枚举了当前 head 上全部 session-map 查找与 admission 调用点——admission coordinator 的 reserveCreate/reserveRestore 入口归一化(即使绕过所有边界也会命中原一化键,有 mixed-case 同 claim 测试钉住);REST 的 requireSessionId 归一化路由参数;ACP HTTP dispatch 在入口一次性归一化 param 与 session header(header 与 param 的分歧检查因此比较规范拼写,纯大小写差异不再被误拒),load/resume 处理器再次归一化作为纵深防御;ACP HTTP 挂载的 SSE 流归属、drain 关联、session header 路径均归一化;stdio agent 的 load/resume/prompt/cancel/setMode/setModel/setConfigOption 查找前归一化,extMethod 在分发到 extMethodInternal 前归一化且是其唯一调用者——一个包装覆盖全部扩展方法扇出;其余 session-map 点以 config.getSessionId() 为键(内部即规范);任何假想的漏网查找都以 session-not-found 失败关闭,理论上也不存在安全缺口。后续操作保持在规范 ID 上——transport 测试以 mixed-case ID restore 后用同一拼写驱动 prompt 得到 end_turn;stdio 测试在归一化 load 后以原 mixed-case ID cancel。增量中无新 Critical;两条既有的 Suggestion 级事项维持不阻塞(capabilities 探测裸 catch 吞中止、三处 503 包装丢失 cause 链)。
测试证据:当前 head 的 PR 自身 CI 全绿(三个 pull_request 工作流完成;macOS/Windows Node 腿按仓库策略跳过;Java 矩阵覆盖 SDK 改动),见上方 CI 表;唯一在跑的是 bot 编排作业 review-pr,非 PR CI。绿色套件包含全部新归一化测试。沙箱验证可收口当前 head 唯一未决的行为问题:此前 /verify 的 merge-ready 判定(2814/2814、2798/2798 断言)针对的是归一化之前的 head,归一化提交是否保持完整 A/B 差距由 @qwen-code /verify 证明——它已作为本次 triage 运行的一部分对该 head 执行,报告将发布在本帖。报告落地前,增量的行为支撑是新回归测试加 CI,而非独立 A/B 轮次。
— Qwen Code · qwen3.8-max
Reviewed at 5a166aad81eee75c3469347413727478ed1ffa06 · re-run with @qwen-code /triage
|
Confidence: 3/5 — capped by the core-scale escalation policy, not by doubt: ~2,210 production lines reshaping the daemon protocol surface and both official SDKs, and the bot does not auto-approve at this scale. The human gate that cap exists to trigger has been passed — @wenshao verified the change end-to-end on real daemons and his approval stands on exactly this head ( Stepping back: this pass completed everything the last deferral asked for, and the delta is exactly what it should be. The branch is level with What remains before merge is mechanical, and maintainer-owned:
Verdict: defer — with nothing left waiting on the bot and no blocking findings. I'm not adding an approval because the core-scale escalation policy forbids the bot approving at this size, and none is needed from me: a maintainer approval already stands on this exact head. Handing back to @wenshao for the review dismissals and the merge. 中文说明置信度:3/5 —— 封顶来自核心规模升级政策,而非质疑:约 2,210 行生产代码重塑 daemon 协议面与两个官方 SDK,bot 不会在此规模上自动批准。该上限所要触发的人工关口已经通过——@wenshao 用真实 daemon 完成了端到端验证,其批准正落在本 head( 退一步看:本轮完成了上次移交所要求的全部事项,增量也恰如其分。分支重新与 合并之前剩余的是机械事项,且由 maintainer 主导:
结论:defer——已无任何事项等待 bot,也无阻塞性发现。不追加批准,因为核心规模升级政策禁止 bot 在此规模上批准,而且也不需要我批准:maintainer 的批准已落在同一 head 上。移交回 @wenshao:dismiss 评审并合并。 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Needs some rethinking before merge — details in my review comments above. Summary: the daemon-wide admission design is right and the tests pin the behavior, but this commit's own unit suite is red — the new createServeApp getSessionBridges invariant breaks two existing tests in workspace-setup-github.test.ts (the harness was not updated alongside the production wiring). Cross-package core scope also means maintainer sign-off is needed regardless. 🙏
Review/CI follow-up —
|
| Item | Decision | Action |
|---|---|---|
Linux unit suite: two workspace-setup-github tests failed because the hot-reload harness omitted the new live-bridge provider |
Fixed | Wired the harness bridge through getSessionBridges, preserving the production fail-fast invariant instead of weakening it. |
| Suggest splitting TypeScript/Java SDK and MCP support into another PR | Not taking | #8411 defines capability-gated official client reachability and response verification as part of the same end-to-end contract; splitting would temporarily advertise a daemon feature without the official clients that negotiate it. |
| Repeated admission fallback construction / positional ACP dispatcher constructor | Deferred | Non-blocking maintainability observations outside this correctness fix; changing them would widen an already large review surface without altering the requested contract. |
fix(serve) versus feat classification |
Not changing | This PR completes and hardens the partially shipped behavior from #7836; no code or release behavior depends on changing the title type during this CI fix. |
Verification on the pushed commit: npm run build, npm run typecheck, the affected CLI test file (16/16), file-scoped ESLint, and two focused diff audits all passed. There were no inline review threads to reply to or resolve (0/0).
|
Qwen Code review timed out. Qwen review timed out after 14400 seconds (of the 240-minute budget). This run already used the maximum 240 minute timeout. See workflow logs. |
|
@qwen-code /review --timeout=240 |
| _Qwen Code review request accepted. Review is queued in [workflow run](https://github.com/QwenLM/qwen-code/actions/runs/30831049787)._ |
|
Qwen Code review timed out. Qwen review timed out after 14400 seconds (of the 240-minute budget). This run already used the maximum 240 minute timeout. See workflow logs. |
|
Qwen Code review timed out. Qwen review timed out after 14400 seconds (of the 240-minute budget). For large PRs, retry with a longer timeout by commenting: |
|
Qwen Code review timed out. Qwen review timed out after 14400 seconds (of the 240-minute budget). This run already used the maximum 240 minute timeout. See workflow logs. |
1 similar comment
|
Qwen Code review timed out. Qwen review timed out after 14400 seconds (of the 240-minute budget). This run already used the maximum 240 minute timeout. See workflow logs. |
|
Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs. |
|
Rebased onto current On the blocking finding: already fixed, in
Remaining is the maintainer sign-off you flagged for the cross-package core scope, which is not something I can resolve from this side. @qwen-code /review |
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
Review:
|
yiliang114
left a comment
There was a problem hiding this comment.
LGTM, no blockers. The coordination model is verified sound: synchronous claim before async scan closes the TOCTOU, identity-checked stale release, generation pinning holds (guard object captured at dispatch entry, no await before runtime-context read), orphan rollback correct, and caller-ID validation is strict RFC-UUID (injection-safe, no Arena squatting). Cross-transport shared admission instance is real. One P2: on the REST create path, reserveCreate non-admission errors (e.g. disk-scan failure) are rethrown OUTSIDE the route's main try/catch, so Express 5 returns a non-conforming 500 (no {code} envelope) unlike every other failure mode — move the reservation inside the main try or map to a structured 503/500. P3s: conflict errors leak liveWorkspaceCwd to token-authenticated callers (consider omitting client-side); restore-path admission keys use raw (non-lowercased) IDs, asymmetric with create.
|
@qwen-code /triage |
|
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: 2664 passed · 0 failed · 2664 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:2664 通过 · 0 失败 · 2664 总计 Verification reportPR 8415 Deep Verification —
|
| Cell | Scenario | Head result | Base result (control) |
|---|---|---|---|
| C00 | /capabilities |
advertises session_id_override |
does not advertise it |
| C1 | REST create, mixed-case UUID, scope single |
200, lowercased, attached:false |
same (REST path pre-existed via #7836) |
| C2 | REST sequential duplicate | 409 session_id_conflict |
409 (route-local guard pre-existed) |
| C3a/b | explicit id + single scope |
fresh thread session, attached:false |
parity (base already forced thread at route level) |
| C4 | REST invalid id | 400 invalid_session_id |
400 |
| C5 | ACP session/new w/ live REST id |
-32602 session_id_conflict |
silently ignored — new random-id session |
| C6 | 2× concurrent ACP same id | exactly 1 winner at requested id, 1 conflict | 2 winners, both random ids |
| C7 | ACP sequential duplicate | 2nd → session_id_conflict |
2nd also created (ignored) |
| C8 | first session still live after all | 200 | 200 |
| C9 | failed load then create with same id |
404 then 200 (claim released) | same |
| C10 | reservation released after 400 (branch+worktree) | retry 200 | same |
| C11 | ACP invalid id _meta |
-32602 invalid_session_id |
accepted verbatim — random-id session |
Result: head 13/13, base 13/13 (control cells red/green in the expected
direction). The load-bearing delta is C5/C6/C7/C11 + capability advertisement — absent
on base, enforced on head.
Witnesses: evidence/01-ab-head-arm.png, evidence/02-ab-base-arm.png.
Secondary claim 1 — SDK capability negotiation (wire-oracle, refusing fake daemon)
evidence/03-sdk-capability-negotiation.png. A loopback fake daemon records every
request; the built TS SDK talks real HTTP. 4/4:
- B1 capability absent →
createOrAttachSession({sessionId})rejects with
DaemonCapabilityMissingErrorbefore anyPOST /sessionreaches the daemon. - B2 capability present → requested id serialized into the POST body; matching
response id accepted. - B3 capability present but response id differs →
DaemonSessionIdProtocolError. - B4 control — no
sessionId, capability absent → POST proceeds (negotiation only
gates the field).
Secondary claim 2 — UUID validation sweep (32/32)
evidence/04-validation-sweep.png. v1–v5 accepted; v0/v6/v7/v8/v9 rejected; variant
nibbles 8/9/a/b accepted, 0/7/c/f rejected; nil rejected; mixed case normalized to
lowercase; trailing/leading whitespace, -agent- suffix, astral char, short form, and
../escape all rejected. (First pass showed 6 "failures" that were a digit-placement bug
in my fixture generator — the version nibble must lead group 3 — not PR behavior; fixed
and re-run clean.)
Mutation matrix (no survivors; positive control included)
| # | Guard mutated | Suite that caught it | Killed by (expected red observed) |
|---|---|---|---|
| M0 | positive control — test expectation itself | session-id-admission | red as required (proves suite live) |
| M1 | disable live-bridge check in reserveCreate |
session-id-admission | "checks every live bridge…" (rejects.toMatchObject got a resolved reservation) |
| M2 | disable pending check in reserveCreate |
session-id-admission | "claims synchronously…", "…stale release…" |
| M3 | revert bridge thread-forcing (effectiveScope) |
acp-bridge bridge.test.ts |
"forces caller-supplied session ids…" (second.attached true vs expected false) |
| M4 | disable stdio-agent reserveStartingSessionId |
acpAgent.test.ts |
"rejects a concurrent duplicate requested sessionId…" (promise resolved, not rejected) |
All four PR-added guards are pinned by a real test; deleting any one turns a suite red
with the intended behavioral assertion. Witness: evidence/05-mutation-livecheck-disabled.png.
Targeted gates (unmodified head, exact counts)
- cli
session-id-admission+session-id+acp-http/transport+workspace-qualified-acp: 369/369 - cli
server+acpAgent+workspace-setup-github: 1243/1243 - acp-bridge
bridge.test.ts: 472/472 - sdk-typescript (
AcpWsTransport,DaemonClient,acpRouteTable,serve-bridge): 477/477 - integration
cli/qwen-serve-routes.test.ts(real daemon, includes the new
"honors and reserves a normalized caller-supplied session ID"): 36/36
Corrections
None required — no earlier round or bot comment misdescribed the code here (first round).
Findings
No blocking findings. Two clarifications that a reviewer may find useful, neither a defect:
- The bridge-level thread-forcing is defense-in-depth for the REST path but
load-bearing for direct/ACP callers. The REST route already forcedthreadscope for
a caller-supplied id (pre-existing, from feat(serve): support caller-supplied sessionId in POST /session #7836), so my initial A/B hypothesis that REST
would diverge onsessionScope:'single'was wrong — base and head are at parity there.
The bridge hunk (effectiveScope = sessionId ? 'thread' : …) is nonetheless real and is
pinned bybridge.test.ts(mutation M3 killed it). Not a problem; just calibrating where
the change actually bites. dist/cli.jsis a chunked esbuild bundle. Greppingdist/cli.jsalone for
session_id_override/session_id_conflictreturns nothing; the markers live in
dist/chunks/*.js, which I confirmed present. Bundle is current head code (built
2026-08-05 08:20, matches CI window). Methodology note, not a finding.
Not covered
- Java SDK tests (
DaemonSessionClientTest, etc.) — the container has nojavaor
mvn(command not found), so the Java side (capability gate + ID-mismatch →
SessionCreationOutcomeUnknownException) was verified by reading the diff only, not executed. - Base-arm integration suite —
integration-tests/globalSetup.tsunconditionally sets
TEST_CLI_PATHto the headdist/cli.js, overriding my base-CLI env, so a base run of the
vitest integration file actually exercised the head bundle. I did not count it as a base
comparison; the base contrast for the same behavior comes fromdaemon-ab.mjs --arm base
instead. - Per-commit attribution — depth-2 shallow checkout exposes only the merge commit,
HEAD^1(base tip), andHEAD^2(PR head); the 5 intermediate commits in the metadata are
unreachable, so I verified the aggregateHEAD^1..HEADdiff, not each commit. - Workspace-qualified / draining-generation and worktree-sidecar persistence paths were
exercised through the PR's own unit + integration tests (all green) but not re-driven through
a hand-built multi-workspace daemon in this round. - No repo-wide test sweep; I ran only the affected workspaces' relevant files.
Methodology
Environment: GitHub-Actions node:22-bookworm container, repo at
/__w/qwen-code/qwen-code checked out at refs/pull/8415/merge (depth 2), npm ci +
npm run build pre-run. Harnesses live in this artifact dir and are re-runnable:
daemon-ab.mjs (real-daemon REST+WS A/B), sdk-capability.mjs (refusing fake daemon),
validation-probe.mjs (dist parseCallerSuppliedSessionId). Base control: git worktree add tmp/base-tree HEAD^1, node_modules symlinked from the root install with each
@​qwen-code/* link re-pointed into the base tree (realpath asserted: core/bridge/cli all
resolved to tmp/base-tree/...); web-templates and cli rebuilt in-tree after generating
the gitignored git-commit.ts. Base-arm daemon purity confirmed by absence of
session-id-admission.js/session-id.js and session_id_override in the base dist.
Raw per-cell stdout/stderr are in logs/ (ab-head.txt, ab-base.txt, sdk-head.txt,
validation-probe.txt) and build logs under tmp/. Mutation reverts were done on scratch
edits and restored with git checkout -- after each run; git status is clean.
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
yiliang114
left a comment
There was a problem hiding this comment.
LGTM — approving at 5a166aad81. Spot-checked the two items I raised in earlier rounds: the restore/load/resume admission now keys on normalizeSessionIdForLookup at both reserveCreate and reserveRestore (case-variant restore claims can no longer slip past live/pending admission, while internal -agent-* and legacy IDs stay untouched), and the disk-scan failure path is wrapped into a retryable session_id_admission_unavailable that the route maps to a structured 503 — so no non-conforming bare 500 escapes the reservation block, and the admission-side catch still releases the reservation on any unexpected throw. Synchronous-claim-before-async-scan, generation pinning via the captured assertion, and orphan cleanup all hold at this head. Deferred Suggestions per the review-budget rule are fine as follow-ups.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: reverse audit — stopped before round 2 by the review time budget.
中文说明
未审查:反向审计——评审时间预算不足,未能开始第 2 轮。
— qwen3.8-max via Qwen Code /review (v0.21.7)
| const rollbackRestore = async (): Promise<void> => { | ||
| if (restored.attached) { |
There was a problem hiding this comment.
[Suggestion] rollbackRestore swallows every cleanup error, making the teardown-race diagnostic unreachable. Both branches attach .catch(() => {}) before awaiting, so the function can never reject and the call-site void rollbackRestore().catch((err) => writeStderrLine(... teardown-race ...)) (~line 1771) is dead — orphan-cleanup failures on the restore race path are silently dropped. The pre-diff code logged qwen serve: /acp orphan kill(<id>) teardown-race: <err> on rejection, and the sibling session/new path still preserves the diagnostic via removeOrphanSession; the still-written-but-dead writeStderrLine shows the diagnostic was meant to survive this refactor. — Concrete cost: during session/load, a racing session/close whose killSession fails (bridge error, bridge draining) leaves a lingering orphan session process with no operator signal, where operators previously had a stderr trace. Suggested fix: drop the two inner .catch(() => {}) so the existing call-site .catch fires; swallow explicitly (await rollbackRestore().catch(() => {})) at the two generation-assert rollback sites so the original error still propagates.
中文说明
rollbackRestore 吞掉所有清理错误,使 teardown-race 诊断不可达。两个分支都在 await 之前挂上了 .catch(() => {}),因此该函数永远不会 reject,调用点 void rollbackRestore().catch((err) => writeStderrLine(... teardown-race ...))(约第 1771 行)成为死代码——restore 竞态路径上的 orphan 清理失败会被静默丢弃。改动前的代码会在 rejection 时记录 qwen serve: /acp orphan kill(<id>) teardown-race: <err>,且同级的 session/new 路径仍通过 removeOrphanSession 保留该诊断;仍然写出但已不可达的 writeStderrLine 表明本意是让该诊断在重构后继续存在。— 具体代价:在 session/load 期间,一个竞态的 session/close 若其 killSession 失败(bridge 错误、bridge 正在排空),会留下一个残留的 orphan session 进程且没有任何运维信号,而此前运维人员有一条 stderr 线索。建议修复:去掉两处内部 .catch(() => {}),让既有的调用点 .catch 生效;在两处 generation-assert 回滚点显式吞错(await rollbackRestore().catch(() => {})),使原始错误仍然传播。
— qwen3.8-max via Qwen Code /review (v0.21.7)
| async prompt(params: PromptRequest): Promise<PromptResponse> { | ||
| const session = this.sessions.get(params.sessionId); | ||
| const sessionId = normalizeSessionIdForLookup(params.sessionId); | ||
| const session = this.sessions.get(sessionId); | ||
| if (!session) { |
There was a problem hiding this comment.
[Suggestion] Five normalized stdio-agent entry points have no test. This diff adds normalizeSessionIdForLookup to prompt, setSessionMode, unstable_setSessionModel, setSessionConfigOption, and extMethod, but only load/resume/cancel normalization is exercised (the file's single toUpperCase call is in the load/resume test). The mixed-case session/prompt in transport.test.ts does not count: the daemon dispatcher normalizes params before anything reaches the child, so that test cannot exercise this child-side normalization for direct-stdio clients. — Failure scenario: a client driving the ACP child directly sends session/prompt or session/set_session_mode with the mixed-case spelling it originally requested; if any of these five calls is later deleted, this.sessions.get(UPPERCASE) misses → "Session not found", and no test in the repo turns red. Suggested fix: extend the existing mixed-case it.each test: create the session, then call agent.prompt / agent.setSessionMode with sessionId.toUpperCase() and assert the call reaches the session (sendUpdate/setMode invoked) instead of throwing.
中文说明
五个做了归一化的 stdio agent 入口没有测试。本 diff 为 prompt、setSessionMode、unstable_setSessionModel、setSessionConfigOption 和 extMethod 添加了 normalizeSessionIdForLookup,但只有 load/resume/cancel 的归一化被测试覆盖(文件中唯一的 toUpperCase 调用在 load/resume 测试里)。transport.test.ts 中的混合大小写 session/prompt 不算数:daemon dispatcher 在请求到达子进程之前就已归一化参数,因此该测试无法覆盖为直连 stdio 客户端提供的这层子进程侧归一化。— 失败场景:直接驱动 ACP 子进程的客户端用其最初请求时的混合大小写拼写发送 session/prompt 或 session/set_session_mode;若这五处调用中任何一处日后被删除,this.sessions.get(大写) 会未命中 → "Session not found",且仓库中没有任何测试变红。建议修复:扩展现有的混合大小写 it.each 测试:先创建 session,再用 sessionId.toUpperCase() 调用 agent.prompt / agent.setSessionMode,断言调用到达了该 session(sendUpdate/setMode 被调用)而不是抛错。
— qwen3.8-max via Qwen Code /review (v0.21.7)
| expect(res.status).toBe(400); | ||
| }); | ||
|
|
||
| it('honors and reserves a normalized caller-supplied session ID', async () => { |
There was a problem hiding this comment.
[Suggestion] The integration coverage for this feature is gated only by the E2E workflow, which is skipped at this commit. The new/changed integration tests live outside every npm workspace, so npm run test never collects them; the pre-submission check report for this commit lists Integration Tests (CLI, No Sandbox) among the skipped checks, so the behaviors exercised only here (UUID normalization + reservation, 409 session_id_conflict, session_id_override capability) would merge with no CI gate executing them. Mitigating evidence: this review ran the changed file against a freshly built production bundle — 36/36 passed. — Concrete cost: if the E2E job stays skipped (or is not required) on this PR, a regression in these integration-only paths ships uncaught by any unit gate. Suggested fix: no code change needed — run/require the integration workflow (test:integration:sandbox:none) at this head before merge.
中文说明
本特性的集成覆盖只由 E2E workflow 把关,而该 job 在本 commit 上被跳过。新增/修改的集成测试位于所有 npm workspace 之外,因此 npm run test 永远不会收集它们;本 commit 的提交前检查报告将 Integration Tests (CLI, No Sandbox) 列在被跳过的检查中,所以只在这里覆盖的行为(UUID 归一化 + 预约、409 session_id_conflict、session_id_override capability)将在没有任何 CI 门禁执行它们的情况下合并。缓解证据:本次评审用新构建的 production bundle 运行了被修改的文件——36/36 通过。— 具体代价:如果 E2E job 在本 PR 上保持跳过(或非必需),这些仅集成覆盖的路径上的回归将不会被任何单测门禁捕获。建议修复:无需代码改动——合并前在本 head 上运行/要求集成 workflow(test:integration:sandbox:none)。
— qwen3.8-max via Qwen Code /review (v0.21.7)
| if (typeof params['sessionId'] === 'string') { | ||
| params['sessionId'] = normalizeSessionIdForLookup(params['sessionId']); | ||
| } |
There was a problem hiding this comment.
[Suggestion] Plural sessionIds batch surfaces skip the normalization the singular surfaces apply. The singular sessionId param/header is normalized here (and at the REST requireSessionId), but parseSessionIds (dispatch, serving qwen/sessions/archive|delete|unarchive) and parseSessionIdsBody (routes/session.ts, serving REST /sessions/delete|archive|unarchive) pass caller IDs verbatim; downstream bridge.closeSession and the exact-path persisted probes then miss case-variant IDs. The protocol doc's own ACP example sends an uppercase UUID, so the asymmetry is reachable by documented usage. — Failure scenario: a client creates a session with a mixed-case caller ID (stored lowercase), then archives/deletes it using its original spelling: the batch op reports notFound and silently no-ops, while session/load with the identical spelling succeeds. Suggested fix: normalize each element where the plural IDs enter — sessionIds.map((s) => normalizeSessionIdForLookup(s)) in both parseSessionIds and parseSessionIdsBody.
中文说明
复数 sessionIds 批处理入口跳过了单数入口所应用的归一化。单数 sessionId 参数/请求头在此处(以及 REST 的 requireSessionId)被归一化,但 parseSessionIds(dispatch 中,服务 qwen/sessions/archive|delete|unarchive)与 parseSessionIdsBody(routes/session.ts 中,服务 REST /sessions/delete|archive|unarchive)原样传递调用方 ID;下游 bridge.closeSession 与精确路径的持久化探测因此会未命中大小写变体的 ID。协议文档自己的 ACP 示例就发送大写 UUID,所以该不对称按文档用法即可触达。— 失败场景:客户端用混合大小写的调用方 ID 创建 session(以小写存储),随后用其原始拼写执行 archive/delete:批处理操作报告 notFound 并静默不生效,而用完全相同拼写的 session/load 却能成功。建议修复:在复数 ID 入口处对每个元素做归一化——在 parseSessionIds 与 parseSessionIdsBody 中使用 sessionIds.map((s) => normalizeSessionIdForLookup(s))。
— qwen3.8-max via Qwen Code /review (v0.21.7)
| getSessionBridges: () => | ||
| registry.listManaged().map((runtime) => runtime.bridge), |
There was a problem hiding this comment.
[Suggestion] No test wiring of getSessionBridges exercises draining-generation visibility. Every test wiring copies createServeApp's registry-managed-only fallback (here and at ~line 3739) or a static single bridge, so the suite never exercises the property the dependency exists for — admission seeing sessions live on a replaced-but-draining bridge generation. listManaged() drops the old runtime the moment activateReplacement swaps entry.current, while production's runtimeBridges retains it until shutdown confirms. — Failure scenario: if run-qwen-serve.ts's getSessionBridges: () => runtimeBridges regressed to a registry-derived enumeration, sessions live on a draining bridge would become invisible to create admission between replacement activation and confirmed shutdown — POST /session reusing such an ID would pass the live scan and spawn a duplicate live session, and every existing test would stay green. Suggested fix: add one createServeApp test injecting getSessionBridges: () => [newBridge, oldDrainingBridge] where oldDrainingBridge is not part of the injected registry, have its getSessionSummary report a live session, and assert POST /session with that ID returns 409 session_id_conflict (conflict: 'live').
中文说明
getSessionBridges 的所有测试接线都没有覆盖排空代际(draining generation)可见性。每处测试接线要么复制 createServeApp 的仅 registry 管理回退(此处与约第 3739 行),要么是静态单个 bridge,因此套件从未覆盖该依赖存在的意义——准入要能看到仍存活于被替换但正在排空的 bridge 代际上的 session。listManaged() 在 activateReplacement 换掉 entry.current 的瞬间就丢弃旧 runtime,而生产环境的 runtimeBridges 会保留它直到 shutdown 确认。— 失败场景:如果 run-qwen-serve.ts 的 getSessionBridges: () => runtimeBridges 回归为基于 registry 的枚举,在替换激活与确认 shutdown 之间,排空 bridge 上存活的 session 将对 create 准入不可见——复用该 ID 的 POST /session 会通过存活扫描并派生重复的存活 session,而所有现有测试仍为绿色。建议修复:新增一个 createServeApp 测试,注入 getSessionBridges: () => [newBridge, oldDrainingBridge],其中 oldDrainingBridge 不属于注入的 registry,让其 getSessionSummary 报告一个存活 session,并断言使用该 ID 的 POST /session 返回 409 session_id_conflict(conflict: 'live')。
— qwen3.8-max via Qwen Code /review (v0.21.7)
| const restFetch = vi | ||
| .fn<typeof globalThis.fetch>() | ||
| .mockResolvedValueOnce( |
There was a problem hiding this comment.
[Suggestion] vi.fn<typeof globalThis.fetch>() is a TS2344 under this package's pinned vitest. packages/sdk-typescript pins vitest ^1.6.0 (the lockfile resolves a nested 1.6.1, whose @vitest/spy declares fn<TArgs extends any[], R>() — a function type violates the any[] constraint; the single-function-type form arrived in vitest 2). Measured with tsc over this file: error TS2344 at exactly this line. Latent today because the package's typecheck excludes test/ and vitest's esbuild transform erases types (the suite passes 34/34 on 1.6.1), but the file is red in any IDE TS server and becomes a hard CI error once typechecking is extended to test/. The sibling recordingFetch in DaemonClient.test.ts uses the implementation form, valid under both majors. — Concrete cost: red squiggles in every IDE today; a CI break the moment someone extends typechecking to tests; the mismatch also masks that this package is still on vitest 1.6 while every other workspace is on ^3.x.
| const restFetch = vi | |
| .fn<typeof globalThis.fetch>() | |
| .mockResolvedValueOnce( | |
| const restFetch = vi | |
| .fn() | |
| .mockResolvedValueOnce( |
中文说明
vi.fn<typeof globalThis.fetch>() 在本包锁定的 vitest 下是 TS2344。packages/sdk-typescript 锁定 vitest ^1.6.0(lockfile 解析出嵌套的 1.6.1,其 @vitest/spy 声明为 fn<TArgs extends any[], R>()——函数类型不满足 any[] 约束;单函数类型泛型形式是 vitest 2 才引入的)。用 tsc 对该文件实测:恰好在这一行报 error TS2344。当前处于潜伏状态,因为该包的 typecheck 排除了 test/,且 vitest 的 esbuild 转换会擦除类型(套件在 1.6.1 下 34/34 通过),但该文件在任何 IDE 的 TS server 中都是红的,一旦 typecheck 扩展到 test/ 就会成为硬性 CI 错误。DaemonClient.test.ts 中的同类 recordingFetch 使用实现形式,在两个大版本下均合法。— 具体代价:当前每个 IDE 都会报红;一旦有人把 typecheck 扩展到测试就会 break CI;该错配还掩盖了本包仍停留在 vitest 1.6 而其他所有 workspace 都在 ^3.x 的事实。
— qwen3.8-max via Qwen Code /review (v0.21.7)
Complete daemon-wide admission across REST, ACP, workspace generations, SDKs, and MCP. Closes QwenLM#8411 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…nLM#8415) Restore the observability and fail-loud guarantees flagged in review: log every session-id admission routing failure, name the live foreign owner workspace in restore conflicts, make the ACP dispatcher's admission dependency required so load/resume cannot run on a mount without one, and require mountAcpHttp hosts to inject the daemon-wide admission instead of silently building a weak fallback. Harden the SDK WS transport against environments without global fetch and against non-capabilities 200 envelopes, and align the design doc with the implemented restore-sharing and persistence-failure semantics.
Preserve REST capability errors, fail closed on malformed envelopes, retain restore routing diagnostics, and align retry documentation. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
5a166aa to
13021d2
Compare
|
Resolved the merge conflict by rebasing onto origin/main at 4a79517. The only manual resolution kept main’s expanded integration-test TypeScript path map, which already includes the two SDK source mappings this PR added; range-diff confirmed the remaining four commits are patch-equivalent. Local verification passed: build, bundle, typecheck, lint, integration TypeScript compile, 36/36 real-daemon route tests, 499/499 ACP bridge tests, targeted CLI/TypeScript SDK tests, and Java SDK Maven tests (127 tests, 5 skipped). |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Fixed the mixed-case legacy session regression in 7efde4d.
Validated with targeted core and CLI tests plus build and typecheck. Resolved 1 review thread. |
Local verification on real daemons — merge-readyI built both sides from source and drove them over the wire. Everything below is a live exchange with a daemon started from that side's own bundle; the probe scripts speak raw HTTP / WebSocket JSON-RPC / ndjson and import no PR code, so the same probe file runs unchanged against base and head. Setup. Two isolated worktrees, each with its own 1. Daemon surface — REST + ACP/WebSocketThe four rows that change are the ones worth merging for:
Rows 6–8 are unchanged base vs head, i.e. no regression in the paths #7836 already shipped: mixed-case → lowercase, 2. The shared stdio ACP agent — this is the sharp edgeOn base, On head every one of those is 3. Official TypeScript SDK against real daemons
4. Suites re-run at this head
5. One flake, and it is not yoursThe single CLI failure was 6. Not covered by this roundLive workspace-qualified ACP against a draining bridge generation (I exercised the primary ACP endpoint only; the generation-guard paths are covered by the new unit tests, not by my live probes), Windows and Linux, authenticated real-model prompting through every client, and UUID v7 / historical duplicate migration — all consistent with what the description already scopes out. Verdict: merge-ready. The behaviour the description claims is the behaviour the daemons actually exhibit, the previously silent ACP paths now fail loudly and structurally, and the unvalidated stdio ID is closed. 中文版本本地真实 daemon 验证 —— 可以合并我在本地把两侧都从源码构建出来,然后走真实协议驱动。下面每一条都是与「用该侧自己 bundle 启动的 daemon」的一次真实交互;探针脚本只讲原始 HTTP / WebSocket JSON-RPC / ndjson,不 import 任何 PR 代码,所以同一个探针文件对 base 和 head 原样运行。 环境。 两个隔离 worktree,各自 1. Daemon 层面 —— REST + ACP/WebSocket(截图 1)真正发生变化、也是值得合并的四行:
第 6–8 行 base 与 head 一致,即 #7836 已交付的路径没有回归:mixed-case 转小写、即使 2. 共享 stdio ACP agent —— 这里是真正的锋利处(截图 2)在 base 上, 在 head 上,这些全部变成带结构化 3. 官方 TypeScript SDK 对真实 daemon(截图 3)
4. 在当前 head 重跑的测试
5. 有一个 flake,但不是这个 PR 的CLI 唯一那条失败是 6. 本轮未覆盖针对正在 draining 的 bridge generation 的 workspace-qualified ACP 实测(我只驱动了 primary ACP 端点;generation guard 路径由新增单测覆盖,不在我的实时探针内)、Windows 与 Linux、带真实认证的模型 prompt 全客户端串联,以及 UUID v7 / 历史重复 ID 迁移 —— 这些与描述中已声明的范围外内容一致。 结论:可以合并。 描述所声称的行为,就是 daemon 实际表现出来的行为;此前静默的 ACP 路径现在会大声且结构化地失败;stdio 侧未校验的 session ID 已经关闭。 |
There was a problem hiding this comment.
Pull request overview
This PR extends the “caller-supplied daemon session ID” contract into a single daemon-wide behavior, coordinating create/load/resume admission across REST, ACP (primary + workspace-qualified), draining runtime generations, stdio agent validation, and official TypeScript/Java SDKs (plus daemon MCP).
Changes:
- Introduces a shared requested-session-id admission coordinator to reject live/pending/persisted conflicts across all registered runtimes and bridge generations.
- Adds capability-gated caller-supplied session ID support (
session_id_override) across REST, ACP metadata (qwen-code/sessionId), daemon MCP tooling, and TypeScript/Java SDK clients with response verification. - Improves normalization/compat handling for mixed-case UUID filenames and session ID routing across transports and runtime lifecycle events.
Reviewed changes
Copilot reviewed 48 out of 48 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/sdk-typescript/test/unit/serve-bridge.test.ts | Adds MCP bridge tool tests for capability-gated session_create.session_id forwarding. |
| packages/sdk-typescript/test/unit/DaemonClient.test.ts | Adds SDK tests for gating, serialization, and response verification for caller-supplied sessionId. |
| packages/sdk-typescript/test/unit/AcpWsTransport.test.ts | Adds tests ensuring capability discovery prefers REST and handles missing/malformed REST responses. |
| packages/sdk-typescript/test/unit/acpRouteTable.test.ts | Tests mapping POST /session sessionId into ACP _meta["qwen-code/sessionId"]. |
| packages/sdk-typescript/src/utils/validation.ts | Updates UUID validation comment to reflect v1–v5. |
| packages/sdk-typescript/src/index.ts | Exports DaemonSessionIdProtocolError. |
| packages/sdk-typescript/src/daemon/index.ts | Re-exports DaemonSessionIdProtocolError from daemon entrypoint. |
| packages/sdk-typescript/src/daemon/DaemonClient.ts | Gates caller-supplied sessionId on session_id_override and verifies returned ID matches. |
| packages/sdk-typescript/src/daemon/AcpWsTransport.ts | Prefers REST /capabilities over ACP initialize result; adds robust fallback. |
| packages/sdk-typescript/src/daemon/acpRouteTable.ts | Maps REST sessionId onto ACP session/new metadata key. |
| packages/sdk-typescript/src/daemon-mcp/serve-bridge/tools/session.ts | Exposes session_create.session_id and forwards into SDK create request. |
| packages/sdk-typescript/src/daemon-mcp/serve-bridge/README.md | Documents session_create.session_id behavior and capability gating. |
| packages/sdk-java/qwencode/src/test/java/com/alibaba/qwen/code/daemon/DaemonSessionClientTest.java | Adds Java SDK tests for capability gating, serialization, and mismatch handling. |
| packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/daemon/DaemonClient.java | Enforces session_id_override capability and validates returned sessionId vs requested. |
| packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/daemon/CreateSessionRequest.java | Adds sessionId to create request builder and JSON serialization. |
| packages/sdk-java/qwencode/README.md | Documents caller-supplied session ID usage and mismatch outcome semantics. |
| packages/core/src/services/sessionService.ts | Adds findSessionIdIgnoringCase for legacy mixed-case transcript filenames. |
| packages/core/src/services/sessionService.test.ts | Adds unit test for findSessionIdIgnoringCase. |
| packages/cli/src/serve/session-id-admission.ts | Introduces daemon-wide requested-session-id admission coordinator (live/pending/persisted). |
| packages/cli/src/serve/session-id-admission.test.ts | Adds tests for admission behavior, conflicts, failure modes, and release semantics. |
| packages/cli/src/serve/server/request-helpers.ts | Normalizes route session IDs via normalizeSessionIdForLookup. |
| packages/cli/src/serve/server/request-helpers.test.ts | Tests UUID route parameter normalization behavior. |
| packages/cli/src/serve/server.ts | Wires shared admission into serve app and requires live bridge enumeration when generations can change. |
| packages/cli/src/serve/server.test.ts | Updates serve tests for new capability tag and cross-transport admission behaviors. |
| packages/cli/src/serve/run-qwen-serve.ts | Injects getSessionBridges to allow admission to see draining generations. |
| packages/cli/src/serve/routes/workspace-setup-github.test.ts | Updates harness to pass getSessionBridges under hot reload. |
| packages/cli/src/serve/routes/session.ts | Replaces per-route guards with shared admission; adds generation-open assertions before side effects. |
| packages/cli/src/serve/capabilities.ts | Advertises session_id_override capability. |
| packages/cli/src/serve/acp-http/workspace-qualified-acp.test.ts | Adds tests for admission sharing and generation guard behaviors in qualified ACP mounts. |
| packages/cli/src/serve/acp-http/transport.test.ts | Extends ACP transport tests for requested session ID validation/normalization and orphan rollback. |
| packages/cli/src/serve/acp-http/index.ts | Normalizes session IDs in ACP HTTP plumbing and injects shared admission + runtime context getters. |
| packages/cli/src/serve/acp-http/dispatch.ts | Adds admission coordination + validation for ACP session/new meta and load/resume restore claims. |
| packages/cli/src/serve/acp-http/client-mcp-ws.test.ts | Updates client-MCP-over-WS setup to provide admission dependencies. |
| packages/cli/src/config/session-id.ts | Adds shared session-id parsing/normalization utilities (internal vs caller-supplied). |
| packages/cli/src/config/session-id.test.ts | Adds unit tests for parser/normalizer and internal ID validity. |
| packages/cli/src/config/config.ts | Moves isValidSessionId to shared session-id.ts module and re-exports it. |
| packages/cli/src/acp-integration/acpAgent.ts | Adds defense-in-depth validation, concurrency guards, normalization, and structured conflicts in stdio agent. |
| packages/cli/src/acp-integration/acpAgent.test.ts | Adds tests for invalid meta rejection, duplicate startup conflicts, and normalization on restore paths. |
| packages/acp-bridge/src/bridge.ts | Forces caller-supplied session IDs to thread scope and includes them in fresh-session admission context. |
| packages/acp-bridge/src/bridge.test.ts | Extends bridge test coverage for thread scope forcing + admission context + ACP meta injection. |
| integration-tests/vitest.config.ts | Adds alias for built daemon transports bundle in integration tests. |
| integration-tests/cli/qwen-serve-routes.test.ts | Adds integration coverage for end-to-end session ID override behavior and ACP WS transport path. |
| docs/developers/sdk-typescript.md | Documents TypeScript SDK caller-supplied sessionId with capability gating and response verification. |
| docs/developers/sdk-java.md | Documents Java SDK caller-supplied session IDs and mismatch outcome behavior. |
| docs/developers/qwen-serve-protocol.md | Updates protocol docs for session_id_override and requested-ID semantics across REST/ACP. |
| docs/developers/daemon/11-capabilities-versioning.md | Adds session_id_override to capability tag inventory. |
| docs/developers/daemon/08-session-lifecycle.md | Adds session_id_override to lifecycle capability tag list. |
| docs/design/2026-08-01-caller-supplied-session-id.md | Adds design doc describing the unified daemon-wide contract and ownership boundaries. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| headers: this.headers({ 'Content-Type': 'application/json' }, clientId), | ||
| body: JSON.stringify({ | ||
| cwd: req.workspaceCwd, | ||
| ...(req.sessionId !== undefined ? { sessionId: req.sessionId } : {}), |
| if (sessionId === undefined) { | ||
| return { ...rest, ...(_meta !== undefined ? { _meta } : {}) }; | ||
| } |
Resolves the conflict in acpAgent.ts left by 60458f5 (fix(serve): Coordinate caller-supplied session IDs, QwenLM#8415). Both sides touched `loadSession` and `unstable_resumeSession`. Upstream wrapped each in a caller-supplied-id reservation — `reserveStartingSessionId` plus a `normalizeSessionIdForLookup` / `findSessionIdIgnoringCase` preamble — which re-indented the whole body into a new try/finally. This branch's change to the same two functions is the Goal v3 migration: drop the `#restoreGoalOnResume` hook, its two call sites, and the `supersedeUnrestorableGoal` replay option, since the Goal runtime now owns restore and the option no longer exists on `collectHistoryReplayUpdates`. The two are orthogonal, so the resolution keeps upstream's control flow verbatim and re-applies this branch's deletions inside it. Verified: npm run build, npm run typecheck, packages/cli src/acp-integration/ (28 files, 1344 tests) and packages/core src/goals/ (15 files, 353 tests) all pass.
|
Released in v0.21.9. |








What this PR does
This PR completes caller-supplied daemon session IDs as one daemon-wide contract across REST, primary and workspace-qualified ACP, live workspace generations, the shared stdio agent, the official TypeScript and Java daemon SDKs, and daemon MCP. It validates RFC-variant UUID v1-v5 values, normalizes them to lowercase, forces explicit IDs to create fresh thread sessions, and verifies that downstream creation honored the requested ID.
Creation and recovery now share one admission coordinator. Creation rejects conflicts with live bridges, in-flight operations, active or archived transcripts, and worktree-backed history across every currently registered runtime. Recovery can share an in-flight claim only inside the same bridge generation; another workspace or generation receives the existing workspace-conflict contract. Draining and replaced bridge generations remain visible until their shutdown is confirmed, and runtime-specific persistence checks use each generation's pinned output directory.
ACP creation accepts
session/new._meta["qwen-code/sessionId"], while load and resume participate in the same recovery admission as REST. The bridge and stdio agent provide defense in depth for direct callers, return structured conflicts without terminating the shared child, and clean up a newly created orphan if a downstream component returns a different ID. Clients negotiate the newsession_id_overridecapability before mutation and verify successful responses; older daemons therefore cannot silently ignore the optional field.Why it's needed
PR #7836 established the REST creation path, but route-local coordination could still allow REST and ACP to race, reuse an ID in another workspace, or overlap recovery across runtime generations. Those duplicates make session ownership ambiguous and can let one transport interfere with a session created by another. SDK callers also lacked a negotiated way to request an ID and detect a daemon that ignored it. This change closes those gaps as a single coherent behavior instead of leaving transport- or workspace-specific follow-ups.
Reviewer Test Plan
How to verify
attached: false, even if the request asks for single scope. Repeat the same UUID through REST or ACP and confirm a stable409 session_id_conflictwithout affecting the first session.session_workspace_conflict.INVALID_PARAMS, performs no settings or filesystem work before rejection, and keeps sibling sessions and the shared child alive.session_create.session_id. Confirm they refuse to mutate whensession_id_overrideis absent, serialize the requested ID when present, and reject a success response containing a different ID.Local verification passed: CLI-focused Vitest 730/730, ACP bridge Vitest 469/469, TypeScript SDK Vitest 477/477, Java Maven tests 127/127 with 5 environment-dependent skips, and real bundled-daemon route integration 36/36.
npm run build,npm run bundle,npm run typecheck, andnpm run lintalso passed.Evidence (Before & After)
N/A — this changes daemon protocols, SDKs, and concurrency behavior without a TUI or other visual surface.
Tested on
Environment (optional)
macOS 26.4.1, Node.js 22.22.3, npm 10.9.8, and Java 21.0.8. The daemon route integration ran against the rebuilt production bundle with sandboxing disabled.
Risk & Scope
session_id_override. An explicit ID always means create a fresh thread session rather than idempotently attach, and callers with an ambiguous creation outcome should load or resume the known ID.Linked Issues
Closes #8411
Completes the caller-supplied ID work introduced by #7836.
中文说明
本 PR 做了什么
本 PR 将调用方指定 daemon session ID 的能力完整收口为一个 daemon 级统一契约,覆盖 REST、primary 与 workspace-qualified ACP、仍存活的 workspace generation、共享 stdio agent、官方 TypeScript 与 Java daemon SDK,以及 daemon MCP。实现会校验 RFC variant 的 UUID v1-v5、统一转为小写、强制显式 ID 创建全新的 thread session,并核验下游实际创建结果是否采用了请求的 ID。
创建与恢复现在共享同一个 admission coordinator。创建会检查所有当前注册 runtime 中的 live bridge、进行中的操作、active/archived transcript 和 worktree-backed history 并拒绝冲突。恢复仅允许同一个 bridge generation 共享进行中的 claim;另一个 workspace 或 generation 会沿用现有 workspace conflict 契约。Draining 或已被替换的 bridge generation 在确认 shutdown 前始终可见,并且 runtime 级持久化检查使用各 generation 固定的输出目录。
ACP 创建支持
session/new._meta["qwen-code/sessionId"],load 和 resume 也会与 REST 一起进入同一恢复 admission。Bridge 与 stdio agent 为直接调用方提供纵深防御,返回结构化冲突而不终止共享 child,并在下游返回不同 ID 时清理本次新建的 orphan。客户端会在 mutation 前协商新的session_id_overridecapability,并再次核验成功响应,因此旧 daemon 无法静默忽略这个可选字段。为什么需要它
PR #7836 建立了 REST 创建路径,但路由局部的协调仍可能让 REST 与 ACP 竞态、在另一个 workspace 重用同一 ID,或让不同 runtime generation 的恢复过程重叠。这类重复会造成 session owner 歧义,也可能让一种 transport 干扰由另一种 transport 创建的 session。SDK 调用方同样缺少经过 capability 协商的指定 ID 能力,无法发现 daemon 静默忽略字段。本变更将这些缺口作为一个一致行为统一解决,不再遗留 transport 或 workspace 维度的 follow-up。
Reviewer 测试计划
如何验证
attached: false,即使请求指定 single scope 也一样。随后通过 REST 或 ACP 再次使用同一 UUID,确认稳定返回409 session_id_conflict,且第一个 session 不受影响。session_workspace_conflict。INVALID_PARAMS,拒绝前不执行 settings 或 filesystem 工作,并保持 sibling session 和共享 child 存活。session_create.session_id。确认缺少session_id_override时不会发出 mutation,capability 存在时正确序列化指定 ID,并在成功响应包含不同 ID 时拒绝该响应。本地验证已通过:CLI 定向 Vitest 730/730、ACP bridge Vitest 469/469、TypeScript SDK Vitest 477/477、Java Maven 测试 127/127(另有 5 个依赖环境的 skip),以及真实 bundle daemon 路由集成测试 36/36。
npm run build、npm run bundle、npm run typecheck和npm run lint也全部通过。证据(Before & After)
N/A——本变更涉及 daemon protocol、SDK 与并发行为,不包含 TUI 或其他可视化界面变化。
测试平台
环境(可选)
macOS 26.4.1、Node.js 22.22.3、npm 10.9.8、Java 21.0.8。Daemon 路由集成测试使用重新构建的 production bundle,并关闭 sandbox。
风险与范围
session_id_override。显式 ID 始终表示创建一个全新的 thread session,而不是幂等 attach;如果创建结果不确定,调用方应使用已知 ID 执行 load 或 resume。关联 Issue
Closes #8411
本 PR 完成由 #7836 引入的 caller-supplied ID 工作。