Skip to content

fix(serve): support reserved characters in virtual subagent ids - #8717

Merged
wenshao merged 18 commits into
QwenLM:mainfrom
carffuca:fix/virtual-subagent-session-id-parts
Aug 12, 2026
Merged

fix(serve): support reserved characters in virtual subagent ids#8717
wenshao merged 18 commits into
QwenLM:mainfrom
carffuca:fix/virtual-subagent-session-id-parts

Conversation

@carffuca

@carffuca carffuca commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Allows virtual subagent session IDs to represent existing agent task IDs that contain reserved characters such as : and /. Parent session IDs remain strictly validated, while agent IDs must be non-empty, bounded, and losslessly round-trip through UTF-8 Base64URL encoding. The final encoded session ID is also bounded consistently during creation and parsing.

Why it's needed

Web Shell resolves a subagent detail view from the parent tool-call ID and then creates a virtual session from the resolved agent task ID. Some providers produce IDs such as agent:8 and general-purpose-agent:8; the previous shared validator rejected those IDs, causing the detail request to fail with HTTP 500 even though the subagent existed.

The tool-call ID arrives with the parent model's function-call response and may be assigned by the model provider, or generated and normalized locally when missing or duplicated. Its concrete format is therefore not a stable application contract and must be treated as an opaque identifier. This differs from the parent session ID, which is locally generated or restored under a controlled UUID-shaped format and can remain strictly validated.

Reviewer Test Plan

How to verify

Open a session containing a completed subagent whose tool-call ID includes a colon, then open that subagent's detail view. The resolution request should return HTTP 200, the virtual session should load, and the detail stream should connect. IDs containing / should also round-trip through virtual session creation and parsing, while empty, oversized, over-encoded, or lossy Unicode IDs remain rejected.

Evidence (Before & After)

Before: opening /session/<parent>/subagents/agent%3A8 returned HTTP 500 with Virtual subagent session ids require valid id parts.

After: the same route returned HTTP 200, the generated virtual subagent session loaded with HTTP 200, and its SSE detail stream connected successfully.

Tested on

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

Environment (optional)

Local daemon and Web Shell through npm run dev:daemon; focused Vitest coverage for virtual-session validation and the subagent resolve/cancel routes.

Risk & Scope

  • Main risk or tradeoff: Agent task IDs accept a wider character set, mitigated by lossless encoding validation, a 500-code-unit component limit, and a 2,000-character encoded-session limit. Parent session IDs remain under the original strict validator.
  • Not validated / out of scope: Full cross-platform and complete-suite validation; CI remains the source of truth for Windows and Linux.
  • Breaking changes / migration notes: None. Previously valid virtual session IDs remain valid.

Linked Issues

N/A

中文说明

这个 PR 做了什么

允许虚拟 Subagent 会话 ID 表示包含 :/ 等保留字符的现有 agent task ID。父会话 ID 仍采用严格校验;agent ID 必须非空、长度受限,并且能够通过 UTF-8 Base64URL 编码无损往返。创建和解析时还会一致地限制最终编码后的会话 ID 长度。

为什么需要

Web Shell 会通过父工具调用 ID 解析 Subagent 详情视图,然后使用解析后的 agent task ID 创建虚拟会话。部分 provider 会产生 agent:8general-purpose-agent:8 这类 ID;原来的共享校验器会拒绝它们,导致 Subagent 明明存在,详情请求却返回 HTTP 500。

工具调用 ID 会随父 Agent 模型的 function-call 响应一起到达,它可能由模型 provider 分配,也可能在缺失或重复时由本地生成或规范化。因此,其具体格式不是稳定的应用契约,应将它视为不透明标识符。这与父会话 ID 不同:父会话 ID 由本地以受控的 UUID 格式生成或恢复,因此仍可保持严格校验。

Reviewer 测试计划

如何验证

打开一个包含已完成 Subagent 的会话,且该 Subagent 的工具调用 ID 包含冒号,然后打开 Subagent 详情。解析请求应返回 HTTP 200,虚拟会话应成功加载,详情流应成功连接。包含 / 的 ID 也应能通过虚拟会话创建和解析往返,而空 ID、超长 ID、编码后超长 ID 以及会损失的 Unicode ID 仍应被拒绝。

前后对比证据

修复前:打开 /session/<parent>/subagents/agent%3A8 会返回 HTTP 500,错误为 Virtual subagent session ids require valid id parts

修复后:相同路由返回 HTTP 200,生成的虚拟 Subagent 会话以 HTTP 200 成功加载,SSE 详情流也成功连接。

测试平台

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

环境

使用 npm run dev:daemon 启动本地 daemon 和 Web Shell;对虚拟会话校验以及 Subagent 解析/取消路由运行了定向 Vitest。

风险与范围

  • 主要风险或取舍:Agent task ID 接受更宽的字符集;通过无损编码校验、500 个代码单元的部件长度上限,以及 2,000 字符的编码会话长度上限降低风险。父会话 ID 仍使用原有严格校验器。
  • 未验证/范围外:未进行完整跨平台和全量测试;Windows 和 Linux 以 CI 为准。
  • 破坏性变更/迁移说明:无。之前有效的虚拟会话 ID 仍然有效。

关联 Issue

无。

@carffuca

carffuca commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

E2E verification report

  • Environment: macOS, local daemon and Web Shell started from this branch with npm run dev:daemon.
  • Scenario: opened an existing parent session containing a subagent with tool-call ID agent:8, then opened the subagent detail view.
  • Result: GET /session/<parent>/subagents/agent%3A8 returned HTTP 200.
  • Result: the generated virtual subagent session loaded with HTTP 200 and its SSE detail stream opened successfully.
  • Regression baseline: before this fix, the resolution request returned HTTP 500 with Virtual subagent session ids require valid id parts.

Verdict: PASS.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 44f0e4c, 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

@carffuca
carffuca marked this pull request as ready for review August 8, 2026 04:20
@carffuca
carffuca requested review from yiliang114 and ytahdn August 8, 2026 04:21
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: real, and verified against the code. The Web Shell resolves a subagent detail view through the parent tool-call ID, and the agent task ID is built directly from the provider-assigned tool-call ID (${subagentConfig.name}-${callId} in packages/core/src/tools/agent/agent.ts). That format is provider-dependent — an ID containing : or / hits the strict [a-zA-Z0-9_-] validator inside createVirtualSubagentSessionId, which throws during resolve() and surfaces as an HTTP 500 even though the subagent exists. The before/after matches the code path exactly (the quoted error string is the one thrown there).

Direction: aligned. The subagent detail view is a shipped Web Shell feature, and this is a hard failure for providers that emit IDs with reserved characters. Treating the tool-call ID as opaque while keeping the locally-generated parent session ID under strict validation is the right distinction.

Size: ~110 production lines vs ~307 test lines. The change spans two packages: packages/cli/src/serve (routes, telemetry catalog, virtual-session module) plus a 12-line mechanical rename in packages/sdk-typescript (toolCallIdsubagentRef parameter names and route attribution strings). The SDK rename is positional-only — every in-repo consumer (Web Shell SubagentDetail, useMessages, and the tests) passes arguments positionally, so nothing breaks. No hard block applies (fix type, not refactor), the size is far below the advisory thresholds, and a maintainer has already reviewed and approved this head — so no separate escalation.

Approach: scope feels right. Splitting the shared validator — strict regex for parent IDs, and for agent IDs only non-empty, ≤500 code units, and lossless round-trip through the existing base64url encoding — is the minimal fix, and bounding the final encoded session ID on creation makes the 2,000-char limit symmetric with parsing. The follow-up rounds strengthened it without expanding scope: a canonical-encoding check on the parse side, a shared part-length constant replacing the bare 500 literals in the route guards, and — per @doudouOUC's review — the route param rename to :subagentRef. Nothing I'd cut; no unrelated changes in the diff.

Risk: no elevated risk signals (none of the changed files match the revert-correlated paths).

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:真实存在,且已在代码中核实。Web Shell 通过父工具调用 ID 解析 Subagent 详情视图,而 agent task ID 直接由 provider 分配的工具调用 ID 拼接生成(packages/core/src/tools/agent/agent.ts 中的 ${subagentConfig.name}-${callId})。该格式取决于 provider——包含 :/ 的 ID 会命中 createVirtualSubagentSessionId 内部严格的 [a-zA-Z0-9_-] 校验器,在 resolve() 中抛错,即使 Subagent 存在也会返回 HTTP 500。before/after 与代码路径完全吻合(引用的错误串正是该处抛出的)。

方向:对齐。Subagent 详情视图是 Web Shell 已上线的功能,而这对会生成含保留字符 ID 的 provider 来说是硬性失败。把工具调用 ID 视为不透明标识、同时对本地生成的父会话 ID 保持严格校验,这个区分是正确的。

规模:约 110 行生产代码、307 行测试。改动跨两个包:packages/cli/src/serve(路由、遥测目录、虚拟会话模块),外加 packages/sdk-typescript 中 12 行的机械重命名(toolCallIdsubagentRef 参数名与路由归属字符串)。SDK 重命名仅涉及形参名——仓库内所有调用方(Web Shell 的 SubagentDetailuseMessages 及测试)都按位置传参,不受影响。fix 类型不触发硬阻塞,规模远低于建议阈值,且已有 maintainer 审阅并批准了当前 head,无需另行升级。

方案:范围合理。拆分共享校验器——父 ID 保持严格正则,agent ID 仅要求非空、不超过 500 个代码单元、且能经现有 base64url 编码无损往返——是最小修复;在创建时对最终编码后的会话 ID 加长度上限,也使 2,000 字符限制与解析侧对称。后续几轮加强了实现但没有扩大范围:解析侧新增规范编码校验、用共享常量替换路由守卫里裸写的 500、以及按 @doudouOUC 的评审把路由参数改名为 :subagentRef。没有可砍的部分,diff 中没有无关改动。

风险:无升级风险信号(改动文件均未命中与 revert 相关的高风险路径)。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 44f0e4c56d707057a0d4083a23c5cb58f8dcb27c · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Re-read the full diff at the current head, which has grown several rounds since my first pass. My independent take is unchanged: keep the strict validator for locally-generated parent session IDs, relax the agent ID to non-empty + bounded + losslessly representable through the existing base64url encoding, and make the length bounds symmetric between creation and parsing. That is exactly what this PR does, and I still don't see a simpler path.

What I verified in the current state:

  • The . separator stays safe: the base64url alphabet can't produce a dot, so encoded parts never collide with split('.') even when the raw agent ID contains / or :.
  • The round-trip check (decodePart(encodePart(value)) === value) is doing real work — it rejects lone surrogates, which UTF-8 encoding would silently replace and alias two distinct IDs onto one session id (covered by the '\ud800' test).
  • The new parse-side canonical check (encodePart(decoded) === part) is the counterpart that makes the ID space injective: Node's base64 decoder is permissive (it skips invalid characters rather than throwing), so without the re-encode equality, hand-crafted padded or mangled encodings of a valid ID would parse. Tests pin both the padded variants and a garbage part.
  • The strict parent charset is load-bearing, not nostalgia: the decoded parentSessionId is interpolated into filesystem paths (getSubagentSessionDir, ${projectDir}/chats/${parentSessionId}.jsonl), while agentId is only ever string-compared in findTask. I confirmed both in the base code. Tests now pin this — an encoded ../foo parent is rejected, as are ./:// parents.
  • The symmetric caps are real: creation now enforces the same 2,000-char total bound parsing does, and both route guards share MAX_VIRTUAL_SESSION_ID_PART_LENGTH instead of the previous bare 500 literals. I checked the boundary numbers by hand: a 500-char agent ID yields a 696-char session ID; ×492+aa encodes to exactly 2,000 (accepted); both parts at the 500-char cap produce 2,677 chars, rejected only by the total cap.
  • The toolCallIdsubagentRef rename is complete: no stale references to the old route strings or the invalid_tool_call_id code remain after this PR; the telemetry catalog and the SDK client were updated in lockstep; the REST URL shape is unchanged and the SDK parameter rename is positional-only, so no consumer breaks.
  • Route tests drive the real Express app with %3A- and %2F-encoded segments and assert the decoded refs reach resolve() and flow through the cancel path; the unit round-trip cases (general-purpose-agent:8, general-purpose-agent/8) fail under the old validator, so the coverage pins the change.

Open non-blocking items from the latest review round, for follow-up rather than merge-blocking: naming resolve()'s third parameter to match subagentRef, mentioning the length constraint in the 400 message, extracting the duplicated ref-extraction guard into request-helpers.ts next to requireSessionId, a route test pinning single-decoding, and coverage for the route-level 400 guard. The guard itself exists and is correct; these are polish.

Test evidence (PR's own CI at 44f0e4c, fetched via API — this run executes no PR code)

All pull_request-event workflow runs completed green: Qwen Code CI, Serve A/B, and SDK Java.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
SDK Java (ubuntu × Java 11/17/21, macOS/Windows × Java 21) ✅ success
Test (macos-latest / windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped

The skipped legs are the repo's normal job layout, not a gap introduced here — the most recently merged PR on main (#8942) shows the same skips, and this PR additionally ran the web-shell E2E smoke and both Desktop Shell legs green. The Serve A/B job (run against this exact head) reported no response changes against the PR base across its scenarios. Not verified independently: the author's end-to-end walk (resolve → virtual session load → SSE stream) was a single-platform macOS run; nothing in this review executed PR code.

Sandboxed verification would settle the remaining behavioural claim: @qwen-code /verify — that the 500→200 fix holds end-to-end for a colon-bearing task ID and oversized/lossy IDs still fail closed. The author has no write access, so this is a sponsored run: a maintainer's comment approves the head it runs against, it carries a pre-execution risk screen and a full workspace wipe, and its report should be read with the same skepticism as the fork's own CI logs.

中文说明

代码审查:已在当前 head 重新通读 diff。我的独立方案与此 PR 一致——父会话 ID 保留严格校验,agent ID 放宽为非空、有界、且能经现有 base64url 编码无损往返,创建与解析的长度上限对称——也没有找到更简的路径。已核实的关键点:base64url 字母表不含 .,保留字符不会破坏 split('.') 分段;往返校验真实有效(拒绝孤立代理项,避免两个不同 ID 编码成同一会话 ID);解析侧新增的规范编码校验是使 ID 空间单射的关键——Node 的 base64 解码器对非法字符是宽容的,不加 re-encode 相等判断,手工构造的填充或畸形编码也能通过解析,测试已钉住填充变体与垃圾片段;父 ID 严格字符集是承重的:解码后的 parentSessionId 会未经净化地拼入文件系统路径(getSubagentSessionDirchats/${parentSessionId}.jsonl),而 agentId 只做字符串比较,测试已钉住编码后的 ../foo 父 ID 被拒绝;创建侧 2,000 字符上限与解析侧对称、路由守卫改用共享常量,边界数字已手工核算(500 字符 agent ID → 696;×492+aa → 恰好 2,000;双侧均达上限 → 2,677 被总长上限拒绝);toolCallIdsubagentRef 改名彻底,无遗留旧引用,遥测目录与 SDK 同步更新,REST URL 形状未变、SDK 形参重命名按位置传参不受影响;路由测试用 %3A/%2F 编码段驱动真实 Express 应用,单元往返用例在旧校验器下会失败,覆盖确实钉住了改动。最新一轮评审遗留的非阻塞项(resolve() 第三参数命名、400 文案提及长度约束、把重复的守卫提取到 request-helpers.ts、单次解码与 400 守卫的路由测试)留作后续打磨。

测试证据(PR 自身 CI,提交 44f0e4c,经 API 获取——本次审查未执行任何 PR 代码):所有 pull_request 事件的工作流(Qwen Code CI、Serve A/B、SDK Java)均绿色完成。跳过的 macOS/Windows 单测与 CLI 集成测试是仓库常规任务布局——最近合入 main 的 #8942 呈现相同的跳过,且本 PR 额外跑绿了 web-shell E2E 冒烟与两个 Desktop Shell 腿。Serve A/B(正是针对当前 head 运行)未发现响应差异。未独立核实:作者的端到端演练(解析 → 虚拟会话加载 → SSE 流)仅在 macOS 单平台执行。

沙箱验证可补齐剩余的行为性声明:@qwen-code /verify——验证含冒号 task ID 的 500→200 修复在端到端成立、超长/有损 ID 仍被拒绝。作者无写权限,因此这是 sponsored run:由 maintainer 评论触发并锁定其针对的 head,运行前有执行前风险筛查与完整工作区清理,报告应与 fork 自身 CI 日志同等审慎地阅读。

Qwen Code · qwen3.8-max

Reviewed at 44f0e4c56d707057a0d4083a23c5cb58f8dcb27c · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — a real, code-verified bug with a minimal fix and tests that pin the change; the only reservation remains that the end-to-end evidence is single-platform (the author's macOS run), with CI covering the rest.

Stepping back: this is still the kind of PR the gate should wave through, and it has gotten better with each round. The problem is real — agent task IDs trace back to provider-assigned tool-call IDs, so reserved characters are not hypothetical — and the before/after matches the actual throw path. The fix doesn't try too hard: one validator split, one symmetric length bound, reuse of the existing encoding instead of anything new. The follow-up rounds closed the soft spots rather than adding scope: the parse side now enforces canonical encodings (the base64 decoder's permissiveness made that check genuinely necessary), the parent charset is pinned by traversal-shaped tests, and the route param rename from human review makes the API honest about what it accepts. The open inline suggestions are polish — naming, message wording, a helper extraction, two more route tests — and a maintainer already approved this head with them standing, which I read as agreement that they don't block. CI is green on the repo's normal job layout, Serve A/B saw no response regressions on this exact head, and the tests fail against the old validator, so the coverage is load-bearing rather than decorative. In six months this reads as two clearly-named validators and a comment explaining why they differ — I'd thank the author, not curse them.

Approving, pinned to the reviewed commit. ✅

中文说明

回顾整体:这仍然是应当放行的 PR,而且每一轮都在变好。问题真实存在——agent task ID 可追溯到 provider 分配的工具调用 ID,保留字符并非假设——before/after 与实际抛错路径吻合。修复克制:一次校验器拆分、一个对称的长度上限,复用现有编码而非新造轮子。后续几轮补上的是薄弱点而非扩大范围:解析侧强制规范编码(base64 解码器的宽容性使该校验确有必要)、用遍历形态的测试钉住父 ID 字符集、来自人工评审的路由参数改名让 API 如实反映其接受的输入。遗留的行内建议属于打磨——命名、错误文案、辅助函数提取、再补两个路由测试——且已有 maintainer 在这些建议存在的情况下批准了当前 head,可视为认同它们不构成阻塞。CI 在仓库常规任务布局下全绿,Serve A/B 在当前 head 上无响应回归,测试在旧校验器下会失败,说明覆盖是有效的而非摆设。六个月后回看,这是两个命名清晰的校验器加一条解释差异的注释。

予以批准,锁定在被审查的提交上。✅

Qwen Code · qwen3.8-max

Reviewed at 44f0e4c56d707057a0d4083a23c5cb58f8dcb27c · re-run with @qwen-code /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.

LGTM, looks ready to ship. ✅

@wenshao wenshao 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 explored to full depth (tool budget reached): This PR relaxes virtual subagent session ID validation: p...: Could not execute the changed unit tests against the PR code — the worktree ( /Users/wenshao/git/qwen-code/.qwen/tmp/review-pr-8717 ) has no installed node_mod…; This PR relaxes virtual subagent session ID validation: p...: Did not trace client-side (web-shell) rendering of taskId / sessionId into HTML — out of the daemon diff's scope; the values reach the client as JSON/SSE data…; This PR relaxes virtual subagent session ID validation: p...: exact failure detail of the one failed server.test.ts run (lost to harness file flip-flop; re-run passed); This PR relaxes virtual subagent session ID validation: p...: full-file server.test.ts run under a stable (non-flipping) source checkout; This PR relaxes virtual subagent session ID validation: p...: attribution of the two unchanged failing tests to a specific environmental cause (missing coverage/.tmp dir, ordering interference, or harness flip) — verified ….

中文说明

已审查。 建议见行内评论。 未探索到全部深度(达到工具调用预算):This PR relaxes virtual subagent session ID validation: p...:Could not execute the changed unit tests against the PR code — the worktree ( /Users/wenshao/git/qwen-code/.qwen/tmp/review-pr-8717 ) has no installed node_mod…;This PR relaxes virtual subagent session ID validation: p...:Did not trace client-side (web-shell) rendering of taskId / sessionId into HTML — out of the daemon diff's scope; the values reach the client as JSON/SSE data…;This PR relaxes virtual subagent session ID validation: p...:exact failure detail of the one failed server.test.ts run (lost to harness file flip-flop; re-run passed);This PR relaxes virtual subagent session ID validation: p...:full-file server.test.ts run under a stable (non-flipping) source checkout;This PR relaxes virtual subagent session ID validation: p...:attribution of the two unchanged failing tests to a specific environmental cause (missing coverage/.tmp dir, ordering interference, or harness flip) — verified …

— DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8)

Comment on lines +230 to +233
const sessionId = `${PREFIX}${encodePart(parentSessionId)}.${encodePart(agentId)}`;
if (sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH) {
throw new Error('Virtual subagent session ids require valid id parts');
}

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.

[Suggestion] The new encoded-length cap branch throws the same "Virtual subagent session ids require valid id parts" message as the invalid-parts branch — Failure scenario: a bridge agent task ID in the ~374–500 multibyte range passes every isValidVirtualAgentId check (≤500 chars, lossless round-trip), then resolve()createVirtualSubagentSessionId trips the encoded-length cap and the route answers HTTP 500 with "require valid id parts" even though no part is invalid; the route-level pre-guard (toolCallId.length > 500 → 400) measures a different quantity, so the caller cannot diagnose from the message. The PR's own '界'.repeat(500) test exercises and blesses this branch.

Suggested change
const sessionId = `${PREFIX}${encodePart(parentSessionId)}.${encodePart(agentId)}`;
if (sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH) {
throw new Error('Virtual subagent session ids require valid id parts');
}
const sessionId = `${PREFIX}${encodePart(parentSessionId)}.${encodePart(agentId)}`;
if (sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH) {
throw new Error(
`Virtual subagent session id exceeds ${MAX_VIRTUAL_SESSION_ID_LENGTH} characters`,
);
}
中文说明

新增的编码后总长度上限分支与无效部件分支抛出了相同的 "Virtual subagent session ids require valid id parts" 错误信息,混淆了两种失败模式:部件确实非法,与部件各自合法但编码后组合长度超过 2,000 字符。触发场景:约 374–500 个多字节字符的 agent task ID 能通过 isValidVirtualAgentId 的全部检查(≤500 字符、无损往返),随后 resolve()createVirtualSubagentSessionId 触发长度上限,路由返回 HTTP 500 且提示 "require valid id parts",实际没有任何部件非法;路由前置校验(toolCallId.length > 500 → 400)与创建侧上限度量的量不一致,调用方无法仅凭报错定位。PR 自带的 '界'.repeat(500) 测试恰好命中并固化了这条分支。建议为长度分支使用独立错误信息,并同步更新 toThrow 断言。

— DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8)

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.

Addressed in 4c2c421. The encoded-length branch now reports a distinct Virtual subagent session id exceeds 2000 characters error, while invalid components continue to use the existing valid-parts error. The oversized multibyte-agent test now asserts the distinct message.

Comment on lines +212 to +218
function isValidVirtualAgentId(value: string): boolean {
return (
value.length > 0 &&
value.length <= 500 &&
decodePart(encodePart(value)) === value
);
}

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.

[Suggestion] On the parse path the "lossless round-trip" check is vacuous — Failure scenario: parseVirtualSubagentSessionId runs isValidVirtualAgentId against the decoded value, which is always well-formed UTF-8 (invalid bytes become U+FFFD) and therefore always re-encodes losslessly, so the effective parse-side agent gate is only "non-empty and ≤500 chars". A token-bearing client can address a virtual subagent via infinitely many distinct session-ID spellings — subagent.<b64parent>.garbage decodes to \ufffd\ufffd\ufffdj\u0007 and now parses where the old strict regex returned undefined; the context/heartbeat routes branch on parse success without verifying the task exists, so a nonexistent virtual session answers 200 instead of the old 404. Create and parse now accept different sets, contradicting the PR's stated "round-trip losslessly" invariant. No privilege boundary is crossed, but the parse-side round-trip term is dead code beyond the length check.

Suggested change
function isValidVirtualAgentId(value: string): boolean {
return (
value.length > 0 &&
value.length <= 500 &&
decodePart(encodePart(value)) === value
);
}
if (
!parentSessionId ||
!agentId ||
!isValidVirtualParentSessionId(parentSessionId) ||
!isValidVirtualAgentId(agentId) ||
encodePart(agentId) !== parts[1]
) {
return undefined;
}
中文说明

在解析路径上,"无损往返" 校验是空转的:parseVirtualSubagentSessionId解码后的值执行 isValidVirtualAgentId,而解码输出永远是合法 UTF-8(非法字节被替换为 U+FFFD),必然能无损再编码,因此解析侧对 agent 部件的有效校验只剩 "非空且 ≤500 字符"。持有 token 的客户端可以用无数种不同的会话 ID 拼写寻址同一虚拟 subagent——例如 subagent.<b64parent>.garbage 解码为 \ufffd\ufffd\ufffdj\u0007,旧的正则校验会返回 undefined,现在却能解析成功;context/heartbeat 路由只依据解析成功就放行且不校验任务是否存在,导致不存在的虚拟会话返回 200 而非旧行为的 404。创建与解析现在接受不同的集合,与 PR 描述中 "经 UTF-8 Base64URL 无损往返" 的不变量不符。未跨越任何权限边界,但解析侧的往返项除长度检查外是死代码。建议在解析侧要求编码部件为规范形式(如 encodePart(agentId) !== parts[1] 时返回 undefined),并补充非规范部件解析失败的测试。

— DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8)

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.

Addressed in 4c2c421. Parsing now re-encodes and compares both the parent and agent components with their original Base64URL parts, so only canonical spellings are accepted. The regression tests cover padded aliases for each component as well as a garbage agent part that Node decodes permissively.

Comment on lines +92 to +94
expect(() =>
createVirtualSubagentSessionId('parent-session', '界'.repeat(500)),
).toThrow('valid id parts');

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.

[Suggestion] The new 2,000-char cap is tested only in the rejection direction — Failure scenario: MAX_VIRTUAL_SESSION_ID_LENGTH drifts down and, because create and parse share the one constant, mutual consistency and every round-trip test stay green (all test inputs encode to ≤ ~700 chars, so even cap=500 passes the suite); a legitimate ≤500-char agent ID whose base64url encoding crosses the cap is then rejected in createVirtualSubagentSessionIdresolve() throws → the route answers HTTP 500 with no test failing. The exact-500-char agent boundary is likewise unpinned in the acceptance direction.

Suggested change
expect(() =>
createVirtualSubagentSessionId('parent-session', '界'.repeat(500)),
).toThrow('valid id parts');
it('accepts a session at the encoded-length boundary', () => {
const agentId = '界'.repeat(492);
const sessionId = createVirtualSubagentSessionId('parent-session', agentId);
expect(sessionId.length).toBe(1998);
expect(parseVirtualSubagentSessionId(sessionId)).toEqual({
parentSessionId: 'parent-session',
agentId,
});
});
中文说明

新增的 2,000 字符上限只在拒绝方向被测试(2,029 字符的会话);没有任何测试断言接近上限的会话能被接受并完成往返,精确 500 字符的 agent 边界在接受方向上也没有被钉住。触发场景:MAX_VIRTUAL_SESSION_ID_LENGTH 被误改小——创建与解析共用同一个常量,相互一致性和全部往返测试仍然全绿(所有测试输入的编码会话 ≤ ~700 字符,即使上限降到 500 整套测试也通过);此时一个合法 ≤500 字符、但 base64url 编码后跨过上限的 agent ID 会在 createVirtualSubagentSessionId 中被拒绝 → resolve() 抛错 → 路由返回 HTTP 500,而没有任何测试失败。建议补充边界接受测试,例如 '界'.repeat(492)(会话约 1998 字符,为可容纳的最大值)并断言 parseVirtualSubagentSessionId 能往返,另加一个恰好 500 字符 ASCII agent 的接受用例。

— DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8)

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.

Addressed in 2ba3bbc and 9750746. The tests now accept and round-trip a 500-character ASCII agent ID, pin an exactly 2,000-character encoded session ID, and reject the next multibyte boundary. The original × 492 example produces 1,997 characters with parent-session, so the exact-cap case uses × 492 plus aa and explicitly asserts a length of 2,000.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: @qwen-code /review --timeout=360. See workflow logs.

@wenshao wenshao 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 explored to full depth (tool budget reached): This PR relaxes virtual subagent session ID validation so...: did not inspect getSubagentSessionDir 's internals for the raw- sessionId path that reaches findTask via the single-workspace requireSessionRuntime fast p…; This PR relaxes virtual subagent session ID validation so...: could not execute the vitest suite ( packages/cli/.../virtual-subagent-sessions.test.ts ) — the worktree has no node_modules and even the parent checkout is m….

中文说明

已审查。 建议见行内评论。 未探索到全部深度(达到工具调用预算):This PR relaxes virtual subagent session ID validation so...:did not inspect getSubagentSessionDir 's internals for the raw- sessionId path that reaches findTask via the single-workspace requireSessionRuntime fast p…;This PR relaxes virtual subagent session ID validation so...:could not execute the vitest suite ( packages/cli/.../virtual-subagent-sessions.test.ts ) — the worktree has no node_modules and even the parent checkout is m…

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

Comment on lines +149 to +150
it.each(['general-purpose-agent:8', 'general-purpose-agent/8'])(
'round-trips an existing agent id containing reserved characters: %s',

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.

[Suggestion] R2-1: No test chains a reserved-character task ID through the real VirtualSubagentSessions.resolve() — the reserved-char round-trip tests below call createVirtualSubagentSessionId/parseVirtualSubagentSessionId directly, the server route test mocks resolve, and every real-resolve unit test uses plain IDs only (fork-agent-1, general-purpose-call-1, ...). — Failure scenario: a future change at the resolve() call site that re-restricts or transforms task.id before ID creation (e.g. reintroducing a per-part charset filter) silently ships this PR's exact bug — HTTP 500 for agent:8-style provider IDs — while every current test stays green (verified by probe: a strict charset gate injected on task.id left all existing tests green; adding the missing chain test failed with the original valid id parts error).

Suggested fix — add one real-resolve case with a reserved-char id, e.g. a sibling of 'resolves an out-of-band fork by agent task id' where the bridge task's id is general-purpose-agent:8, asserting the resolved sessionId parses back to that agent ID:

it('resolves an out-of-band task whose id contains reserved characters', async () => {
  // same scaffold as the fork test, bridge task id 'general-purpose-agent:8'
  const target = await sessions.resolve(runtime, 'parent-1', 'general-purpose-agent:8');
  expect(parseVirtualSubagentSessionId(target.sessionId)).toMatchObject({
    agentId: 'general-purpose-agent:8',
  });
});
中文说明

没有测试把含保留字符的 task ID 经由真实的 VirtualSubagentSessions.resolve() 串到 createVirtualSubagentSessionId:下方的保留字符往返测试直接调用 create/parse,server 路由测试 mock 了 resolve,而所有真实 resolve 单测只用普通 ID(fork-agent-1general-purpose-call-1 等)。— 失败场景:未来在 resolve() 调用点对 task.id 重新收紧或变换(例如重新引入按部件的字符集过滤),会在所有现有测试保持绿色的情况下悄悄复发本 PR 修复的 bug——agent:8 这类 provider ID 返回 HTTP 500(已用探针验证:在 task.id 上注入严格字符集门槛后全部现有测试仍为绿色;补上缺失的链路测试则以原始的 valid id parts 报错失败)。

建议修复——为真实 resolve 补一个含保留字符 ID 的用例,例如在 'resolves an out-of-band fork by agent task id' 旁新增一个 bridge task idgeneral-purpose-agent:8 的用例,断言解析出的 sessionId 能 parse 回该 agent ID。

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

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.

Addressed in a4c7df2. The existing out-of-band resolve test is now parameterized with general-purpose-agent:8 and asserts that the sessionId returned by the real VirtualSubagentSessions.resolve() path parses back to the same task ID. I also verified the test's efficacy by temporarily reintroducing a strict charset gate at the resolve call site: only the new reserved-ID case failed with the original valid id parts error. The focused suite passes 19/19 on the final code.

Comment on lines +215 to +216
value.length <= 500 &&
decodePart(encodePart(value)) === value

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.

[Suggestion] R2-2: The validator split duplicates the 500-char part cap in two spellings — {1,500} in isValidVirtualParentSessionId's regex and value.length <= 500 here — while the sibling 2000 bound was extracted into MAX_VIRTUAL_SESSION_ID_LENGTH in the same change. The route-level toolCallId.length > 500 guards in routes/session.ts are behaviorally coupled to the same bound and equally unpinned. — Failure scenario: a future change adjusting the cap for one part type (e.g. longer provider-assigned agent IDs) edits one literal and leaves the other; the boundary tests only exercise agent ids at 500/501 with a short parent, so a divergent parent cap ships silently.

Suggested fix — name the bound once and use it in both validators (leave the route-level toolCallId caps alone unless they are meant to track the same bound):

const MAX_ID_PART_LENGTH = 500;

function isValidVirtualParentSessionId(value: string): boolean {
  return (
    value.length > 0 &&
    value.length <= MAX_ID_PART_LENGTH &&
    /^[a-zA-Z0-9_-]+$/.test(value)
  );
}

function isValidVirtualAgentId(value: string): boolean {
  return (
    value.length > 0 &&
    value.length <= MAX_ID_PART_LENGTH &&
    decodePart(encodePart(value)) === value
  );
}
中文说明

校验器拆分后,500 字符的部件上限出现了两种写法——isValidVirtualParentSessionId 正则里的 {1,500} 和这里的 value.length <= 500——而同一次改动中,2000 的姊妹上限却被提取成了 MAX_VIRTUAL_SESSION_ID_LENGTHroutes/session.ts 路由层的 toolCallId.length > 500 守卫与同一上限存在行为耦合,同样没有被钉住。— 失败场景:未来为某一类部件调整上限(例如更长的 provider 分配 agent ID)时只改了一处字面量而漏掉另一处;边界测试只用短父 ID 验证了 agent id 在 500/501 的行为,父 ID 上限不一致会静默漏过。

建议修复——把该上限命名为一个常量并在两个校验器中使用(路由层的 toolCallId 上限除非有意与之保持一致,否则不必改动)。

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

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.

Addressed in 1c27914. MAX_VIRTUAL_SESSION_ID_PART_LENGTH now defines the 500-character cap used by both the parent and agent validators; the parent regex retains only the character-class check. I intentionally left the route-level toolCallId limits unchanged because they guard a different request input boundary and changing them would widen this follow-up beyond the two validator definitions. The focused suite passes 19/19.

@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 explored to full depth (tool budget reached): Context: this PR relaxes virtual subagent session ID vali...: could not run the actual vitest suites ( virtual-subagent-sessions.test.ts , server.test.ts ) — dependencies not installed in the review worktree; logic verifi….

中文说明

已审查。 建议见行内评论。 未探索到全部深度(达到工具调用预算):Context: this PR relaxes virtual subagent session ID vali...:could not run the actual vitest suites ( virtual-subagent-sessions.test.ts , server.test.ts ) — dependencies not installed in the review worktree; logic verifi…

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

return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&
/^[a-zA-Z0-9_-]+$/.test(value)

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.

[Suggestion] The strict parent-session-ID charset is not pinned by any test — Failure scenario: mutating this regex to also accept :, /, . (or reusing isValidVirtualAgentId for the parent) survives the entire suite, because the only invalid parent ID exercised anywhere is 'parent session', which either variant rejects (verified by probe: with the mutated regex, 19/19 tests still pass). The strictness is load-bearing: the decoded parentSessionId is interpolated into filesystem paths — getSubagentSessionDir(projectDir, parentSessionId) + readdir in findTask, and ${projectDir}/chats/${parentSessionId}.jsonl in the legacy path — so a future regression would let a crafted subagent.<base64url('../foo')>.<part> session ID put path-active characters into those paths on a green suite.

Suggested fix — add rejection cases in virtual-subagent-sessions.test.ts:

expect(() =>
  createVirtualSubagentSessionId('parent/session', 'agent-1'),
).toThrow('valid id parts');
expect(() =>
  createVirtualSubagentSessionId('parent:session', 'agent-1'),
).toThrow('valid id parts');
expect(
  parseVirtualSubagentSessionId(
    `subagent.${Buffer.from('../foo', 'utf8').toString('base64url')}.${Buffer.from('agent-1', 'utf8').toString('base64url')}`,
  ),
).toBeUndefined();
中文说明

严格的父会话 ID 字符集目前没有任何测试钉住——失败场景:把该正则放宽为也接受 :/.(或让父 ID 复用 isValidVirtualAgentId),整个测试套件仍会全部通过,因为现有用例中唯一的非法父 ID 是 'parent session',而两种变体都会拒绝它(已通过探针验证:变异后 19/19 测试仍通过)。这一严格性是关键保障:解码出的 parentSessionId 会被插入文件系统路径——findTask 中的 getSubagentSessionDir(projectDir, parentSessionId) + readdir,以及遗留路径中的 ${projectDir}/chats/${parentSessionId}.jsonl——未来一旦回归,构造的 subagent.<base64url('../foo')>.<part> 会话 ID 就能把路径敏感字符带进这些路径,且套件仍然全绿。

建议修复——在 virtual-subagent-sessions.test.ts 中补充拒绝用例(见上方英文代码块)。

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

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.

Thanks — agreed this would be useful regression coverage for the parent-session ID trust boundary. The production validator already enforces the strict parent charset, so this is not a current correctness fix. Since this PR has already gone through several review rounds, I’m going to defer this test-only hardening to a follow-up rather than widen the bugfix further.

Comment thread packages/cli/src/serve/server.test.ts Outdated
try {
const resolveRes = await request(app)
.get('/session/s-1/subagents/tool-1')
.get('/session/s-1/subagents/agent%3A8')

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.

[Suggestion] The reserved-character path this PR enables is route-tested only with : (%3A) — Failure scenario: / is the riskier newly-accepted character inside a path segment (%2F); if path matching/raw-URL handling (or an intervening proxy on a remote-daemon deployment) later regresses encoded-slash handling, resolve/cancel for /-bearing task IDs would 404 or misroute while this %3A-only test stays green. / is an intended case — the unit tests round-trip general-purpose-agent/8 and the SDK ships an agent%2F1 fixture — and a probe against this branch confirmed agent%2F8 resolves and cancels correctly today, so this is a coverage gap worth pinning, not a live defect.

Suggested fix — extend this route test (or add an it.each case) to drive one resolve + cancel round-trip through /session/s-1/subagents/agent%2F8, asserting the decoded agent/8 reaches resolve and the cancel bridge call:

const resolveRes = await request(app)
  .get('/session/s-1/subagents/agent%2F8')
  .set('Host', `127.0.0.1:${tokenOpts.port}`)
  .set('Authorization', 'Bearer secret');
// ... then assert:
expect(resolveSpy).toHaveBeenCalledWith('s-1', 'agent/8');
中文说明

本 PR 启用的保留字符路径在路由层只用 :%3A)做了测试——失败场景:/ 是路径段中风险更高的新接受字符(%2F);如果未来的路径匹配/原始 URL 处理变化(或远程 daemon 部署中插入的代理)使编码斜杠的处理发生回归,含 / 的 task ID 的 resolve/cancel 会 404 或被错误路由,而这个只测 %3A 的测试仍会保持绿色。/ 是预期支持的用例——单元测试已对 general-purpose-agent/8 做了往返验证,SDK 也带有 agent%2F1 的 URL 夹具——且对本分支的探针确认 agent%2F8 目前能正确解析与取消,因此这是一个值得钉住的覆盖缺口,而非当前缺陷。

建议修复——扩展该路由测试(或增加一个 it.each 用例),通过 /session/s-1/subagents/agent%2F8 驱动一次 resolve + cancel 往返,并断言解码后的 agent/8 到达 resolve 及 cancel 桥接调用(见上方英文代码块)。

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

@carffuca carffuca Aug 10, 2026

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.

Thanks — agreed that an encoded-slash route case would strengthen coverage. The current implementation already handles %2F correctly (and the review probe confirmed resolve/cancel works), so this is a test coverage gap rather than a live defect. Since this PR has already gone through several review rounds, I’m going to defer this test-only addition to a follow-up rather than widen the bugfix further.

@wenshao

wenshao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 11, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

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

Copy link
Copy Markdown
Collaborator

AutoFix round 6 finishedview run. See this round's report below.

中文说明

AutoFix 第 6 轮已完成 —— 查看运行。本轮报告见下方。

@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 explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; Context: this PR relaxes virtual subagent session ID vali...: could not run vitest on the two changed test files (no node_modules in the worktree; monorepo npm install exceeded the review budget) — substituted the No…; Context: this PR relaxes virtual subagent session ID vali...: did not run npm run typecheck /lint on packages/cli for the same reason.; Context: this PR relaxes virtual subagent session ID vali...: none — all checks I needed completed within budget.; Context: this PR relaxes virtual subagent session ID vali...: did not run the two updated test files ( virtual-subagent-sessions.test.ts , server.test.ts ) — my conclusions rest on source reading plus the empirical Node b…, and 3 more.

中文说明

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

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;Context: this PR relaxes virtual subagent session ID vali...:could not run vitest on the two changed test files (no node_modules in the worktree; monorepo npm install exceeded the review budget) — substituted the No…;Context: this PR relaxes virtual subagent session ID vali...:did not run npm run typecheck /lint on packages/cli for the same reason.;Context: this PR relaxes virtual subagent session ID vali...:none — all checks I needed completed within budget.;Context: this PR relaxes virtual subagent session ID vali...:did not run the two updated test files ( virtual-subagent-sessions.test.ts , server.test.ts ) — my conclusions rest on source reading plus the empirical Node b…,另有 3 条。

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

Comment on lines +30 to +31
const MAX_VIRTUAL_SESSION_ID_PART_LENGTH = 500;
const MAX_VIRTUAL_SESSION_ID_LENGTH = 2_000;

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.

[Suggestion] The 500-char part cap this new constant names is still hardcoded as a bare literal at the two upstream route guards — routes/session.ts:2408 and :2460 both read if (!toolCallId || toolCallId.length > 500). The route's toolCallId is matched against task.id in resolve() (including via the endsWith(-${toolCallId}) suffix branch, which can resolve a ≤500-char toolCallId to a longer task.id), and task.id is exactly the agentId part MAX_VIRTUAL_SESSION_ID_PART_LENGTH bounds — two expressions of one domain limit on the same string, now unsynchronised. Failure scenario: if MAX_VIRTUAL_SESSION_ID_PART_LENGTH is raised to admit longer agent task IDs, the unit tests pass but both routes still 400 with invalid_tool_call_id above 500 chars — the feature appears implemented yet silently rejects at the old bound, with no compile-time signal tying the sites together.

Suggested fix (routes/session.ts, both guards — the constant would need to be exported):

if (!toolCallId || toolCallId.length > MAX_VIRTUAL_SESSION_ID_PART_LENGTH) {
中文说明

[建议] 这个新常量所命名的 500 字符部件上限,在上游的两处路由守卫中仍以裸字面量硬编码 —— routes/session.ts:2408:2460 都是 if (!toolCallId || toolCallId.length > 500)。路由的 toolCallId 会在 resolve() 中与 task.id 匹配(包括通过 endsWith(-${toolCallId}) 后缀分支,它可以让 ≤500 字符的 toolCallId 解析到更长的 task.id),而 task.id 正是 MAX_VIRTUAL_SESSION_ID_PART_LENGTH 所约束的 agentId 部件 —— 同一字符串上同一领域上限的两种表达,如今互不同步。失败场景:如果调高 MAX_VIRTUAL_SESSION_ID_PART_LENGTH 以接受更长的 agent task ID,单元测试会通过,但两个路由仍会在超过 500 字符时返回 400 invalid_tool_call_id —— 功能看似已实现,却在旧边界处被悄悄拒绝,且没有任何编译期信号把这些位置关联起来。

建议修复(routes/session.ts 的两处守卫 —— 需要先导出该常量):

if (!toolCallId || toolCallId.length > MAX_VIRTUAL_SESSION_ID_PART_LENGTH) {

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

Comment on lines +236 to +238
if (sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH) {
throw new Error('Virtual subagent session id exceeds 2000 characters');
}

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.

[Suggestion] The added error message hardcodes 2000 even though MAX_VIRTUAL_SESSION_ID_LENGTH is defined a few lines above; the serve directory's house convention is to interpolate the constant into such messages (e.g. server/request-helpers.ts:145 interpolates MAX_WORKSPACE_PATH_LENGTH). Failure scenario: a future change raises MAX_VIRTUAL_SESSION_ID_LENGTH; parse follows the constant, but the thrown error still tells callers the limit is 2000 characters, and this PR's own test .toThrow('exceeds 2000 characters') silently pins the stale text — a caller debugging an oversized id is given the wrong limit.

Suggested change
if (sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH) {
throw new Error('Virtual subagent session id exceeds 2000 characters');
}
if (sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH) {
throw new Error(`Virtual subagent session id exceeds ${MAX_VIRTUAL_SESSION_ID_LENGTH} characters`);
}
中文说明

[建议] 新增的错误信息硬编码了 2000,而 MAX_VIRTUAL_SESSION_ID_LENGTH 就定义在几行之上;serve 目录的惯例是把常量插值进此类信息(例如 server/request-helpers.ts:145 会插值 MAX_WORKSPACE_PATH_LENGTH)。失败场景:未来某次修改调高了 MAX_VIRTUAL_SESSION_ID_LENGTH;parse 会跟随常量,但抛出的错误仍告诉调用方上限是 2000 字符,而本 PR 自己的测试 .toThrow('exceeds 2000 characters') 会悄悄钉住这段过期文本 —— 调试超长 id 的调用方会得到错误的上限值。

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

Comment on lines +217 to 223
function isValidVirtualAgentId(value: string): boolean {
return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&
decodePart(encodePart(value)) === value
);
}

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.

[Suggestion] decodePart(encodePart(value)) === value reads like a tautology but is the only create-time guard against lone-surrogate aliasing — verified by probe: encodePart('a\ud800b') === encodePart('a\ufffdb') (both Ye-_vWI), so without this check two distinct agent ids would encode to byte-identical session ids and alias onto one VirtualSubagentTarget (targets map keyed by ${workspaceId}:${sessionId}), cross-contaminating status updates and event streams. The '\ud800' test pins the behaviour, but neither the test name nor the code explains the collision it prevents; a maintainer "simplifying" the apparently always-true round-trip away would silently reintroduce the aliasing (the parse-side canonical checks run too late to prevent create-time collisions).

Suggested change
function isValidVirtualAgentId(value: string): boolean {
return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&
decodePart(encodePart(value)) === value
);
}
function isValidVirtualAgentId(value: string): boolean {
return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&
// Round-trip rejects lone surrogates: UTF-8 maps them to U+FFFD, so two
// distinct agent ids would otherwise encode to the same session id.
decodePart(encodePart(value)) === value
);
}
中文说明

[建议] decodePart(encodePart(value)) === value 读起来像恒真式,但它是创建时防止孤立代理项(lone surrogate)别名的唯一守卫 —— 已用探针验证:encodePart('a\ud800b') === encodePart('a\ufffdb')(两者都是 Ye-_vWI),因此没有这个检查,两个不同的 agent id 会编码出字节完全相同的 session id,并别名到同一个 VirtualSubagentTarget(targets 映射以 ${workspaceId}:${sessionId} 为键),交叉污染状态更新和事件流。'\ud800' 测试钉住了这个行为,但测试名和代码都没有解释它所防止的冲突;一位维护者若"简化"掉这个看似恒真的往返检查,就会悄悄重新引入别名问题(parse 侧的规范化检查为时已晚,无法防止创建时的冲突)。

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

return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&
/^[a-zA-Z0-9_-]+$/.test(value)

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.

[Suggestion] R1-1: Still standing from the previous round — the strict parent-session-ID validation remains unpinned by tests. Every rejection case in virtual-subagent-sessions.test.ts varies only the agent part; the single parent case ('parent session') does not pin the charset — mutating this regex to also accept :, /, . (or reusing isValidVirtualAgentId for the parent) survives the whole suite. This round's mutation analysis extends the gap: deleting value.length > 0 && or the <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH check also survives, letting createVirtualSubagentSessionId emit an id the same module's parser refuses — downstream routes then 404 on the server's own freshly-created id. Failure scenario: any such mutation keeps the suite green while create() emits unparseable ids.

Suggested tests:

expect(() => createVirtualSubagentSessionId('', 'agent-1')).toThrow('valid id parts');
expect(() => createVirtualSubagentSessionId('a'.repeat(501), 'agent-1')).toThrow('valid id parts');
expect(() => createVirtualSubagentSessionId('parent:session', 'agent-1')).toThrow('valid id parts');
中文说明

[建议] R1-1:上一轮的发现仍然存在 —— 严格的父会话 ID 校验依然没有被测试钉住。virtual-subagent-sessions.test.ts 中的每个拒绝用例都只变化 agent 部件;唯一的 parent 用例('parent session')并没有钉住字符集 —— 把这个正则变异为也接受 :/.(或对 parent 复用 isValidVirtualAgentId)在整个测试套件下依然存活。本轮的变异分析进一步扩展了这个缺口:删除 value.length > 0 &&<= MAX_VIRTUAL_SESSION_ID_PART_LENGTH 检查同样存活,使得 createVirtualSubagentSessionId 可以产出同一模块的解析器拒绝接受的 id —— 下游路由会对服务器自己刚创建的 id 返回 404。失败场景:任何此类变异都会让套件保持绿色,而 create() 却产出无法解析的 id。

建议补充的测试见上方代码块。

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

Comment thread packages/cli/src/serve/server.test.ts Outdated
try {
const resolveRes = await request(app)
.get('/session/s-1/subagents/tool-1')
.get('/session/s-1/subagents/agent%3A8')

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.

[Suggestion] R1-2: Still standing from the previous round — the reserved-character route path this PR enables is route-tested only with : (%3A); / (%2F), the riskier newly-accepted character inside a path segment, never travels through the route param even though general-purpose-agent/8 round-trips at unit level. Re-verified this round that Express 5.2.1 decodes %2F correctly today, so this remains a regression-coverage gap rather than a live bug — but the %3A test cannot detect a %2F-specific regression (router upgrade or middleware/proxy rejecting encoded slashes), which would silently 404 the exact scenario this PR targets while the suite certifies reserved-character support.

Suggested fix — parametrize the route test:

it.each([
  ['agent%3A8', 'agent:8'],
  ['agent%2F8', 'agent/8'],
])('resolves and cancels a reserved-character task id: %s', async (encoded, decoded) => {
  // same body, using encoded in the URLs and decoded in the resolve/cancel assertions
});
中文说明

[建议] R1-2:上一轮的发现仍然存在 —— 本 PR 启用的保留字符路由路径在路由层只用 :%3A)测试过;/%2F)—— 路径段中风险更高的新接受字符 —— 尽管 general-purpose-agent/8 在单元层可以往返,却从未通过路由参数传递。本轮重新验证了 Express 5.2.1 目前能正确解码 %2F,所以这仍是回归覆盖缺口而非现存 bug —— 但 %3A 测试无法发现 %2F 特有的回归(路由器升级或中间件/代理拒绝编码斜杠),届时本 PR 针对的确切场景会被悄悄 404,而套件却为保留字符支持背书。

建议修复 —— 参数化路由测试(见上方代码块)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Round summary

New commit: 6688f96baatest(serve): pin parent charset and encoded slash coverage (test-only; two files, +79/−51). All seven inline findings are now resolved in the code; no production logic changed this round.

Feedback points

Implemented this round (test-only)

  • [Suggestion] The strict parent-session-ID charset is not pinned by any test — implemented. The rejection test now also rejects parent ids containing /, :, and . at creation, and a new test rejects a parsed session id whose parent part is base64url('../foo'), pinning the charset that guards filesystem-path interpolation. A mutation probe (relaxing the regex to accept :, /, .) made exactly these two tests fail; the revert restores green.
  • [Suggestion] The reserved-character path is route-tested only with : (%3A) — implemented. The resolve+cancel route test is now parameterized over agent%3A8 and agent%2F8, asserting the decoded agent/8 reaches resolve and the resolved task id reaches the cancel bridge call through the real Express routing.

Both findings had been deferred to a follow-up in the previous round, but they were still presented as actionable this round. Since each is a small test-only pin on behavior this PR itself introduces, they were implemented to close them out rather than deferred again.

Re-verified as already fixed (earlier commits survived the merge of main)

  • [Suggestion] Encoded-length cap threw the same error as invalid parts — fixed in 4c2c4211db; the oversized-multibyte case asserts the distinct exceeds 2000 characters message. Re-verified at HEAD.
  • [Suggestion] Parse-path round-trip check was vacuous — fixed in 4c2c4211db; parsing now requires both parts to re-encode canonically, with tests for padded parent/agent aliases and a garbage agent part. Re-verified at HEAD.
  • [Suggestion] The 2,000-char cap tested only in the rejection direction — fixed in 2ba3bbc9b3 + 9750746e90; acceptance cases round-trip a 500-char ASCII agent id and pin an exactly 2,000-char encoded session id. Re-verified at HEAD.
  • [Suggestion] No test chains a reserved-character task ID through the real resolve() — fixed in a4c7df2bd2; the out-of-band resolve test is parameterized with general-purpose-agent:8 and asserts the session id parses back. Re-verified at HEAD.
  • [Suggestion] The 500-char part cap duplicated in two spellings — fixed in 1c2791490a; MAX_VIRTUAL_SESSION_ID_PART_LENGTH is shared by both validators. Re-verified at HEAD. The route-level toolCallId guards were intentionally left as a separate request-input boundary.

Failed check: precheck-pr / precheck

Diagnosed as not addressable by branch code changes. Evidence:

  • The precheck scans the PR title/body plus added diff lines for sensitive patterns. Running the repository's own assessPullRequestSafety locally returns allow_triage with zero reason codes for the net diff, for every per-commit diff exposed by GitHub's patch endpoint, and for the merge commit.
  • The same PR title/body passed this exact gate when the PR was created (automatic triage ran, and it is gated on an allow_triage decision for fork PRs).
  • No manual-required precheck comment was posted on the PR, which points to a transient step/API failure inside the precheck job itself rather than a content verdict.

The check re-runs on the next push and is expected to pass; nothing in this branch's content triggers it.

Conflicts

None (--conflict false). The merge of main was already on the branch from the base-update flow; no merge was performed this round.

Verification

  • npm run build — passed (run before and after the test edits).
  • npm run typecheck — passed (all workspaces).
  • npm run lint — passed (eslint . --ext .ts,.tsx && eslint integration-tests).
  • npx eslint on the two changed files — passed, no findings.
  • npx prettier --check on the two changed files — passed.
  • cd packages/cli && npx vitest run src/serve/virtual-subagent-sessions.test.ts — 20/20 passed.
  • cd packages/cli && npx vitest run src/serve/server.test.ts — 923/923 passed (both route cases included).
  • Both files together — 943/943 passed.
  • Mutation probe: relaxing the parent-charset regex made the two new tests fail; reverted, suite green again.
  • Integration tests after npm run bundle were not run: the touched behavior is exercised by the serve route tests against the real Express app (CI's Serve A/B already passed on the pre-round head), and this round changed tests only. Settings sources were untouched, so no schema regeneration was needed.
中文说明

本轮摘要

新提交:6688f96baatest(serve): pin parent charset and encoded slash coverage(纯测试改动;两个文件,+79/−51)。全部七条行内发现均已在代码中解决;本轮未改动任何生产逻辑。

反馈点

本轮实现(纯测试)

  • [建议] 严格的父会话 ID 字符集没有任何测试钉住 — 已实现。拒绝用例现在额外拒绝创建时包含 /:. 的父 ID,并新增一个测试:当解析的会话 ID 的父部件为 base64url('../foo') 时拒绝,从而钉住保护文件系统路径拼接的字符集。变异探针(放宽正则使其接受 :/.)恰好使这两个测试失败;还原后恢复全绿。
  • [建议] 保留字符路径在路由层只用 :%3A)测试 — 已实现。resolve+cancel 路由测试现在以 agent%3A8agent%2F8 参数化,断言解码后的 agent/8 到达 resolve,且解析出的 task id 经由真实 Express 路由到达 cancel 桥接调用。

这两条发现在上一轮曾被推迟到后续跟进,但本轮仍作为可操作项呈现。由于两者都是针对本 PR 自身引入行为的小型纯测试钉扎,故直接实现以关闭它们,而不是再次推迟。

已确认早前修复仍然有效(早前提交在合并 main 后完好)

  • [建议] 编码后长度上限与非法部件抛出相同错误 — 已在 4c2c4211db 修复;超大多字节用例断言独立的 exceeds 2000 characters 错误信息。已在 HEAD 复核。
  • [建议] 解析路径的往返校验是空转的 — 已在 4c2c4211db 修复;解析现在要求两个部件都能规范化地重新编码,并带有父/agent 部件加 padding 别名及 garbage agent 部件的测试。已在 HEAD 复核。
  • [建议] 2,000 字符上限只在拒绝方向被测试 — 已在 2ba3bbc9b3 + 9750746e90 修复;接受方向用例会往返一个 500 字符 ASCII agent ID,并钉住恰好 2,000 字符的编码会话 ID。已在 HEAD 复核。
  • [建议] 没有测试把含保留字符的 task ID 经由真实 resolve() 串起来 — 已在 a4c7df2bd2 修复;out-of-band resolve 测试以 general-purpose-agent:8 参数化,并断言会话 ID 能解析回原值。已在 HEAD 复核。
  • [建议] 500 字符部件上限以两种写法重复出现 — 已在 1c2791490a 修复;MAX_VIRTUAL_SESSION_ID_PART_LENGTH 被两个校验器共用。已在 HEAD 复核。路由层的 toolCallId 守卫有意保留为另一个请求输入边界。

失败检查:precheck-pr / precheck

已诊断为无法通过分支代码改动解决。证据:

  • 该预检扫描 PR 标题/正文及新增 diff 行中的敏感模式。在本地运行仓库自带的 assessPullRequestSafety:对净 diff、对 GitHub patch 端点暴露的每一个按 commit 的 diff、以及对合并提交,结果均为 allow_triage,原因码为零。
  • 相同的 PR 标题/正文在 PR 创建时通过了同一关卡(自动 triage 已运行,而 fork PR 的 triage 以 allow_triage 决定为前置条件)。
  • PR 上未出现 manual-required 预检评论,这指向预检任务内部的瞬时步骤/API 失败,而非内容判定。

该检查会在下次推送时重新运行,预期通过;本分支内容没有任何会触发它的模式。

冲突

无(--conflict false)。main 的合并已由 base 更新流程置于分支上;本轮未执行合并。

验证

  • npm run build — 通过(测试改动前后各运行一次)。
  • npm run typecheck — 通过(全部 workspace)。
  • npm run lint — 通过(eslint . --ext .ts,.tsx && eslint integration-tests)。
  • 对两个改动文件运行 npx eslint — 通过,无发现。
  • 对两个改动文件运行 npx prettier --check — 通过。
  • cd packages/cli && npx vitest run src/serve/virtual-subagent-sessions.test.ts — 20/20 通过。
  • cd packages/cli && npx vitest run src/serve/server.test.ts — 923/923 通过(含两个路由用例)。
  • 两个文件一起运行 — 943/943 通过。
  • 变异探针:放宽父字符集正则使两个新测试失败;还原后套件恢复全绿。
  • 未运行 npm run bundle 后的集成测试:受影响行为已由针对真实 Express 应用的 serve 路由测试覆盖(CI 的 Serve A/B 在本轮前的 head 上已通过),且本轮只改动测试。settings 来源未变动,无需重新生成 schema。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

Wire MAX_VIRTUAL_SESSION_ID_PART_LENGTH into the two subagent route
guards and interpolate MAX_VIRTUAL_SESSION_ID_LENGTH into the oversized
session id error so each limit keeps a single source of truth. Pin the
parent part's empty and length rejections in tests and document the
lone-surrogate round-trip guard.

@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 explored to full depth (tool budget reached): Context: this PR relaxes virtual subagent session ID vali...: none — all checks above were completed within budget.; Context: this PR relaxes virtual subagent session ID vali...: did not run the full server.test.ts suite (only the filtered virtual-subagent route tests), nor typecheck/lint.; Context: this PR relaxes virtual subagent session ID vali...: did not audit the web-shell client UI code paths that consume these routes beyond confirming the SDK client percent-encodes params.; Context: this PR relaxes virtual subagent session ID vali...: none — all planned checks completed within budget..

中文说明

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

未探索到全部深度(达到工具调用预算):Context: this PR relaxes virtual subagent session ID vali...:none — all checks above were completed within budget.;Context: this PR relaxes virtual subagent session ID vali...:did not run the full server.test.ts suite (only the filtered virtual-subagent route tests), nor typecheck/lint.;Context: this PR relaxes virtual subagent session ID vali...:did not audit the web-shell client UI code paths that consume these routes beyond confirming the SDK client percent-encodes params.;Context: this PR relaxes virtual subagent session ID vali...:none — all planned checks completed within budget.

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

Comment on lines +209 to +212
function isValidVirtualParentSessionId(value: string): boolean {
return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&

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.

[Suggestion] The length bound of the new strict parent-session-ID validator (value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH) is pinned by no test on either the create or the parse path — mutation-verified: deleting only that line from isValidVirtualParentSessionId survives the whole suite (colocated suite 20/20, virtual-subagent route subset 7/7), while the sibling isValidVirtualAgentId is pinned on all of its properties. (The empty-bound half was probed and dropped: /^[a-zA-Z0-9_-]+$/ already rejects empty strings, so value.length > 0 is redundant defense-in-depth and that mutant is equivalent.) — Failure scenario: under the mutant, createVirtualSubagentSessionId('a'.repeat(501), 'agent-1') stops throwing (the probe produced a 688-char id), and parse accepts a hand-crafted subagent.<800-char canonical base64url parent>.<agent> (~820 chars total, under the 2,000 cap) at the six URL-fed route call sites — all with a green suite, so the regression ships.

Suggested fix — parent-side boundary tests alongside the existing agent-side ones:

expect(() =>
  createVirtualSubagentSessionId('a'.repeat(501), 'agent-1'),
).toThrow('valid id parts');
// parse side: a crafted oversized canonical parent part stays under the 2,000 total cap
expect(
  parseVirtualSubagentSessionId(
    `subagent.${Buffer.from('a'.repeat(600)).toString('base64url')}.YWdlbnQtMQ`,
  ),
).toBeUndefined();
中文说明

[建议] 新的严格父会话 ID 校验器的长度边界(value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH)在创建路径和解析路径上都没有任何测试钉住 —— 已经过变异验证:只从 isValidVirtualParentSessionId 中删除这一行,整个测试套件仍然通过(同目录套件 20/20,虚拟 subagent 路由子集 7/7),而 sibling isValidVirtualAgentId 的所有属性都有测试钉住。(空值边界那一半经过探针验证后排除:/^[a-zA-Z0-9_-]+$/ 本身就会拒绝空字符串,所以 value.length > 0 是冗余的防御性检查,该变异体是等价变异体。)—— 失败场景:在该变异体下,createVirtualSubagentSessionId('a'.repeat(501), 'agent-1') 不再抛错(探针产生了 688 字符的 id),且 parse 会在六个由 URL 输入的路由调用点接受手工构造的 subagent.<800 字符规范 base64url 父部件>.<agent>(总计约 820 字符,低于 2,000 上限)—— 整套测试仍然是绿色的,因此回归会悄悄上线。

建议修复 —— 在现有 agent 侧测试旁补充父侧边界测试(见上方代码块)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round — PR #8717

All five inline suggestions were addressed; four by this round's commit 53265f299e, one re-verified as already resolved by the previous commit 6688f96b. No conflict handling was needed (--conflict false; origin/main was not merged).

Feedback points and dispositions

  1. [Implemented] Suggestion on virtual-subagent-sessions.ts:31 — the 500-char part cap was still hardcoded as a bare literal in the two upstream route guards (routes/session.ts, GET and POST subagent routes), leaving two unsynchronised expressions of one domain limit. MAX_VIRTUAL_SESSION_ID_PART_LENGTH is now exported and both guards compare against it, so raising the part cap raises the route guards with it (compile-time linked).
  2. [Implemented] Suggestion on virtual-subagent-sessions.ts:238 — the oversized-id error hardcoded 2000. It now interpolates MAX_VIRTUAL_SESSION_ID_LENGTH, matching the serve directory's convention (e.g. MAX_WORKSPACE_PATH_LENGTH interpolation in server/request-helpers.ts). The existing test's .toThrow('exceeds 2000 characters') still matches the produced text and will fail if the message ever stops tracking the constant.
  3. [Implemented] Suggestion on virtual-subagent-sessions.ts:223 — the decodePart(encodePart(value)) === value round-trip check reads like a tautology but is the only create-time guard against lone-surrogate aliasing (two distinct agent ids encoding to the same session id). Added a two-line comment stating that invariant, so a future "simplification" does not silently reintroduce the collision.
  4. [Implemented] Suggestion on virtual-subagent-sessions.ts:213 (R1-1, standing) — parent-session-ID validation was under-pinned. The previous commit 6688f96b already added the parent charset rejections (parent/session, parent:session, parent.session) plus a parse-side charset test; this round adds the two mutation cases that still survived: an empty parent and a 501-character parent are both asserted to throw valid id parts.
  5. [Already resolved — re-verified] Suggestion on server.test.ts (R1-2, standing) — parametrizing the reserved-character route test over %3A and %2F. The previous commit 6688f96b already converted the test to it.each([['agent%3A8', 'agent:8'], ['agent%2F8', 'agent/8']]), exercising both encoded reserved characters through the route param end to end. Re-verified in the current tree; both parametrized cases ran and passed in this round's test run.

Nothing was declined or escalated this round.

Verification

Commands actually run this round, after the changes, all from the repository root unless noted:

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx prettier --check on the three touched files — passed
  • npx vitest run src/serve/virtual-subagent-sessions.test.ts (in packages/cli) — 20 passed
  • npx vitest run src/serve/server.test.ts (in packages/cli) — 923 passed, including both %3A and %2F reserved-character route cases
  • Integration tests after npm run bundle — not run: no integration test exercises the serve subagent routes (no subagents/ references under integration-tests/); the touched route-guard behavior is exercised by the app-level route tests in server.test.ts above
  • npm run generate:settings-schema — not applicable; no settings source changed
中文说明

Autofix 审查轮次 — PR #8717

五条行内建议全部处理完毕:四条由本轮提交 53265f299e 实现,一条经复核确认已由上一次提交 6688f96b 解决。无需处理冲突(--conflict false;未合并 origin/main)。

反馈点及处理结论

  1. [已实现] 针对 virtual-subagent-sessions.ts:31 的建议 —— 500 字符部件上限在上游两处路由守卫(routes/session.ts 的 GET 与 POST subagent 路由)中仍以裸字面量硬编码,导致同一领域上限存在两处互不同步的表达。现已导出 MAX_VIRTUAL_SESSION_ID_PART_LENGTH,两处守卫均改为与该常量比较,因此调高部件上限时路由守卫会随之调高(编译期关联)。
  2. [已实现] 针对 virtual-subagent-sessions.ts:238 的建议 —— 超长 id 错误信息硬编码了 2000。现改为插值 MAX_VIRTUAL_SESSION_ID_LENGTH,与 serve 目录的惯例一致(例如 server/request-helpers.ts 中对 MAX_WORKSPACE_PATH_LENGTH 的插值)。现有测试的 .toThrow('exceeds 2000 characters') 仍与实际产生的文本匹配,且一旦错误信息不再跟随该常量,测试即会失败。
  3. [已实现] 针对 virtual-subagent-sessions.ts:223 的建议 —— decodePart(encodePart(value)) === value 往返检查看起来像恒真式,但它是创建时防止孤立代理项别名化(两个不同的 agent id 编码出相同 session id)的唯一守卫。已添加两行注释说明该不变量,避免未来的"简化"悄悄重新引入冲突。
  4. [已实现] 针对 virtual-subagent-sessions.ts:213 的建议(R1-1,遗留)—— 父会话 ID 校验的测试钉扎不足。上一次提交 6688f96b 已补充父部件字符集拒绝用例(parent/sessionparent:sessionparent.session)及 parse 侧字符集测试;本轮补齐仍然存活的两处变异用例:空父部件与 501 字符父部件均断言抛出 valid id parts
  5. [已解决 —— 复核确认] 针对 server.test.ts 的建议(R1-2,遗留)—— 将保留字符路由测试参数化为 %3A%2F。上一次提交 6688f96b 已将该测试改为 it.each([['agent%3A8', 'agent:8'], ['agent%2F8', 'agent/8']]),让两个编码后的保留字符都端到端地经过路由参数。已在当前代码树中复核;两个参数化用例在本轮测试运行中均执行并通过。

本轮没有拒绝或升级(交由维护者决策)的反馈点。

验证

本轮修改后实际运行的命令(除特别说明外均在仓库根目录执行):

  • npm run build — 通过(退出码 0)
  • npm run typecheck — 通过(退出码 0)
  • npm run lint — 通过(退出码 0)
  • 对三个改动文件运行 npx prettier --check — 通过
  • npx vitest run src/serve/virtual-subagent-sessions.test.ts(在 packages/cli 中)— 20 个通过
  • npx vitest run src/serve/server.test.ts(在 packages/cli 中)— 923 个通过,包括 %3A%2F 两个保留字符路由用例
  • npm run bundle 后的集成测试 — 未运行:集成测试中没有任何用例覆盖 serve subagent 路由(integration-tests/ 下无 subagents/ 引用);所改动的路由守卫行为已由上述 server.test.ts 中的应用级路由测试覆盖
  • npm run generate:settings-schema — 不适用;未改动任何 settings 源

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

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

No issues found. LGTM! ✅

Not explored to full depth (tool budget reached): Context: PR #8717 (QwenLM/qwen-code) relaxes virtual suba...: did not run the two changed vitest suites locally (fresh review worktree, no node_modules); boundary expectations were verified by independent arithmetic instea…; Context: PR #8717 (QwenLM/qwen-code) relaxes virtual suba...: I did not run a full npm run typecheck (repo-wide tsc) — I relied on the vitest transform/execution of the changed modules as type-level evidence. Also, I did…; Context: PR #8717 (QwenLM/qwen-code) relaxes virtual suba...: full-repo npm run typecheck — relied on successful vitest transform+execution of all changed modules as type-level evidence instead.; Context: PR #8717 (QwenLM/qwen-code) relaxes virtual suba...: full unfiltered server.test.ts run (923 tests) — deliberately skipped per the repo's "run individual/filtered tests" guidance; only the two tests changed by t….

中文说明

未发现问题。LGTM!✅

未探索到全部深度(达到工具调用预算):Context: PR #8717 (QwenLM/qwen-code) relaxes virtual suba...:did not run the two changed vitest suites locally (fresh review worktree, no node_modules); boundary expectations were verified by independent arithmetic instea…;Context: PR #8717 (QwenLM/qwen-code) relaxes virtual suba...:I did not run a full npm run typecheck (repo-wide tsc) — I relied on the vitest transform/execution of the changed modules as type-level evidence. Also, I did…;Context: PR #8717 (QwenLM/qwen-code) relaxes virtual suba...:full-repo npm run typecheck — relied on successful vitest transform+execution of all changed modules as type-level evidence instead.;Context: PR #8717 (QwenLM/qwen-code) relaxes virtual suba...:full unfiltered server.test.ts run (923 tests) — deliberately skipped per the repo's "run individual/filtered tests" guidance; only the two tests changed by t…

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary — PR #8717

Feedback points and dispositions

rc:3761713110 — [Suggestion] Parent-session-ID length bound unpinned by tests (virtual-subagent-sessions.ts:212)

Disposition: Addressed (RESOLVED).

The finding asked for the MAX_VIRTUAL_SESSION_ID_PART_LENGTH bound in
isValidVirtualParentSessionId to be pinned by tests on both the create and
parse paths. Verified against the current head before acting:

  • The create-side half was already pinned by the previous commit
    (refactor(serve): sync virtual subagent id length limits): the colocated
    suite contains
    createVirtualSubagentSessionId('a'.repeat(501), 'agent-1') → throws
    'valid id parts'.
  • The parse-side half was still unpinned — no test parsed an oversized
    parent part.

Change: added one colocated test, rejects an oversized parent part under the total session id cap, which parses subagent.<canonical base64url of a 600-char parent>.YWdlbnQtMQ (820 chars total, under the 2,000 session-id
cap) and expects undefined. It sits alongside the existing parse-side
rejection tests and follows their style.

Mutation verification (applied and reverted in this round): deleting only the
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH line from
isValidVirtualParentSessionId now fails both
rejects invalid, oversized, or non-round-trippable id parts (create path)
and the new parse-side test — the bound is pinned on both paths, as the
finding requested.

Note: the finding's remark that value.length > 0 is redundant next to the
+-quantified charset regex was left untouched — the reviewer explicitly
probed and dropped that half, and the check is not redundant in the sibling
isValidVirtualAgentId (which has no charset regex), so keeping the two
validators symmetric is intentional. No source-code change was needed this
round; the fix is test-only.

Changes

  • packages/cli/src/serve/virtual-subagent-sessions.test.ts (+11 lines):
    parse-side boundary test for the parent part length limit.

Commit: a91880c658 test(serve): pin parse-side parent id length limit (#8717)

Verification

  • npm run build — passed
  • npm run typecheck — passed (all workspaces)
  • npm run lint — passed
  • npx vitest run src/serve/virtual-subagent-sessions.test.ts (packages/cli,
    touched) — 21 passed
  • npx vitest run src/serve/server.test.ts -t 'subagent' (route-level
    subset) — 5 passed | 918 skipped
  • Mutation check: removing the parent length bound fails the create-side and
    the new parse-side test; mutation reverted, clean tree confirmed
  • npx prettier --check on the touched test file — passed
中文说明

Autofix 审查轮次总结 — PR #8717

反馈点及处理

rc:3761713110 — [建议] 父会话 ID 长度边界缺少测试钉住(virtual-subagent-sessions.ts:212)

处理:已解决(RESOLVED)。

该 finding 要求用测试在创建路径和解析路径上同时钉住
isValidVirtualParentSessionId 中的
MAX_VIRTUAL_SESSION_ID_PART_LENGTH 边界。行动前已对照当前 HEAD 核实:

  • 创建侧一半已由上一个提交
    refactor(serve): sync virtual subagent id length limits)钉住:
    同目录测试套件中包含
    createVirtualSubagentSessionId('a'.repeat(501), 'agent-1') → 抛出
    'valid id parts'
  • 解析侧一半仍未钉住 —— 没有任何测试解析超长的父部件。

改动:新增一个同目录测试 rejects an oversized parent part under the total session id cap,解析 subagent.<600 字符父 ID 的规范 base64url>.YWdlbnQtMQ
(总计 820 字符,低于 2,000 的会话 ID 上限)并期望返回 undefined
该测试紧邻现有的解析侧拒绝测试,并遵循其风格。

变异验证(本轮内应用并已还原):仅删除
isValidVirtualParentSessionId 中的
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH 这一行后,
rejects invalid, oversized, or non-round-trippable id parts(创建路径)
和新增的解析侧测试同时失败 —— 该边界已在两条路径上被钉住,
与 finding 的要求一致。

说明:finding 中提到在 + 量词字符集正则旁 value.length > 0 是冗余的,
这一半未做改动 —— 审查者已明确探针验证后放弃了这一半,
且该检查在 sibling isValidVirtualAgentId(没有字符集正则)中并不冗余,
因此保持两个校验器对称是有意为之。本轮无需源码改动;修复仅为测试。

变更内容

  • packages/cli/src/serve/virtual-subagent-sessions.test.ts(+11 行):
    父部件长度限制的解析侧边界测试。

提交:a91880c658 test(serve): pin parse-side parent id length limit (#8717)

验证

  • npm run build — 通过
  • npm run typecheck — 通过(全部 workspace)
  • npm run lint — 通过
  • npx vitest run src/serve/virtual-subagent-sessions.test.ts(packages/cli,
    被触碰包)— 21 通过
  • npx vitest run src/serve/server.test.ts -t 'subagent'(路由层子集)—
    5 通过 | 918 跳过
  • 变异检查:删除父侧长度边界后创建侧与新增解析侧测试均失败;
    变异已还原,工作树干净已确认
  • 对被触碰的测试文件执行 npx prettier --check — 通过

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

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

⚠️ This run could not certify that any of this diff was reviewed. Suggestions are inline. Not reviewed: the entire diff — no agent reported covering it; nobody read it. Not reviewed: the linked-issue fidelity pass, the line-by-line correctness pass, the security pass, the reuse and duplication pass, the altitude and abstraction pass, the consistency and clarity pass, the performance pass, the test-coverage pass, the open-ended audit (attacker mindset), the open-ended audit (oncall mindset), the open-ended audit (maintainer mindset), the removed-behavior audit, the cross-file consistency pass — its prompt was built, but no agent on record was launched with it. Not reviewed: verification and reverse audit — both prompts were built, but no agent was launched with either — the posted findings cannot be counted as verified, and the pass that hunts what the rest of the review missed cannot be certified.

中文说明

⚠️ 本次运行无法证明这个 diff 的任何部分经过了审查。 建议见行内评论。 未审查:整个 diff——没有 agent 报告覆盖过这部分,也没有人读过它。 未审查:关联 issue 一致性检查、逐行正确性检查、安全检查、复用与重复代码检查、修复层次与抽象合理性检查、一致性与可读性检查、性能检查、测试覆盖检查、开放式审计(攻击者视角)、开放式审计(值班排障视角)、开放式审计(后续维护者视角)、删除行为审计、跨文件一致性检查——它的 prompt 已构建,但没有任何 agent 有记录用它启动过。 未审查:验证与反向审计——两份 prompt 都已构建,但都没有 agent 用它们启动——发布的发现不能算作已验证,搜寻评审遗漏问题的工序也无法作证。

— deepseek-v4-flash via Qwen Code /review (v0.21.8)

Comment on lines +2408 to +2409
const toolCallId = req.params['toolCallId'];
if (!toolCallId || toolCallId.length > 500) {
if (!toolCallId || toolCallId.length > MAX_VIRTUAL_SESSION_ID_PART_LENGTH) {

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.

[Suggestion] Route parameter name :toolCallId is misleading since it also accepts agent task IDs — Failure scenario: A maintainer sees the route /session/:id/subagents/:toolCallId and the error message "toolCallId must be a non-empty tool call id", reasonably assumes the value is always a tool call ID, and adds a regex guard that rejects : and /. The feature that resolves agent task IDs with these characters would silently return 400 errors.

中文说明

路由参数名 :toolCallId 具有误导性,因为它同时也接受 agent task ID——当维护者看到 /session/:id/subagents/:toolCallId 路由时,会合理认为该值始终是工具调用 ID,可能会添加拒绝 :/ 的正则校验,导致该功能静默返回 400 错误。建议将路由参数重命名为 :taskRef:subagentRef,并更新相关的错误消息。

— deepseek-v4-flash via Qwen Code /review (v0.21.8)

@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 explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all planned checks completed within budget..

中文说明

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

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all planned checks completed within budget.

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

Comment on lines +249 to +252
if (
!sessionId.startsWith(PREFIX) ||
sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH
) {

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.

[Suggestion] R5-1: The parse-side total-length rejection branch has no decisive test — every 2,000-char cap assertion exercises createVirtualSubagentSessionId, and the test named "rejects an oversized parent part under the total session id cap" builds an ~820-char id that is rejected by the 500-char part cap, not by this branch. Probe-confirmed: removing only this condition leaves all 21 tests green. — Failure scenario: parseVirtualSubagentSessionId consumes client-supplied session ids at six route call sites; if a future change drops this check (rationale: create enforces the cap — but create only sees server-generated inputs while parse sees untrusted request strings), every existing test stays green and oversized attacker-crafted ids flow into the virtual-subagent lookup path.

// decisive parse-side case: ASCII parent at the part cap + multibyte agent part
const parentPart = Buffer.from('a'.repeat(500), 'utf8').toString('base64url'); // 667 chars
const agentPart = Buffer.from('界'.repeat(500), 'utf8').toString('base64url'); // 2,000 chars
expect(
  parseVirtualSubagentSessionId(`subagent.${parentPart}.${agentPart}`), // 2,677 chars
).toBeUndefined();

Also rename the mislabeled test to reflect the part cap it actually exercises.

中文说明

解析侧的总长度拒绝分支没有决定性测试——所有 2,000 字符上限的断言都走 createVirtualSubagentSessionId;名为 "rejects an oversized parent part under the total session id cap" 的测试构造的 ~820 字符 ID 实际是被 500 字符部件上限拒绝的,而非此分支。已用探针确认:仅删除该条件后全部 21 个测试仍然通过。— 失败场景:parseVirtualSubagentSessionId 在 6 个路由调用点消费客户端提供的会话 ID;若未来改动删除此检查(理由可能是 create 已强制上限——但 create 只见到服务端生成的输入,而 parse 面对的是不可信的请求字符串),所有现有测试仍为绿色,超长的攻击构造 ID 会流入虚拟 subagent 查找路径。建议补充决定性的解析侧用例(ASCII 父部件达到部件上限 + 多字节 agent 部件,编码后总长 2,677 字符 → undefined,已双向验证),并将名称不实的测试重命名以反映其实际覆盖的部件上限。

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

Comment on lines +209 to +215
function isValidVirtualParentSessionId(value: string): boolean {
return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&
/^[a-zA-Z0-9_-]+$/.test(value)
);
}

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.

[Suggestion] R5-2: The strict-parent / relaxed-agent validator asymmetry is load-bearing but undocumented — the decoded parentSessionId is interpolated into filesystem paths (getSubagentSessionDir, ${projectDir}/chats/${parentSessionId}.jsonl — the latter unsanitized), while agentId is only ever string-compared, and requireSessionRuntime skips its membership lookup entirely in the single-workspace case, so no gate stands behind the charset there. — Failure scenario: a future maintainer "harmonizes" the two adjacent same-shape validators and relaxes the parent to the round-trip check → a crafted id like subagent.<b64url("../../foo")>.<...> carries a traversal-shaped parent into the path constructions; the existing '../foo' test would fail, but nothing in production code tells the maintainer why that charset matters before they change it.

Suggested change
function isValidVirtualParentSessionId(value: string): boolean {
return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&
/^[a-zA-Z0-9_-]+$/.test(value)
);
}
// Parent ids reach filesystem paths, so they keep the strict charset;
// agent ids are comparison-only and may use the round-trippable space.
function isValidVirtualParentSessionId(value: string): boolean {
return (
value.length > 0 &&
value.length <= MAX_VIRTUAL_SESSION_ID_PART_LENGTH &&
/^[a-zA-Z0-9_-]+$/.test(value)
);
}
中文说明

严格父 ID / 宽松 agent ID 的校验器不对称是关键设计但没有文档说明——解码出的 parentSessionId 会被插入文件系统路径(getSubagentSessionDir${projectDir}/chats/${parentSessionId}.jsonl,后者未经净化),而 agentId 只做字符串比较;并且 requireSessionRuntime 在单工作区场景会完全跳过成员查找,因此那里没有任何网关兜底该字符集。— 失败场景:未来的维护者"统一"这两个相邻且同形的校验器,把父 ID 放宽为往返校验 → 形如 subagent.<b64url("../../foo")>.<...> 的构造 ID 会把穿越形状的父 ID 带入路径构造;现有的 '../foo' 测试会失败,但生产代码中没有任何东西能在维护者动手之前告诉他这个字符集为何重要。建议在函数上方加一行注释说明原因。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary — PR #8717

Feedback points and dispositions

1. Review-level comment by @doudouOUC (deepseek-v4-flash via Qwen Code /review)

⚠️ This run could not certify that any of this diff was reviewed. …

Disposition: informational — no code action available. This comment is a certification statement about the review run's own internal bookkeeping (which audit passes were launched/recorded), not a code-level request. The only finding that run produced is the inline suggestion below, which is addressed in this round.

2. Inline [rc:3762339069] — [Suggestion] Route parameter name :toolCallId is misleading since it also accepts agent task IDs (packages/cli/src/serve/routes/session.ts:2409)

Disposition: implemented.

Verified the finding against the code before acting:

  • VirtualSubagentSessions.resolve() matches the route parameter against the agent task id (candidate.id === ref), the tool-use id (candidate.toolUseId === ref), and a task-id suffix (candidate.id.endsWith(-${ref})), and the code comments note /fork tasks are referenced by task id, not tool call id.
  • The failure scenario is concrete, not hypothetical: this branch's own tests pass refs containing : and / (agent:8, agent/8) through these routes, and the SDK test suite does the same (agent/1). A future charset guard assuming "always a tool call id" would silently 400 those.
  • The misleading name and message sit inside the exact validation hunks this PR modifies, and this is an early review round, so the rename is in scope.

Changes (behavior-neutral; the URL shape is unchanged, only the Express parameter name, labels, and error text):

  • packages/cli/src/serve/routes/session.ts: renamed the route parameter :toolCallId:subagentRef on both GET /session/:id/subagents/:subagentRef and POST /session/:id/subagents/:subagentRef/cancel, including handler variables, route labels, the 400 message (now `subagentRef` must be a non-empty subagent reference), the error code (invalid_tool_call_idinvalid_subagent_ref), and the 404 echo field. Chose subagentRef over taskRef because the value can be a tool call id as well as a task id; the name also matches existing codebase usage (focusedSubagentRef in ToolGroupMessage.tsx). Grepped for consumers of the old error code and 404 body field before renaming — none exist.
  • packages/cli/src/serve/server/telemetry.ts: renamed the two catalog entries' path/route labels to match (the drift-guard test telemetry-catalog.test.ts cross-checks this catalog against the actual Express registrations in both directions).
  • packages/sdk-typescript/src/daemon/DaemonClient.ts: renamed the positional parameter of resolveSubagentSession / cancelSubagentSession and their route-label strings to the new name (positional parameters, so no caller is affected; URL construction is unchanged).
  • packages/cli/src/serve/server.test.ts: renamed the local names in the existing virtual-subagent route test block to match.

Conflict notes

--conflict false; no merge of origin/main was performed, no conflicts.

Verification

Commands actually run this round (after the change, on commit 3d0f6e008a):

  • npm run build — passed
  • npm run typecheck — passed, 0 TS errors (the first run failed only because dependent packages' dist/ outputs were not yet built in this checkout; it passed after npm run build)
  • npm run lint — passed
  • npx prettier --check on the four changed files — passed
  • cd packages/cli && npx vitest run src/serve/server/telemetry.test.ts src/serve/server/telemetry-catalog.test.ts src/serve/virtual-subagent-sessions.test.ts — 3 files, 72 tests passed (includes the telemetry drift guard)
  • cd packages/cli && npx vitest run src/serve/server.test.ts — 923 tests passed (includes the renamed virtual-subagent route tests with :// refs)
  • cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts — 329 tests passed (asserts the unchanged subagent route URLs)

Integration tests after npm run bundle were not needed: the change is route-naming-only and is exercised end-to-end by the supertest route suites above. No settings source changed, so npm run generate:settings-schema was not required.

中文说明

Autofix 评审轮次总结 — PR #8717

反馈点及处理

1. @doudouOUC(deepseek-v4-flash via Qwen Code /review)的评审级评论

⚠️ 本次运行无法证明这个 diff 的任何部分经过了审查。……

处理:信息性说明——无需代码层面的操作。 该评论是评审运行自身内部记账(哪些审计工序被启动/被记录)的证明性陈述,不是代码层面的诉求。该次运行产出的唯一发现就是下面的行内建议,本轮已处理。

2. 行内 [rc:3762339069] — [Suggestion] 路由参数名 :toolCallId 具有误导性,因为它同时接受 agent task ID(packages/cli/src/serve/routes/session.ts:2409

处理:已实现。

修改前先对照代码核实了该发现:

  • VirtualSubagentSessions.resolve() 会将该路由参数与 agent task idcandidate.id === ref)、tool-use idcandidate.toolUseId === ref)以及 task-id 后缀candidate.id.endsWith(-${ref}))进行匹配,且代码注释说明 /fork 任务是通过 task id 而非 tool call id 来引用的。
  • 失败场景是具体而非假设性的:本分支自身的测试就通过这些路由传递包含 :/ 的引用(agent:8agent/8),SDK 测试套件也是如此(agent/1)。未来若有维护者假设"该值永远是 tool call id"而添加字符集校验,这些请求会被静默地返回 400。
  • 误导性的命名与错误消息恰好位于本 PR 修改的校验代码块内,且当前处于评审早期轮次,因此该重命名在 PR 范围之内。

变更内容(行为不变;URL 形状未改变,仅改动 Express 参数名、标签与错误文案):

  • packages/cli/src/serve/routes/session.ts:将 GET /session/:id/subagents/:subagentRefPOST /session/:id/subagents/:subagentRef/cancel 两个路由上的路由参数 :toolCallId:subagentRef,包括处理函数内的变量名、路由标签、400 错误消息(现为 `subagentRef` must be a non-empty subagent reference)、错误码(invalid_tool_call_idinvalid_subagent_ref)以及 404 回显字段。选择 subagentRef 而非 taskRef,因为该值既可能是 task id 也可能是 tool call id;该命名也与代码库现有用法一致(ToolGroupMessage.tsx 中的 focusedSubagentRef)。重命名前已 grep 确认旧错误码与 404 回显字段没有任何消费方。
  • packages/cli/src/serve/server/telemetry.ts:同步重命名两个目录条目的 path/route 标签(漂移守卫测试 telemetry-catalog.test.ts 会双向校验该目录与真实 Express 路由注册一致)。
  • packages/sdk-typescript/src/daemon/DaemonClient.ts:将 resolveSubagentSession / cancelSubagentSession 的位置参数名及其路由标签字符串重命名为新名称(位置参数,所有调用方不受影响;URL 构造不变)。
  • packages/cli/src/serve/server.test.ts:将既有虚拟 subagent 路由测试块中的局部变量名同步重命名。

冲突说明

--conflict false;未执行 origin/main 合并,无冲突。

验证

本轮实际执行的命令(改动之后,基于提交 3d0f6e008a):

  • npm run build — 通过
  • npm run typecheck — 通过,0 个 TS 错误(首次运行失败仅因本检出中依赖包的 dist/ 产物尚未构建;执行 npm run build 后通过)
  • npm run lint — 通过
  • 对四个变更文件执行 npx prettier --check — 通过
  • cd packages/cli && npx vitest run src/serve/server/telemetry.test.ts src/serve/server/telemetry-catalog.test.ts src/serve/virtual-subagent-sessions.test.ts — 3 个文件、72 个测试通过(含遥测漂移守卫测试)
  • cd packages/cli && npx vitest run src/serve/server.test.ts — 923 个测试通过(含重命名后带 :// 引用的虚拟 subagent 路由测试)
  • cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts — 329 个测试通过(断言 subagent 路由 URL 保持不变)

无需在 npm run bundle 之后运行集成测试:本次改动仅涉及路由命名,且上述 supertest 路由测试套件已对其做了端到端覆盖。未改动任何 settings 源文件,因此无需执行 npm run generate:settings-schema

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

Add a decisive parse-side test for the 2,000-char total length cap where
both decoded parts sit at the part length cap, rename the mislabeled
part-cap test, and document why the parent validator keeps the strict
charset while the agent validator only requires round-tripping.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下:

Autofix review response — PR #8717 (round 5)

Two [Suggestion] findings from the automated reviewer; both verified against the code and addressed. No failed checks, no base conflicts.

R5-1 (rc:3762433531) — parse-side total-length branch has no decisive test → Addressed

Verified the finding first: the test named "rejects an oversized parent part under the total session id cap" builds an 820-char id (600-char parent → 800-char base64url part, total well under 2,000), which is rejected by the 500-char per-part cap in isValidVirtualParentSessionId, never by the parse-side total-length check.

Changes:

  • Added a decisive parse-side test 'rejects an oversized session id at the parse-side total length cap': ASCII parent decoded to exactly 500 chars (667 chars encoded) plus a multibyte agent part of exactly 500 chars (2,000 chars encoded) → a 2,677-char id. Both decoded parts sit exactly at the per-part cap, so only the 2,000-char total-length branch can reject it; the test pins the length (2,677) and expects undefined.
  • Mutation probe: removing only the sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH condition from parseVirtualSubagentSessionId makes the new test fail (1 failed | 21 skipped), confirming decisiveness; the source was restored afterwards.
  • Renamed the mislabeled test to 'rejects an oversized parent part above the part length cap' to reflect the cap it actually exercises.

R5-2 (rc:3762433535) — strict-parent / relaxed-agent asymmetry is load-bearing but undocumented → Addressed

Verified the claim: the decoded parentSessionId is interpolated into filesystem paths (getSubagentSessionDir(projectDir, parentSessionId) and the unsanitized ${projectDir}/chats/${parentSessionId}.jsonl in both findLegacyTaskByToolCall and readParentToolCallMetrics), while agentId is only ever string-compared against task ids. Added the suggested two-line comment above isValidVirtualParentSessionId explaining why the parent keeps the strict charset while the agent validator only requires round-tripping. Comment-only change; no behavior change.

Conflict

None (--conflict false); no merge performed.

Commit

44f0e4c56d test(serve): pin parse-side total session id cap (#8717) — 2 files changed, 18 insertions(+), 1 deletion(-).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check (both touched files) — passed
  • npx vitest run src/serve/virtual-subagent-sessions.test.ts (packages/cli, touched) — 22 passed (21 before + the new test)
  • npx vitest run src/serve/virtual-subagent-sessions.test.ts src/serve/server.test.ts (packages/cli, touched) — 944 passed | 1 skipped
  • Mutation probe: removed only the parse-side total-length condition → new test failed as expected; restored the source and re-ran → 22 passed
  • Integration tests after npm run bundle: not run — the changed behavior (id validation unit tests and a code comment) is fully exercised by the unit tests, not only through the bundled CLI or integration harness
  • npm run generate:settings-schema: not needed — no settings source changed
中文说明

Autofix 评审响应 — PR #8717(第 5 轮)

自动化评审器提出了两条 [Suggestion] 发现;均已对照代码核实并处理。无失败检查,无与 base 分支的冲突。

R5-1 (rc:3762433531) — 解析侧总长度分支没有决定性测试 → 已处理

先核实了该发现:名为 "rejects an oversized parent part under the total session id cap" 的测试构造了一个 820 字符的 ID(600 字符父部件 → 800 字符 base64url 编码,总长远低于 2,000),实际是被 isValidVirtualParentSessionId 中的 500 字符部件上限拒绝的,从未经过解析侧总长度检查。

改动内容:

  • 新增决定性的解析侧测试 'rejects an oversized session id at the parse-side total length cap':ASCII 父部件解码后恰好 500 字符(编码后 667 字符),加上恰好 500 字符的多字节 agent 部件(编码后 2,000 字符),得到总长 2,677 字符的 ID。两个解码后的部件都恰好处于部件上限,因此只有 2,000 字符总长度分支能拒绝它;测试固定了总长(2,677)并断言返回 undefined
  • 变异探针:仅删除 parseVirtualSubagentSessionId 中的 sessionId.length > MAX_VIRTUAL_SESSION_ID_LENGTH 条件后,新测试失败(1 failed | 21 skipped),确认其决定性;随后已还原源码。
  • 将名称不实的测试重命名为 'rejects an oversized parent part above the part length cap',以反映其实际覆盖的上限。

R5-2 (rc:3762433535) — 严格父 ID / 宽松 agent ID 的不对称是关键设计但没有文档 → 已处理

已核实该论断:解码出的 parentSessionId 会被插入文件系统路径(getSubagentSessionDir(projectDir, parentSessionId),以及 findLegacyTaskByToolCallreadParentToolCallMetrics 中未经净化的 ${projectDir}/chats/${parentSessionId}.jsonl),而 agentId 只做与任务 ID 的字符串比较。按建议在 isValidVirtualParentSessionId 上方加了两行注释,说明父 ID 为何保留严格字符集、而 agent 校验器只要求往返一致。纯注释改动,无行为变化。

冲突

无(--conflict false);未执行合并。

提交

44f0e4c56d test(serve): pin parse-side total session id cap (#8717) — 2 个文件变更,18 行新增,1 行删除。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check(两个改动文件)— 通过
  • npx vitest run src/serve/virtual-subagent-sessions.test.ts(packages/cli,触及)— 22 通过(原 21 个 + 新增测试)
  • npx vitest run src/serve/virtual-subagent-sessions.test.ts src/serve/server.test.ts(packages/cli,触及)— 944 通过 | 1 跳过
  • 变异探针:仅删除解析侧总长度条件 → 新测试按预期失败;还原源码后重跑 → 22 通过
  • npm run bundle 后的集成测试:未运行 —— 本次改动(ID 校验单元测试与一条代码注释)完全由单元测试覆盖,并非只能通过打包 CLI 或集成测试框架验证
  • npm run generate:settings-schema:无需运行 —— 未改动任何 settings 源

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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 — no blockers. Suggestions are inline.

中文说明

已审查——无阻断问题。 建议见行内评论。

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

Comment on lines 2428 to 2432
const resolved = await virtualSubagentSessions.resolve(
runtime,
sessionId,
toolCallId,
subagentRef,
);

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.

[Suggestion] The toolCallIdsubagentRef rename stops one layer short: VirtualSubagentSessions.resolve()'s third parameter is still named toolCallId, although after this change the value flowing in is routinely a provider task id like agent:8 rather than a tool-call id — the PR's own tests pass task ids here. — Failure scenario: a maintainer tracing subagentRef from the route into resolve(runtime, parentSessionId, toolCallId) is told the wrong contract at the seam and may reason the ref must be a transcript tool-call id when analyzing the candidate.id === toolCallId || candidate.toolUseId === toolCallId || candidate.id.endsWith(...) matching; a grep for subagentRef also misses the layer where matching actually happens.

The fix belongs in virtual-subagent-sessions.ts (not at this call site): rename resolve()'s third parameter to subagentRef. The transcript-level helpers (findLegacyTaskByToolCall, readParentToolCallMetrics) genuinely compare transcript tool-call ids and can keep their names.

中文说明

[建议] toolCallIdsubagentRef 的重命名还差一层:VirtualSubagentSessions.resolve() 的第三个参数仍然叫 toolCallId,而本次改动之后,流入这个参数的值通常是 agent:8 这样的 provider task id,而不是工具调用 id —— 本 PR 自己的测试也是在这里传入 task id。—— 失败场景:维护者从路由追踪 subagentRef 进入 resolve(runtime, parentSessionId, toolCallId) 时,会在这个接缝处得到错误的契约,在分析 candidate.id === toolCallId || candidate.toolUseId === toolCallId || candidate.id.endsWith(...) 匹配时可能误以为 ref 必须是 transcript 工具调用 id;grep subagentRef 也会漏掉真正发生匹配的这一层。

修复位置在 virtual-subagent-sessions.ts(而不是这个调用点):把 resolve() 的第三个参数重命名为 subagentRef。transcript 层的辅助函数(findLegacyTaskByToolCallreadParentToolCallMetrics)确实是在比较 transcript 的工具调用 id,可以保持不变。

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

Comment on lines +2414 to +2415
error: '`subagentRef` must be a non-empty subagent reference',
code: 'invalid_subagent_ref',

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.

[Suggestion] The 400 message only mentions the non-empty constraint, but this diff's guard also rejects refs longer than MAX_VIRTUAL_SESSION_ID_PART_LENGTH (500) — and an Express :subagentRef segment can never be empty for a matched route, so the length bound is the only practically reachable trigger. The sibling parseClientIdHeader in server/request-helpers.ts states both constraints ("must be a non-empty token of 128 characters or fewer"). The identical block in the cancel route (~line 2469) has the same issue. — Failure scenario: a client sends a 501-character subagentRef (a long provider task id); the daemon answers 400 with a message implying the ref was empty, so the debugging engineer investigates the wrong property.

Suggested change
error: '`subagentRef` must be a non-empty subagent reference',
code: 'invalid_subagent_ref',
error: `\`subagentRef\` must be a non-empty subagent reference of ${MAX_VIRTUAL_SESSION_ID_PART_LENGTH} characters or fewer`,
code: 'invalid_subagent_ref',
中文说明

[建议] 这个 400 错误信息只提到了非空约束,但本次改动的守卫还会拒绝超过 MAX_VIRTUAL_SESSION_ID_PART_LENGTH(500)的 ref —— 而 Express 的 :subagentRef 段在匹配到路由时不可能为空,所以长度上限才是实际唯一可能触发的条件。同目录的 server/request-helpers.tsparseClientIdHeader 会同时声明两个约束("must be a non-empty token of 128 characters or fewer")。cancel 路由中相同的代码块(约第 2469 行)存在同样的问题。—— 失败场景:客户端发送 501 个字符的 subagentRef(比如一个很长的 provider task id);daemon 返回 400 并提示 ref 为空,调试的工程师会因此排查错误的方向。

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

Comment on lines +2408 to +2412
const subagentRef = req.params['subagentRef'];
if (
!subagentRef ||
subagentRef.length > MAX_VIRTUAL_SESSION_ID_PART_LENGTH
) {

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.

[Suggestion] The subagentRef extraction + length validation + coded-400 payload is duplicated verbatim in the two subagent routes this PR rewrites (GET resolve and POST cancel). server/request-helpers.ts is the established home for exactly this shape — requireSessionId and parseClientIdHeader (extraction + length cap + coded 400) — and this file already imports from it. — Failure scenario: any future change to this validation must be applied to both copies in lockstep — this very diff had to edit both copies identically twice (the rename and the constant). One missed copy silently diverges the resolve and cancel routes' acceptance boundaries, so a ref accepted by resolve is rejected by cancel (or vice versa).

Extract a shared helper called from both handlers, e.g. beside requireSessionId:

function requireSubagentRef(req: Request, res: Response): string | null {
  const subagentRef = req.params['subagentRef'];
  if (
    !subagentRef ||
    subagentRef.length > MAX_VIRTUAL_SESSION_ID_PART_LENGTH
  ) {
    res.status(400).json({
      error: '`subagentRef` must be a non-empty subagent reference',
      code: 'invalid_subagent_ref',
    });
    return null;
  }
  return subagentRef;
}
中文说明

[建议] 本 PR 重写的两个 subagent 路由(GET resolve 和 POST cancel)中,subagentRef 的提取 + 长度校验 + 带 code 的 400 响应体是逐字重复的。server/request-helpers.ts 正是这类逻辑的既定归属 —— requireSessionIdparseClientIdHeader(提取 + 长度上限 + 带 code 的 400)—— 而且本文件已经从该模块导入。—— 失败场景:未来对这个校验的任何修改都必须同步应用到两处 —— 本 diff 自己就不得不两次同步修改这两份副本(重命名和常量)。漏改一处会悄悄让 resolve 与 cancel 路由的接受边界产生分歧,导致 resolve 接受的 ref 被 cancel 拒绝(反之亦然)。

提取一个两个 handler 共用的辅助函数,例如放在 requireSessionId 旁边(示例代码见上)。

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

Comment on lines +8788 to +8791
it.each([
['agent%3A8', 'agent:8'],
['agent%2F8', 'agent/8'],
])(

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.

[Suggestion] Both added route cases decode to forms containing no %, so the tests cannot pin that the router decodes subagentRef exactly once (the plain unencoded case the replaced test had was also dropped). Mutation-probe verified: layering decodeURIComponent(req.params['subagentRef']!) on top of Express's built-in decode — a plausible "fix" in exactly this code area — passes both existing cases green, because decodeURIComponent('agent:8') === 'agent:8'. — Failure scenario: the relaxed validator accepts any round-trippable UTF-8 agent id, which includes %: for a task whose literal id is agent%3A8, the SDK sends agent%253A8, Express decodes once to agent%3A8, and a double-decoding handler resolves/cancels agent:8 instead — the wrong subagent session (or 404 on a valid one).

Suggested change
it.each([
['agent%3A8', 'agent:8'],
['agent%2F8', 'agent/8'],
])(
it.each([
['agent%3A8', 'agent:8'],
['agent%2F8', 'agent/8'],
['agent%253A8', 'agent%3A8'],
])(

The added case passes today and fails under the double-decode mutant; optionally also restore the pre-diff plain scenario with ['tool-1', 'tool-1'].

中文说明

[建议] 新增的两个路由用例解码后都不含 %,因此测试无法钉住"路由器只对 subagentRef 解码一次"这一点(被替换的旧测试中的纯文本未编码用例也被删掉了)。已用变异探针验证:在 Express 内建解码之上再叠一层 decodeURIComponent(req.params['subagentRef']!) —— 在这段代码区域是一个很"合理"的"修复" —— 两个现有用例仍然全绿,因为 decodeURIComponent('agent:8') === 'agent:8'。—— 失败场景:放宽后的校验器接受任何可无损往返的 UTF-8 agent id,其中包括含 % 的:若某个 task 的字面 id 是 agent%3A8,SDK 会发送 agent%253A8,Express 解码一次得到 agent%3A8,而二次解码的 handler 会解析/取消 agent:8 —— 命中错误的 subagent 会话(或对一个有效会话返回 404)。

新增的 ['agent%253A8', 'agent%3A8'] 用例在当前代码下通过,在二次解码变异体下失败;也可以选择用 ['tool-1', 'tool-1'] 恢复改动前的纯文本场景。

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

Comment on lines +2410 to +2411
!subagentRef ||
subagentRef.length > MAX_VIRTUAL_SESSION_ID_PART_LENGTH

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.

[Suggestion] The route-level 400 guard this PR rewrote (constant-based cap + new invalid_subagent_ref code) has zero test coverage — the only route tests are the two happy-path it.each cases below. Probe-verified: removing this guard flips an oversized ref's response from 400 to 500 (Virtual subagent session ids require valid id parts), because the ref reaches resolve() and suffix-matches a long task id. The cancel route's copy (~line 2465) is equally untested. — Failure scenario: with nothing pinning the 400 contract, a follow-up refactor (e.g. deduplicating this guard into request-helpers.ts) that drops or mis-bounds the length check ships green, letting >500-char refs reach resolve() — reintroducing via the back door the HTTP-500 failure class this PR exists to fix.

Suggested test alongside the happy-path cases, following this file's request(app) + Host/Bearer scaffolding:

it('rejects an oversized subagentRef with a 400', async () => {
  const oversized = 'a'.repeat(MAX_VIRTUAL_SESSION_ID_PART_LENGTH + 1);
  const tokenOpts: ServeOptions = { ...baseOpts, token: 'secret' };
  const app = createServeApp(
    { ...tokenOpts, workspace: WS_BOUND },
    undefined,
    { bridge: fakeBridge() },
  );
  const resolveRes = await request(app)
    .get(`/session/s-1/subagents/${oversized}`)
    .set('Host', `127.0.0.1:${tokenOpts.port}`)
    .set('Authorization', 'Bearer secret');
  const cancelRes = await request(app)
    .post(`/session/s-1/subagents/${oversized}/cancel`)
    .set('Host', `127.0.0.1:${tokenOpts.port}`)
    .set('Authorization', 'Bearer secret');
  expect(resolveRes.status).toBe(400);
  expect(resolveRes.body).toMatchObject({ code: 'invalid_subagent_ref' });
  expect(cancelRes.status).toBe(400);
  expect(cancelRes.body).toMatchObject({ code: 'invalid_subagent_ref' });
});
中文说明

[建议] 本 PR 重写的路由级 400 守卫(基于常量的上限 + 新的 invalid_subagent_ref code)没有任何测试覆盖 —— 路由测试只有下面两个 happy-path it.each 用例。已用探针验证:移除该守卫后,超长 ref 的响应会从 400 变为 500(Virtual subagent session ids require valid id parts),因为 ref 会进入 resolve() 并与长 task id 发生后缀匹配。cancel 路由中的副本(约第 2465 行)同样没有测试。—— 失败场景:在没有任何测试钉住 400 契约的情况下,后续的 refactor(比如把这个守卫去重提取到 request-helpers.ts)如果漏掉或写错长度检查,仍然能全绿合入,让超过 500 字符的 ref 进入 resolve() —— 从后门重新引入本 PR 要修复的 HTTP-500 失败类别。

建议在 happy-path 用例旁补充测试,沿用本文件 request(app) + Host/Bearer 的既有写法(示例代码见上)。

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

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Independent verification (maintainer, local real-stack A/B)

I verified this PR locally with a real daemon + Web Shell + browser stack on both the PR head (44f0e4c56d) and the merge-base (0e8a361b37), driven end to end. TL;DR: the fix works exactly as described, the new tests pin it, and boundary validation still holds. LGTM from my side.

Setup

  • Two isolated worktrees (PR head / merge-base), each fully bundled (npm run bundle) and started as qwen serve with its own isolated QWEN_HOME, workspace, and port, serving its own Web Shell UI.
  • A scripted OpenAI-compatible provider that assigns the function-call id agent:8 to a single agent tool call (reproducing providers that emit name:index-style ids), then lets the general-purpose subagent complete normally. This yields agent task id general-purpose-agent:8 on disk (confirmed in the subagent .meta.json), matching the PR description.
  • Real Chromium (Playwright) drives each stack: send prompt → subagent completes → reopen session → expand the turn → click the subagent row to open the detail view, while recording every network response.

Result 1 — real-stack before/after

merge-base (before) PR head (after)
GET /session/:id/subagents/agent%3A8 HTTP 500 ×3, {"error":"Virtual subagent session ids require valid id parts"} HTTP 200, resolves taskId "general-purpose-agent:8"
Virtual session subagent.<b64url>.<b64url> never created — detail panel stuck on "Loading agent details…" POST /load 200 · GET /context 200 · GET /events (SSE) 200
Detail panel stuck loading forever shows completed badge, task prompt, token usage, subagent transcript

before/after

Full-size screenshots: before (base) · after (head)

Result 2 — boundary probes on PR head (raw HTTP)

  • Oversized subagent ref (501 chars) → 400 invalid_subagent_ref (part-length cap still enforced, now shared via MAX_VIRTUAL_SESSION_ID_PART_LENGTH).
  • Unknown ref containing a colon (nope%3A99) → 404 (wider charset does not turn misses into errors).

Result 3 — tests are non-hollow

  • PR head: virtual-subagent-sessions.test.ts 22/22 pass; server.test.ts 923/923 pass.
  • Copying the PR's test file unchanged onto the merge-base: 7 of the new tests fail — colon/slash id round-trips, out-of-band colon task resolution, and (a nice extra hardening) rejection of non-canonical encodings, which the old parser silently accepted as alias session ids for the same target. The new encodePart(decoded) === part check closes that.

Code notes

  • The split validator keeps the strict charset for parent ids (they reach filesystem paths) while agent ids only need lossless UTF-8 Base64URL round-trip; the lone-surrogate rejection comment is accurate — without it two distinct agent ids could encode to the same session id.
  • The creation-side total-length cap (2,000) now matches the parse-side cap, so any id that can be created can also be parsed back.
  • Route/SDK renames to subagentRef are cosmetic and wire-compatible; telemetry route labels were updated consistently.

Environment: macOS (Darwin 25.6.0), Node 24. Windows/Linux not covered here — CI remains the source of truth there.

中文版本(Chinese version)

独立验证(维护者本地真实栈 A/B)

我在本地用真实 daemon + Web Shell + 浏览器栈对 PR head(44f0e4c56d)与 merge-base(0e8a361b37)做了端到端 A/B 验证。结论:修复与描述完全一致,新测试能钉住该缺陷,边界校验依然有效。我这边 LGTM。

环境搭建

  • 两棵隔离 worktree(PR head / merge-base),各自完整 npm run bundle,以独立的 QWEN_HOME、workspace、端口启动 qwen serve,各自服务自己的 Web Shell UI。
  • 一个脚本化的 OpenAI 兼容 provider,给唯一一次 agent 工具调用分配 function-call id agent:8(复现会产生 name:index 形式 id 的 provider),随后 general-purpose subagent 正常完成。落盘的 agent task id 为 general-purpose-agent:8(已在 subagent .meta.json 中确认),与 PR 描述一致。
  • 真实 Chromium(Playwright)驱动两套栈:发送 prompt → subagent 完成 → 重新进入会话 → 展开回合 → 点击 subagent 行打开详情视图,全程记录网络响应。

结果 1 — 真实栈前后对比

merge-base(修复前) PR head(修复后)
GET /session/:id/subagents/agent%3A8 HTTP 500 ×3,{"error":"Virtual subagent session ids require valid id parts"} HTTP 200,解析出 taskId "general-purpose-agent:8"
虚拟会话 subagent.<b64url>.<b64url> 从未建立——详情面板永远停在 "Loading agent details…" POST /load 200 · GET /context 200 · GET /events(SSE)200
详情面板 永久加载中 显示 completed 徽标、任务 prompt、token 用量、subagent 转录

结果 2 — PR head 边界探测(原始 HTTP)

  • 超长 subagent ref(501 字符)→ 400 invalid_subagent_ref(部件长度上限仍然生效)。
  • 含冒号的未知 ref(nope%3A99)→ 404(放宽字符集不会把未命中变成错误)。

结果 3 — 测试非空洞

  • PR head:virtual-subagent-sessions.test.ts 22/22 通过;server.test.ts 923/923 通过。
  • 把 PR 的测试文件原样拷到 merge-base 上运行:7 个新用例失败——冒号/斜杠 id 往返、按含冒号的 agent task id 解析,以及一项额外加固:拒绝非规范编码(旧解析器会把非规范编码静默接受为指向同一目标的别名会话 id,新增的 encodePart(decoded) === part 校验堵住了这一点)。

代码备注

  • 校验器拆分合理:父会话 id 保持严格字符集(会进入文件系统路径),agent id 只要求 UTF-8 Base64URL 无损往返;关于孤立代理项(lone surrogate)的注释准确——不拒绝的话,两个不同的 agent id 可能编码出同一个会话 id。
  • 创建侧 2,000 字符总长上限与解析侧一致,凡能创建的 id 必能解析回来。
  • 路由/SDK 改名为 subagentRef 属外观性修改,线协议兼容;telemetry 路由标签同步更新。

验证环境:macOS(Darwin 25.6.0)、Node 24。Windows/Linux 未覆盖,以 CI 为准。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action required

No actionable feedback arrived in this round, so no changes were made.

  • Reviews: none pending.
  • Inline comments: none pending.
  • Issue-level comments: the only new comment is @wenshao's independent real-stack A/B verification (real daemon + Web Shell + browser on both the PR head and the merge-base), confirming the fix works as described, the new tests pin it, and boundary validation still holds — LGTM. It raises no requests or defects; no action needed.
  • Failed / still-red checks: none. All required CI checks on the current head are passing.
  • Critical-only mode: this PR has completed five change-producing rounds. The deferred non-Critical items listed in the round input (one automated review and five inline suggestions) are intentionally left untouched — no code changes, no thread resolutions, no replies — and remain open for human follow-up per the workflow's ratchet rule.

Working tree and branch head are unchanged.

中文说明

Autofix 评审轮次:无需处理

本轮没有收到需要处理的反馈,因此未做任何改动。

  • 评审(Reviews): 无待处理项。
  • 行内评论(Inline comments): 无待处理项。
  • Issue 级评论: 唯一的新评论是 @wenshao 的独立真实栈 A/B 验证(在 PR head 与 merge-base 上分别用真实 daemon + Web Shell + 浏览器端到端驱动),确认修复与描述一致、新测试能钉住该缺陷、边界校验依然有效——LGTM。该评论没有提出任何要求或缺陷,无需处理。
  • 失败 / 持续失败的检查: 无。当前 head 上所有必需的 CI 检查均通过。
  • 仅 Critical 模式: 本 PR 已完成 5 个产生改动的轮次。本轮输入中列出的延后非 Critical 条目(1 条自动评审、5 条行内建议)按工作流的棘轮规则刻意保持原样——不改代码、不解决线程、不回复评论——继续留待人工跟进。

工作区与分支 head 均未变化。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge August 12, 2026 02:53

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Aug 12, 2026
Merged via the queue into QwenLM:main with commit 17449aa Aug 12, 2026
116 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

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

Scripted assertions: 112 passed · 0 failed · 112 total

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

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

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

Verification report

PR #8717 Deep Verification — fix(serve): support reserved characters in virtual subagent ids

Verdict: merge-ready — 112 scripted assertions executed, 112 passed, 0 failed.
Verified head: 44f0e4c56d707057a0d4083a23c5cb58f8dcb27c (merge commit 945c4b54d0, base tip a64d1291d2 = origin/main, so the merge is verified against current main).

中文摘要
  • 结论: merge-ready。112 条脚本化断言全部通过,0 失败。
  • A/B 结论: 核心主张成立且承重。base 上 createVirtualSubagentSessionId 对含 :// 的 agent task id(如 agent:8general-purpose-agent:8)抛出 "valid id parts",路由 GET /session/:id/subagents/agent%3A8 返回 500(daemon 日志原样捕获);head 上同一请求返回 200,生成的虚拟会话 id 可无损 parse 回原 agent id。单单元矩阵 34/34(head)、29/29(base 按 base 期望断言);真实 resolve(无 spy)的路由 E2E 两臂各 6/6。
  • 变异/非空洞: 三个新守卫(规范化编码校验、create 侧 2000 总长上限、孤立代理项往返校验)分别被精确变异杀死(3/1/1 个测试变红),head 测试在 base 源码上按预期红 7+2 个,正面对照( base 自身测试)全绿。
  • findings: 无阻塞项。3 条观察级说明,见 Findings(parse 侧对从未被签发过的非规范 id 收紧、server.test.ts 路由测试经 fixture 构造而非真实 resolve 产生区分度、极宽 UTF-8 的 task id 仍会以 500 形态出现但两臂一致)。
  • 未覆盖: 逐 commit 归因(浅克隆仅 3 个 commit 可达,已验证聚合 diff)、全仓测试套件、真实 daemon 进程启动(用 supertest 驱动真实 express app + 真实 VirtualSubagentSessions 复现线上形状)、Web Shell 浏览器渲染。

Scope selection

  • Central claim: agent task IDs containing reserved characters (agent:8, agent/8, general-purpose-agent:8) round-trip through virtual subagent session create/parse and the HTTP resolve/cancel routes (HTTP 200), where base rejected them (throw in resolve → HTTP 500).
  • Secondary claim 1: parent IDs stay under the strict charset; bounds (500 code units/part, 2000 total) enforced at both create and parse; lossy (lone-surrogate) and non-canonical encodings rejected.
  • Secondary claim 2: the toolCallIdsubagentRef rename (route param, error code invalid_subagent_ref, telemetry attribution, SDK parameter) is consistent with no stale consumers.

Central claim — A/B table

Environment per cell: base = scratch worktree at HEAD^1 wired to the root node_modules (lockfile untouched by PR; packages/core and packages/acp-bridge proven byte-identical between arms via git diff HEAD^1..HEAD --stat = empty; vitest aliases resolve @qwen-code/* to tree-local sources); head = the merge checkout. Harnesses: unit-ab.mts (imports the module per arm) and zz-verify-route-ab.test.ts (identical file in both trees, arm-specific expectations via VERIFY_ARM).

Cell Oracle base (HEAD^1) head (HEAD^2)
create agent:8 / agent/8 / general-purpose-agent:8 accept + parse round-trip throws valid id parts (3/3) accepted, round-trips (3/3)
create agent-1, 'a'.repeat(500) (len 696) — controls accept accepted accepted
create parent parent/session, parent:session, '', 501 units reject rejects rejects
create '界'.repeat(493) (total 2001) reject rejects via charset rejects via new exceeds 2000 characters cap
create lone surrogate \ud800, emoji a🚀b reject / accept both rejected surrogate rejected, emoji accepted
parse padded part …YWdlbnQtMQ==, skipped-char YWdl!bnQtMQ head rejects; base accepts (lenient decoder) accepted (p02–p04) rejected (canonicality check)
parse 2677-char id (both parts at part cap) reject rejects (>2000 pre-existing) rejects (>2000)
resolve() with task general-purpose-agent:8, ref agent:8 resolve / throw throws valid id parts resolves; sessionId parses back
Route E2E, real resolve (no spy): GET /session/s-1/subagents/agent%3A8, agent%2F8 HTTP status 500 Virtual subagent session ids require valid id parts (daemon log captured) 200, taskId + parseable sessionId
Route E2E POST …/agent%3A8/cancel HTTP status 500 200 {cancelled:true}
Route E2E plain tool-1 / unknown ref / 501-char ref control 200 / 404 / 400 invalid_tool_call_id 200 / 404 / 400 invalid_subagent_ref

Counts: unit matrix 34/34 head, 29/29 base; route E2E 6/6 per arm. Evidence: 01-unit-ab-head-reserved-ids-accepted.png, 02-unit-ab-base-reserved-ids-rejected.png, 03-route-e2e-head-200.png, 04-route-e2e-base-500.png (the base capture shows the daemon's own bridge error (GET /session/:id/subagents/:toolCallId …) log lines).

Vacuity & mutation matrix

Head's new tests run against base source (coarse mutant = the whole fix reverted), with positive controls:

Run Result
base's own virtual-subagent-sessions.test.ts on base (control) 11/11 green
base's own server.test.ts on base (control) 922/922 green
head's virtual-subagent-sessions.test.ts on base 7 red / 15 green — all reds fail the intended behavioral assertion (e.g. expected [Function] to throw error including 'exceeds 2000 characters' but got 'Virtual subagent session ids require …')
head's server.test.ts on base 2 red / 921 green — both fail in fixture construction (createVirtualSubagentSessionId('s-1', 'general-purpose-agent:8') at server.test.ts:8801 throws on base)
M1: delete canonicality comparisons in parse 3 red (non-canonical parent/agent encoding, garbage) — killed
M2: delete create-side total cap 1 red (exceeds 2000 characters assertion) — killed
M3: delete lone-surrogate round-trip guard 1 red (\ud800 assertion) — killed
unmutated head control 22/22 green

Every guard the PR introduces is pinned by a test that dies when the guard is deleted; no survivors among the new guards. Coarse-mutant survivors (15 + 921) are shared-behavior regression pins — base behaves identically on them (verified by the green base controls), classified as completeness, not merge conditions. Evidence: 05-mutation-head-tests-on-base-7-red.png; logs in logs/mutation-matrix.log, logs/mutant-head-tests-on-base.log.

Findings (none blocking)

  1. Nit — the description's "previously valid virtual session IDs remain valid" needs one qualifier. Measured cells p02–p04: base's parse accepted non-canonical spellings — padded parts (…YWdlbnQtMQ==) and parts containing characters Node's lenient base64 decoder silently skips (YWgl!bnQtMQ → decodes as agent-1) — because decodePart never validates and base had no canonicality check. Head rejects them. No legitimately minted ID is affected: the only producer is createVirtualSubagentSessionId (virtual-subagent-sessions.ts:958 is the sole production call site; grep-verified), and it emits unpadded canonical base64url. The tightening is a hardening — under base, two different spellings parsed to the same session key — but strictly read, the compatibility sentence covers only system-minted IDs. Label: description clarification, not a code change request.
  2. Note — the route tests' discriminating power comes from the fixture, not the route. server.test.ts mocks VirtualSubagentSessions.prototype.resolve, so on base the route would have returned 200; the tests fail on base only because the mocked return value is built by calling the real createVirtualSubagentSessionId with a reserved-char taskId (throws at line 8801, before any request). Route wiring (URL decode of %3A/%2F, guard, params) and real-resolve composition are pinned separately — by these same tests' resolveSpy call args and by the unit resolve() tests — and end-to-end (route + real resolve) was confirmed by this round's E2E harness (500→200 flip). No action needed; recorded so a reviewer does not assume the route test alone exercises the validator.
  3. Observation — one exotic input class still throws through resolve on both arms. A task id at the part cap with wide UTF-8 (e.g. '界'.repeat(493), 493 code units ≤ 500 but 2001 chars encoded) makes create throw inside resolve, surfacing as a generic 500 via sendBridgeError (fallback at error-response.ts:817). Base behaves the same way for this input (rejects it at the charset check, also 500) — no regression, and the class is unrealistic for provider/local task IDs. A dedicated 400 mapping is conceivable but out of this PR's scope.

Additional checks, all clean:

  • Scaling ladder (tool-call ids are provider-authored = untrusted writer): 6 parse rungs to 40k chars and 4 create rungs at the 500-code-unit cap — all ≤ 0.013 ms/call, flat curve, no rung near the 30 s cap (logs/ladder-head.log). Length checks precede the single anchored charset regex; no backtracking hazard.
  • Rename completeness: zero remaining invalid_tool_call_id / subagents/:toolCallId consumers in packages or docs; routes, telemetry table, and SDK route strings agree on GET/POST /session/:id/subagents/:subagentRef; Web Shell calls the SDK positionally (SubagentDetail.tsx:309, useMessages.ts:202) — rename-safe; SDK urlEncode = encodeURIComponent percent-encodes : and /, and the SDK wire tests pin …/subagents/agent%2F1 (329/329 green).
  • Parse-side dispatch consumers (routes/session.ts:2029/2969/3031/3578/3620, sse-events.ts:333) treat undefined as "not virtual" → the canonicality tightening degrades to the normal-session path (404), never a new 500.
  • Parent-vs-agent asymmetry is sound: only parent ids reach filesystem paths (chats/${parentSessionId}.jsonl, getSubagentSessionDir); agent ids are comparison keys (candidate.id === / endsWith), and the composed session id is pure base64url alphabet — path-safe in both parts.

Not covered

  • Per-commit attribution: checkout is depth 2 (merge commit + two parents only); the metadata lists 18 commits but git rev-list HEAD^1..HEAD^2 returns 1 at the shallow boundary, so the aggregate HEAD^1..HEAD diff was verified and per-commit behavior was out of reach.
  • Repo-wide test suite and integration-tests/ — targeted workspaces only (cli serve files + sdk DaemonClient + telemetry).
  • A live qwen serve daemon process (real bridge/channel bring-up): route behavior was exercised through supertest against the real express app with the real VirtualSubagentSessions and a fake bridge at the getSessionTasksStatus/cancelSessionTask seam — this reproduces the wire shape of the reported failure (HTTP 500/200 semantics and the daemon log line), not the provider-side trigger that mints agent:8-style ids.
  • Web Shell UI rendering of the detail view in a browser (client→daemon contract covered via the SDK wire tests instead).
  • Windows/macOS specifics — the changed code is platform-neutral string handling; this round ran on Linux (the CI lane's own container image class).

Methodology

Ran in the CI verify container on node:22-bookworm, working tree at refs/pull/8717/merge. Base control: git worktree add tmp/base-tree HEAD^1 wired to the already-installed root node_modules plus per-package node_modules symlinks (PR touches no package.json/lockfile — verified empty diff); readlink -f node_modules/@qwen-code/qwen-code-core from the base tree resolves into the head tree, which is a clean control here because git diff HEAD^1..HEAD over packages/core, packages/acp-bridge, packages/cli/vitest.config.ts, and test-setup.ts is empty, and vitest's aliases additionally re-point @qwen-code/* resolution to tree-local sources; src/generated/git-commit.ts (build artifact, gitignored) was copied from head — version metadata only. The unit harness (unit-ab.mts) imports each arm's virtual-subagent-sessions.ts directly under tsx; the route harness (zz-verify-route-ab.test.ts, identical in both trees) drives createServeApp with a Proxy bridge whose only live seams are getSessionTasksStatus/cancelSessionTask, under supertest. Mutations were applied to a scratch copy and restored (git status clean after each). Gates: head runs of both affected cli test files (945/945, captured in 06-gate-head-945-green.png), telemetry tests (51/51), SDK DaemonClient.test.ts (329/329), npm run typecheck across all workspaces (exit 0), ESLint on all six changed files (clean; live-gate proof: a planted any/unused-var violation was detected, then removed). Raw logs in logs/; harness scripts in this directory.

Evidence images

01-unit-ab-head-reserved-ids-accepted

02-unit-ab-base-reserved-ids-rejected

03-route-e2e-head-200

04-route-e2e-base-500

05-mutation-head-tests-on-base-7-red

06-gate-head-945-green

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

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.11.

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

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants