Skip to content

feat(serve): support model and reasoning selection at session creation - #12308

Merged
callmeYe merged 13 commits into
mainfrom
codex/session-startup-config
Sep 25, 2026
Merged

callmeYe merged 13 commits into
mainfrom
codex/session-startup-config

Conversation

@callmeYe

@callmeYe callmeYe commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

External callers can create ordinary and standalone sessions with startupConfig: { modelServiceId, reasoningEffort }. modelServiceId is required and reasoningEffort is optional. When omitted, creation selects only the model without calling the reasoning setter or substituting default; this supports models without reasoning controls. Creation confirms the requested selection before returning or admitting an initial prompt, using composer behavior without saving shared defaults. The TypeScript SDK supports REST and daemon ACP HTTP/WS, checks the daemon capability, and rejects missing or inconsistent confirmation.

Definite standalone startup rejection now removes its owned recording so the caller can retry a corrected request with the same ID. Timeouts and channel failures retain the existing outcome-unknown recovery. SDK confirmation mismatch after a readable successful response fails directly, without recovery/adoption.

Why it's needed

A successful legacy create does not jointly confirm model and reasoning selection; an old daemon can silently ignore the new input. Callers should be able to request a model with high, default, or none and know that startup preparation succeeded before sending work. This is the narrowed PR 4 from #11944: subsequent prompts, manual changes, reload, fallback, skills, and restoration retain ordinary session behavior.

Reviewer Test Plan

How to verify

  • Create an ordinary session with an explicit model and high; expect modelApplied: true and a canonical startupConfigApplied acknowledgment. The first model request and requests after tool results should use that pair, and a later turn should retain the selection.
  • First try model-only creation on a model without reasoning controls: expect only the canonical model in startupConfigApplied, without reasoning fields. An explicit unsupported reasoning selection must still fail. Repeat with default and none, including a saved shared preference of none overridden by requested high. Compare the effective provider request with composer-equivalent model/reasoning selection.
  • Repeat for standalone sessions and SDK REST, ACP HTTP, and ACP WS. Standalone must finish deferred activation before selection, then release and admit any initial prompt only after preparation succeeds.
  • Verify startup success and failure leave user/workspace settings unchanged, emit no default-value change event, preserve healthy siblings, and use the selected workspace. An unsupported selection must fail without admitting an initial prompt.
  • Verify malformed input, mixed legacy selectors, and explicit single scope fail; an old daemon is rejected by SDK capability preflight before a mutation. Later manual changes remain available with their existing behavior.

Evidence (Before & After)

Before: globally installed CLI 0.18.5-preview.0 lacks session_startup_config and silently ignores the object. The candidate SDK rejects that daemon with zero POST mutations.

After: build, typecheck, bundle, changed-file ESLint, and 972 focused unit/integration tests passed. Controlled-provider E2E captured 24 requests and passed model/reasoning confirmation, tool continuation, later turns, settings and sibling isolation, all three SDK transports, secondary-workspace ownership, composer-equivalent API/wire comparison, and manual changes. Two complete diff self-audits and a local medium review found no remaining actionable issues. A separate E2E report accompanies this PR.

Tested on

OS Status
macOS ✅ Tested locally
Windows ⚠️ Not tested locally
Linux ⚠️ Not tested locally

Environment

Node.js 22, local built daemon and SDK, isolated settings/runtime/workspaces, and a controlled local OpenAI-compatible provider. No browser UI automation or real external-provider calls were performed.

Risk & Scope

  • The main change is creation preparation ordering and failure cleanup. Existing standalone commit/release and owned-session cleanup remain authoritative.
  • No lifetime pin, persistent policy record, recording migration, strict restore protocol, fallback restriction, UI change, direct-child ACP extension, or new tool parameter. Existing requests without startupConfig keep legacy behavior.
  • Fallback, skill overrides, reload, cold restoration, and transport-disconnect behavior were not exhaustively exercised in E2E. Startup follows current UI ordering and does not promise selection before initial authentication.
  • Complete synchronized designs: English · 简体中文.

Linked Issues

Refs #11944. This implements independent PR 4; it does not close the four-PR tracker.

中文说明

本 PR 的改动

外部调用方可以用 startupConfig: { modelServiceId, reasoningEffort } 创建普通和 standalone 会话。modelServiceId 必填,reasoningEffort 可选。省略时只选择模型,不调用思考 setter,也不自动补 default,支持没有思考控件的模型。创建返回或准入首条 prompt 前确认请求的选择,行为与输入框一致,但不保存共享默认值。TypeScript SDK 支持 REST 和 daemon ACP HTTP/WS,预检 daemon 能力,并拒绝缺失或不一致的确认。

确定的 standalone 启动拒绝现在会删除本次持久记录,使调用方可以用同一 ID 修正后重试。超时和断连保留原有结果不确定恢复语义。SDK 收到可读成功响应但确认不匹配时直接报错,不触发恢复或采用该会话。

为什么需要

旧的创建成功不代表模型和思考强度共同确认生效;旧 daemon 还可能静默忽略新参数。调用方应能指定模型及 high、default 或 none,在发送工作前确认启动准备成功。这是 #11944 收敛后的 PR 4:后续提问、人工修改、reload、fallback、skill 和恢复保持普通会话行为。

评审测试计划

如何验证

  • 指定模型和 high 创建普通会话,确认返回 modelApplied: true 及规范的 startupConfigApplied。首个模型请求及工具结果之后的请求使用该组合,后续轮次保留选择。
  • 先对没有思考控件的模型测试仅指定模型创建:startupConfigApplied 只含规范模型选择符、不含思考字段;显式传入不支持的思考选择仍应失败。重复测试 default、none,以及共享设置为 none、请求为 high 的情况;将实际 provider 请求与输入框等效的模型/思考选择流程比较。
  • 重复验证 standalone 和 SDK REST、ACP HTTP、ACP WS。Standalone 应先完成延迟激活,再设置配置;准备成功后才 release 并准入初始 prompt。
  • 启动成功与失败都不修改用户/工作区设置,不发送默认值变更事件,保留健康兄弟会话并使用选定 workspace。不支持的选择失败时不得准入首条 prompt。
  • 非法输入、混用旧选择符、显式 single scope 应失败;SDK 通过能力预检在 mutation 前拒绝旧 daemon。之后人工修改仍可用,行为沿用现状。

验证证据(改动前后)

改动前:全局 CLI 0.18.5-preview.0 不支持 session_startup_config,会静默忽略该对象。候选 SDK 对该 daemon 执行零 POST mutation 并返回能力缺失。

改动后:build、typecheck、bundle、变更文件 ESLint 和 972 项聚焦单元/集成测试通过。受控 provider E2E 捕获 24 个请求,覆盖模型/思考确认、工具后续请求、后续轮次、设置和兄弟会话隔离、三种 SDK transport、第二个 workspace 归属、输入框等效 API/请求比较及人工修改。两轮完整 diff 自审和本地 medium review 未发现剩余可操作问题。本 PR 另附独立 E2E 报告。

测试平台

系统 状态
macOS ✅ 本地已测试
Windows ⚠️ 本地未测试
Linux ⚠️ 本地未测试

环境

Node.js 22、本地构建的 daemon 和 SDK、隔离设置/runtime/workspace,以及本地受控 OpenAI-compatible provider。未进行浏览器 UI 自动化或真实外部 provider 调用。

风险与范围

  • 主要改动是创建准备顺序及失败清理;沿用既有 standalone commit/release 和本次会话清理机制。
  • 不新增生命周期固定、持久策略记录、recording 迁移、严格恢复协议、fallback 限制、UI 改动、直接 child ACP 扩展或工具参数。不带 startupConfig 的请求保持 legacy 行为。
  • E2E 未穷尽 fallback、skill 覆盖、reload、冷恢复及传输断连。启动沿用当前 UI 顺序,不承诺在初始认证前选择模型。
  • 完整同步设计:English · 简体中文。

关联 Issue

Refs #11944。本次实现独立 PR 4,不关闭包含四个 PR 的跟踪 issue。

@callmeYe

Copy link
Copy Markdown
Collaborator Author

E2E verification report

Result: final candidate bundle passed controlled-provider E2E (exit 0), with 15 captured provider requests. This is an API-sequence and wire verification, not browser UI automation or a real external-provider test.

Scenario Evidence
Ordinary startup Canonical acknowledgment matches gpt-5.5/high; first request and tool continuation use high.
Default / off Default resolves to configured low; none sends disabled reasoning.
Shared none → high Explicit startup high overrides saved none for the new session only.
Later behavior Second turn retains the selection; subsequent manual switch to gpt-5.4/low works.
Isolation User/workspace settings unchanged before deliberate manual persistence; no default-change event; healthy sibling stays gpt-5.4/none.
Standalone Startup confirmation and provider request match; invalid model fails with zero provider requests.
SDK transports Built SDK REST, ACP HTTP and ACP WS each confirm and execute the requested pair.
Workspace ownership Explicit secondary workspace appears in both creation response and provider context.
Compatibility Built SDK rejects old CLI 0.18.5-preview.0 for missing capability with zero POST mutations.
Failure ordering Separate service tests prove commit → model → reasoning → release → initial prompt, and zero release/prompt after preparation failure; cleanup failure preserves uncertain-outcome containment.

Every main-wire comparison requires a nonempty capture. Fixtures isolate settings/runtime/workspaces and the standalone output root. Auxiliary suggestion/memory traffic is excluded from main-turn comparisons.

Build, typecheck, bundle and changed-file lint passed. Focused tests: bridge 13, SDK 628, standalone 136, REST/ACP creation and scope 22 — total 799 passed. The REST/ACP run filtered out 1663 unrelated tests; it was not a full-suite run.

No browser UI, real external provider, Windows or Linux execution is claimed. Fallback, skill overrides, reload, cold restoration and disconnect paths were not exhaustively validated in E2E. CI and merge status are separate from these local results.

中文

最终候选 bundle 的受控 provider E2E 退出码为 0,共捕获 15 个请求,覆盖普通/standalone 启动确认、high/default/none、工具后续请求、后续轮次、设置及兄弟会话隔离、SDK REST/ACP HTTP/ACP WS、第二个 workspace、输入框等效 API 请求比较和之后人工修改。旧 CLI 0.18.5-preview.0 的能力预检产生零 POST mutation。

所有主请求比较都要求非空捕获。Fixture 隔离设置、runtime、workspace 和 standalone 输出目录;辅助建议与记忆请求不计作主轮请求。Standalone 内部 commit → 模型 → 思考 → release → 首条 prompt 顺序,以及准备失败时不 release/不准入、清理失败保持结果不确定,由独立 service 单测验证。

build、typecheck、bundle、变更文件 lint 通过。聚焦测试:bridge 13、SDK 628、standalone 136、REST/ACP 创建和 scope 22,总计 799 通过。REST/ACP 测试过滤了 1663 项无关用例,并非运行全部测试。

未测试浏览器 UI、真实外部 provider、Windows 或 Linux。E2E 未穷尽 fallback、skill 覆盖、reload、冷恢复和断连路径。本地验证与 CI、合并状态分别报告。

@callmeYe

Copy link
Copy Markdown
Collaborator Author

Follow-up: optional reasoning selection

startupConfig.modelServiceId remains required; reasoningEffort is now optional. Omission applies only the model, skips the reasoning setter, and does not substitute default. The acknowledgment then contains only modelServiceId. An explicit reasoning value still uses the existing model-specific validation and fails when unsupported.

Validation passed: build, full typecheck, bundle, changed-file lint, 804 focused tests, and the capability-registry unit/integration cases that failed on the prior commit. That CI failure was expected-capability lists missing session_startup_config; the expectations now include the advertised tag.

Updated controlled-provider E2E passed with 24 captured requests. Non-reasoning gpt-4.1 succeeds in ordinary/standalone model-only creation without a reasoning wire field; reasoning-capable gpt-5.5 with omitted effort keeps the existing shared none preference instead of forcing model default low. Explicit high on the non-reasoning model fails with zero provider calls. All prior explicit-selection, tool-loop, settings/sibling isolation, later-turn, secondary-workspace and manual-change checks remain passing.

Ordinary SDK model-only creation and prompts were tested through REST, ACP HTTP and ACP WS. Standalone creation remains REST-backed even on clients configured for ACP; prompts use the owning REST path. The existing generic ACP-to-standalone prompt ownership mismatch was also reproduced without startupConfig and is outside this change. No browser UI automation or real external provider is claimed.

中文

startupConfig.modelServiceId 必填,reasoningEffort 现在可选。省略时只应用模型、跳过思考 setter、不自动补 default,确认只包含 modelServiceId。显式思考强度仍按具体模型能力校验,不支持则失败。

build、完整 typecheck、bundle、变更文件 lint、804 项聚焦测试和此前失败的能力列表单元/集成用例通过。旧 CI 失败是预期能力列表漏加 session_startup_config,现已补齐。

新受控 provider E2E 捕获 24 个请求:非思考模型 gpt-4.1 可仅指定模型创建普通/standalone 会话,请求不含思考字段;gpt-5.5 省略档位时沿用现有 none 偏好,不强制变为模型默认 low。对非思考模型显式传 high 仍失败且零 provider 调用。此前显式选择、工具循环、设置/兄弟隔离、后续轮次、第二个 workspace、人工修改等回归仍通过。

普通 SDK model-only 创建和提问覆盖 REST、ACP HTTP、ACP WS。Standalone 创建在 ACP 配置下仍使用既有 REST 路径,提问使用其所属 REST 路径;通用 ACP 向 standalone 提问的既有归属问题在不带 startupConfig 时同样复现,不在本次修改范围。未测试浏览器 UI 或真实外部 provider。

@callmeYe

Copy link
Copy Markdown
Collaborator Author

CR round 1 follow-up verification

All sixteen findings have corresponding fixes or committed regression coverage. The critical standalone rollback was reproduced before fixing it: the failed UUID remained listed/loadable and could not be retried. The final real-child probes now verify422, absence of the owned JSONL/catalog entry, GET404, and successful same-UUID retry/load. Real child exit remains a coded recoverable outcome-unknown result.

The new real-daemon tests also caught two error-boundary mistakes that mocks missed: JSON-RPC rejects decoded error objects rather than RequestError instances, and split bundles contain distinct constructors for the same startup error class. Classification now uses only the narrow stable code/message contracts; auth and transport errors are not blanket-wrapped.

Validation: build, complete typecheck, bundle and changed-file lint passed. Bridge28 + SDK645 + CLI service/docs/error mappings228 + focused startup/scope/registry routes30 + full real-daemon route integration41 = 972 passed. The full committed route integration file includes model-only confirmation/current model cache, shared none→high, ordinary rejected-selection live teardown, and standalone durable rollback/retry. Five isolated verification runs passed, including the existing 24-request controlled-provider E2E.

The full CLI/provider-key integration suite was not run; the committed real-daemon route tests belong to the existing no-AK CI suite. Browser UI and real external-provider execution remain untested. Ordinary failed-creation persisted-history/UUID semantics remain unchanged; the durable rollback/retry guarantee here is explicitly standalone scoped.

中文

16 条发现均已有修复或已提交的回归覆盖。Critical 在修复前已复现:失败 UUID 仍可列出和加载且不能重试。最终真实子进程探针验证422、删除本次 JSONL/目录索引记录、GET404,并能用同 UUID 成功重试和加载;真实子进程退出仍返回可恢复的结果不确定错误。

新增真实 daemon 测试还捕获到 mock 未暴露的两个边界:RPC reject 的是解码后的普通错误对象,且拆分 bundle 内存在同一错误类的不同构造器。现在仅按明确的稳定错误码/消息契约分类,不把认证和传输错误统一包装成配置拒绝。

build、完整 typecheck、bundle、变更文件 lint 通过。Bridge28 + SDK645 + CLI service/文档/错误映射228 + 启动/scope/registry 路由30 + 完整真实 daemon 路由集成41 = 972 通过。5 组隔离验证通过,包含既有 24 次请求的受控 provider E2E。

未运行完整需要 provider 凭据的 CLI 集成套件;已提交的真实 daemon 路由测试属于现有 no-AK CI。未测试浏览器 UI 或真实外部 provider。普通失败创建的持久历史/UUID 语义保持原状;本次持久回滚和同 ID 重试保证明确限于 standalone。

@callmeYe
callmeYe dismissed a stale review September 20, 2026 16:30

Superseded by f497b74. All 16 R1 findings have code/test/documentation fixes and per-thread replies; all threads are resolved. The blocking standalone rollback was reproduced and then verified with real child probes (record removed, GET404, same UUID retry/load succeeds), while transient failures retain outcome-unknown recovery. Build/typecheck/lint, 972 focused/unit/integration tests and the controlled-provider matrix passed. This dismisses the stale review of 4c9467a; it is not an approval of the new head and does not bypass its required checks/review. Evidence: #12308 (comment)

@wenshao

wenshao commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover from 4

@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 Sep 22, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This window's round counter starts at 4 (the rounds this PR spent in review before takeover), so the Critical-only brake engages after 1 more change-producing round(s) instead of a full fresh 5. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本窗口轮次计数从 4 起算(即本 PR 托管前已进行的评审轮数),因此再经过 1 个产生改动的轮次即进入 Critical-only,而非重新计满 5 轮。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

✅ AutoFix round 3 finished — view run. See this round's report below.

中文说明

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

@wenshao

wenshao commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Deep verification: PR #12308 — startupConfig model/reasoning selection at session creation

Verdict: merge-ready — 2615/2615 executed scripted assertions passed, 0 unexpected failures.
Verified head: f497b740aabb1a42b9f8fff732fa16c4675d53fb (matches the PR's current head). Base: 39b22efb6fda0cec0ed0b295f1effbc78af5e61e (== merge-base).
Environment: Linux aarch64 (Orange Pi 5), Node v24.13.0 — the PR's "Tested on" table lists Linux as untested; this round fills that gap with an independent, maintainer-side run.

中文摘要

结论:可以合并(merge-ready)——2615/2615 条实际执行的脚本化断言全部通过,无意外失败。

  • A/B 核心结论:同一 fixture 下,PR 构建(head)创建带 startupConfig: {modelServiceId, reasoningEffort: 'high'} 的会话时返回 modelApplied: true 和规范的 startupConfigApplied,且 provider 真实收到的请求确实带 model=qwen-startup-test + reasoning_effort=high(第二轮仍保持);base 构建则静默忽略 startupConfig(返回无确认字段、会话落在默认模型上)——正是 PR 描述要消除的隐患。对照表见 "Central claim + A/B"。
  • SDK 能力预检:head 构建的 SDK 连接旧 daemon 时,在发出任何 POST mutation 之前以 session_startup_config is missing 拒绝(日志代理证实 0 次 POST);同一 SDK 经同一代理连 head daemon 正常创建(正向控制)。
  • standalone 回滚:不受支持的思考档位使 standalone 创建返回 422 且持久记录被删除(GET 404),同一 ID 修正后可重试成功。
  • composer 等效性:startupConfig 路径与"旧版创建 + composer setter"路径产生逐请求一致的 provider wire 签名(turn:high | memory:high | turn:high | memory:high | suggestion:none)。
  • vacuity 变异检验:删掉 REST 路由里那一行 startupConfig 传递后,10 条核心断言精确变红(且 S5 malformed 拒绝、S6 standalone 不受影响),证明 harness 是有承载力的;变异后文件已逐字节恢复。
  • gates:acp-bridge 28 + sdk-typescript 622 + cli serve 1914 + 新增集成测试 3 全绿(真实 daemon + bundle,Linux aarch64)。
  • 观察项(非缺陷):后续建议(suggestion-mode)调用始终使用共享默认思考强度(本 fixture 中为 none),composer 路径表现完全相同——属既有行为。base 上 POST /session 静默忽略未知字段而 POST /standalone/sessions 返回 400,属既有不对称。CI 的 review-pr 自动评审 lane 跑满 6 小时超时,不是代码失败。
  • 未覆盖:ACP HTTP/WS transport 的端到端驱动(由 CI serve A/B、作者 E2E 与相关单测覆盖)、真实外部 provider、Windows/macOS、fallback/reload/断连边界、逐 commit 归因。

Central claim + A/B

Claim: an external caller can create ordinary and standalone sessions with startupConfig: {modelServiceId, reasoningEffort?}; creation applies and confirms the selection before returning, per-session only, without persisting shared defaults — and an old daemon silently ignores the object (the hazard the SDK capability preflight exists for).

Identical scenario driven against both builds: real qwen serve daemon (per-tree packages/cli/dist), real loopback fake OpenAI provider capturing every request, isolated $HOME/workspace, settings fixture with a reasoning-capable model (efforts: [low, high], disableField: reasoning_effort), a no-reasoning model, and shared preference reasoningEffort: 'none'.

Cell Oracle base 39b22efb head f497b74
GET /capabilities session_startup_config feature absent (as predicted) present
POST /session + startupConfig {model, high} response confirmation 200, no modelApplied/startupConfigApplied (silent ignore) 200, modelApplied: true, canonical startupConfigApplied incl. effectiveReasoning {enabled, high}
Session context after create model / reasoning_effort currentValue plain-default(openai) / none qwen-startup-test(openai) / high
Provider wire, turn 1 model, reasoning_effort plain-default, absent qwen-startup-test, high
Provider wire, turn 2 (retention) same n/a (base never applied) qwen-startup-test, high
Shared settings.json bytes before/after unchanged unchanged
Sibling session (no startupConfig) context n/a default model + shared none, first session unaffected
Model-only create on no-reasoning model startupConfigApplied shape silently ignored 200, only modelServiceId, no reasoning keys; wire has no reasoning_effort
Reasoning on no-reasoning model / tier max outside efforts status + code n/a 422 startup_config_rejected ×2; rejected caller-supplied ID admits nothing (GET 404)
Malformed matrix (6 shapes: unknown field, legacy modelServiceId mixed, sessionScope:'single', missing/invalid/oversized values) status + code n/a 400 invalid_startup_config ×6
Standalone {reasoningEffort:'max'} → same-id retry 'high' rollback semantics 400 invalid_request (exact-body route rejects the unknown key) 422 → GET 404 (recording removed) → retry 200 with confirmation

Witness: 01-ab-head-vs-base.png (27/27 head, 9/9 base control, both green under per-arm encoded expectations).

Secondary claim: SDK capability preflight — zero mutations against old daemons

Head-built @qwen-code/sdk DaemonClient pointed at the base daemon through a logging loopback proxy (Host header rewritten, forwarding verified by positive control):

  • createOrAttachSession({startupConfig}) → throws DaemonCapabilities.session_startup_config is missing; proxy counted 0 POST /session (total 1 request: the capabilities GET).
  • createStandaloneSession({startupConfig}) → same rejection; 0 POST /standalone/sessions.
  • Positive control: same SDK through an identical proxy against the head daemon — legacy create succeeds (proxy saw the POST), model-only startupConfig succeeds with startupConfigApplied {modelServiceId} (proxy saw exactly 1 POST each).

Witness: 02-sdk-preflight-zero-mutation.png (7/7).

Secondary claim: composer-equivalence on the wire

Same two-turn scenario run twice on one head daemon: (A) startupConfig creation; (B) legacy modelServiceId creation + setSessionConfigOption('reasoning_effort', 'high') (the REST-exposed composer setter). Sorted per-request wire signatures (category:model:effort) are identical:

A(startup)  : turn:qwen-startup-test:high | memory:high | turn:high | memory:high | suggestion:none
B(composer) : turn:qwen-startup-test:high | memory:high | turn:high | memory:high | suggestion:none

Witness: 05-composer-equivalence.png (4/4). This is also what cleared up an artefact in my own first harness revision: the next-prompt suggestion-mode call embeds full conversation history and runs at the shared default effort (none here), so a naive "last request containing my probe text" oracle intermittently misattributed it as turn 2. With the oracle anchored on the last user message, the head arm is stable at 27/27 across 3 consecutive runs; the suggestion call's none is identical on the composer path, i.e. pre-existing ancillary behavior, not something this PR introduces.

Vacuity / mutation check

Reverting exactly one line in the built head (...(startupConfig ? { startupConfig } : {}) in dist/src/serve/routes/session.js) and re-running the full head suite flipped precisely the 10 central-claim assertions red (confirmation fields, context, both wire checks, retention, model-only applied shape, both 422s → 200) while the parse-layer (S5 malformed ×6) and standalone-path (S6, its own hunk) checks stayed green — no import/compile breakage, failures are the behavioral mismatches the harness exists to catch. The file was restored byte-identical (cmp verified, RESTORED-OK).

Witness: 03-vacuity-mutation.png.

Gates (PR head, Linux aarch64)

Gate Result
packages/acp-bridge session-startup-config.test.ts 28/28
packages/sdk-typescript DaemonClient + DaemonClientStandalone + acpRouteTable 622/622
packages/cli 7 serve-surface files (routes/standalone, acp-http dispatch-error + transport, server, error-response, docs-contract, standalone-session-service) 1914/1914
integration-tests qwen-serve-routes.test.ts -t startup (the PR's 3 new tests, real daemon + real bundle) 3/3

Witness: 04-gates.png. The integration gate needed npm run bundle in the worktree (its globalSetup pins TEST_CLI_PATH to dist/cli.js); noted for anyone reproducing.

Merge-drift check

Base is 70 commits behind current main (7837c6c200), and main has touched several PR-touched files since. git merge-tree 39b22efb main f497b74 produces zero conflict markers; GitHub reports MERGEABLE. All PR CI lanes that run on the merge ref (Test, Lint & Static, Integration, Serve A/B) are green at this head, so the merged tree is covered; my local A/B deliberately compared the pure head against the pure base.

Findings

None blocking. Two informational observations:

  1. Suggestion-mode calls ignore per-session reasoning selection (they run at the shared default effort) — identical on the composer path, so pre-existing and out of scope; worth knowing only because it can confuse wire-level verification (see above).
  2. CI review-pr lane failed by timeout (ran to its 6h ceiling mid-review, log ends inside the automated review loop). Not a code failure, but the automated review verdict for this head does not exist; this round plus CI's other lanes are the available evidence.

Not covered

  • ACP HTTP/WS transport end-to-end (session/new with startupConfig over those transports) — covered by CI's serve A/B, the author's controlled-provider E2E (all three SDK transports), and the passing dispatch-error/transport/acpRouteTable unit tests; my harnesses drove REST + SDK.
  • Real external providers (all traffic here is a controlled loopback fake); Windows/macOS; fallback, reload, skill override, cold-restoration and transport-disconnect edges (the PR itself lists these as not exhaustively exercised).
  • Per-commit attribution (verified the aggregate diff; the PR carries 4 commits).
  • Direct child ACP and create_sub_session (explicitly out of the PR's scope).

Methodology

Two throwaway git worktrees at the exact base/head OIDs, each with the repo node_modules hardlink-copied in (cp -al); the internal @qwen-code/* workspace symlinks resolve relative, and I asserted readlink -f node_modules/@qwen-code/qwen-code-core points into each own tree before trusting the control. The PR touches no dependency manifests (only an exports addition in packages/acp-bridge/package.json), so the shared install is a clean control. Both trees built with npm run build (exit 0) plus npm run bundle on head. Harnesses are self-contained .mjs scripts driving the real daemons over real loopback sockets; every PASS/FAIL line is a scripted comparison. Head arm was re-run 3× after the oracle fix (27/27 each). Assertion accounting: distinct scripted checks counted once — A/B harness 27 (head) + 9 (base control with encoded expected behavior) + SDK preflight 7 + composer-equivalence 4 + 1 adjudicated mutation check + 2567 vitest tests = 2615 pass, 0 fail. Raw per-run JSON (results-*.json, wire-*.json), harness sources, and build logs live in tmp/pr12308-verify-20260922-222800/ on the verifying machine; images were captured live with scripts/verify-capture.mjs.

Evidence

  • 01-ab-head-vs-base.png — full A/B run, both arms
  • 02-sdk-preflight-zero-mutation.png — SDK preflight vs old daemon + positive control
  • 03-vacuity-mutation.png — mutation flips central assertions red, then RESTORED-OK
  • 04-gates.png — focused unit + integration gates on PR head
  • 05-composer-equivalence.png — identical wire signatures for startup vs composer creation

01-ab-head-vs-base.png

02-sdk-preflight-zero-mutation.png

03-vacuity-mutation.png

04-gates.png

05-composer-equivalence.png

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (3600000ms)) (attempt 5/100) — it will retry on the next scan.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:
Qwen failed during address-review: timeout (3600000ms). [API Error: 429 status code (no body)]

See the Qwen Autofix agent step logs for model/tool output.

中文说明

🤖 AutoFix 在完成前耗尽了时间(timeout (3600000ms))(第 5/100 次尝试)—— 将在下次扫描时重试。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/35739497141


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.3

- Map the ACP internalError wire shape ({code: -32603, data.details})
  produced when switchModel refuses an unregistered model to
  startup_config_rejected on the startup-config call path, so the
  ordinary route answers 422 instead of 500 and the standalone path
  takes its definite rollback branch.
- Roll back the persisted recording of a caller-supplied sessionId on
  POST /session after a definite startup rejection, so a retry of the
  same id is no longer answered with 409 session_id_conflict; uncertain
  failures keep the recording.
- Publish auto in the OpenAPI approvalMode enum and pin it against
  APPROVAL_MODES; narrow the 422/protocol/design cleanup wording to
  match the implemented best-effort semantics; restore the sessionId in
  the new rollback warning.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round summary — PR #12308

Critical-only brake is active (prior rounds exhausted), so this round implements the four Critical findings plus three adjacent, low-cost Suggestions the Critical findings themselves couple in, and defers the remaining Suggestions to the next round via inline replies. The deferred non-Critical audit section was left untouched per the brake rules.

Addressed

  1. rc:4058288896 (Critical, OpenAPI approvalMode omits auto) — Added auto to the CreateStandaloneSessionRequest.approvalMode enum in docs/developers/daemon-rest-api.openapi.json (now ["plan","default","auto-edit","auto","yolo"], matching APPROVAL_MODES order), and extended rest-integration-docs-contract.test.ts to assert the published enum equals [...APPROVAL_MODES] so a narrower list can never ship again.
  2. rc:4058288898 (Critical, protocol contract over-promises cleanup outcome) — docs/developers/qwen-serve-protocol.md: narrowed the sentence to "a recording rollback whose removal cannot be confirmed and transient startup failures retain standalone_creation_outcome_unknown, while a failed directory discard is only logged and the definite rejection is still returned" — exactly what standalone-session-service.ts:2806-2822 implements.
  3. rc:4058288900 (Critical, caller-supplied sessionId burned by leftover recording) — packages/cli/src/serve/routes/session.ts: on a definite startup_config_rejected from POST /session with a caller-supplied id, the catch now runs deleteDaemonSessionIfOrphan (best-effort, with a daemonLog warn on failure) so the recording the spawn persisted is rolled back and the id stays retryable; uncertain outcomes still keep the recording. New regression tests in server.test.ts: the definite-rejection case asserts 422 → recording gone → retry with the same id returns 200 (witness of the reviewer's ROW F), and the uncertain-failure case asserts the recording is preserved.
  4. rc:4058288902 (Critical, unregistered model reaches the daemon as -32603) — packages/acp-bridge/src/session-startup-config.ts: rejectInvalidSelection now also maps the ACP internalError wire shape {code: -32603, message: 'Internal error', data: {details: string}} — which is exactly what switchModel's plain-Error rejection becomes in the SDK (acp.js serializes non-RequestError handler failures that way) — to startup_config_rejected, carrying data.details as the message. Scoped to this call path; auth (-32000), transport, and timeout errors keep their identity. New unit test covers the mapped shape and the three non-mapped -32603 shapes (no data, non-string details, data: null). Result: the ordinary route now answers 422 (not 500) and the standalone path takes the definite rollback branch, so a typo'd modelServiceId no longer burns the standalone id.
  5. rc:4058288904 (Suggestion, EN design doc same overstatement) — docs/design/2026-09-15-task-session-startup-config.md: "failed cleanup enters existing containment" → "cleanup whose outcome cannot be confirmed enters existing containment", restoring parity with the zh-CN "清理无法确认时进入原有隔离流程".
  6. rc:4058288907 (Suggestion, OpenAPI 422 over-promises the close) — The 422 description now reads "closes the new live session and rolls back the persisted recording of a caller-supplied sessionId on a best-effort basis, so the id can be retried; verify the live-session set through GET /daemon/status?detail=full" (matching both the bridge's swallowed-close reality and this round's route rollback; no reference to the non-existent GET /session/{id}).
  7. rc:4058288886 (Suggestion, new warning dropped ${sessionId}) — standalone-session-service.ts: the rollback discard warning now interpolates the session id like its twin (Could not discard the rolled-back standalone directory for ${sessionId}: ...). Declined the suggested logger-helper extraction: two sites is not worth the abstraction this round.

Already resolved before this round (verified)

  • rc:4057076895 (R1-1 Critical, standalone rollback) — Verified at HEAD (f497b740aa): the startupPreparationFailed branch runs cleanRollbackBeforePersistence (kill + removeSession) + best-effort discardEmptyConversationDirectory, and standalone-session-service.test.ts:629 (rolls back a rejected selection and allows the same standalone id to be retried) asserts removeSession, the discard, and a successful same-id retry; :714 pins the cleanup-failure → quarantine path. Marked resolved.

Deferred to the next round (replied inline, threads left open)

  • rc:4058288862 (R1-13) typed id-carrying SDK confirmation error — new public error surface.
  • rc:4058288866 (R1-2) integration teardown probe should use daemonStatus('full') — needs the integration harness to re-witness.
  • rc:4058288877 (R2-11) flip startup_config_rejected to RPC.INVALID_PARAMS in the ACP dispatcher — wire-mapping change; pairs with R2-4's follow-up.
  • rc:4058288872, rc:4058288881, rc:4058288883, rc:4058288889, rc:4058288891, rc:4058288893, rc:4058288895, rc:4058288914 (R2-10, R2-12, R2-13, R2-15, R2-16, R2-17, R2-19, R2-9) — test-only coverage/witness additions.
  • rc:4058288894 (R2-18) value-export DAEMON_REASONING_SELECTIONS — expands the published SDK surface.
  • rc:4058288908 (R2-7) schema-level mutual-exclusion clauses; rc:4058288910 (R2-8) reference the standalone component from a path — contract restructuring.

Notes

  • Failed checks review-pr / review-address: the maintainer's note attributes the review-pr lane failure to the 6-hour tool-budget ceiling (reviewer-side), not a code failure; nothing in this round addresses it.
  • Integration tests were not re-run locally (round budget); the route-rollback behavior is covered by unit tests with a real SessionService, and the mutation probes below are recorded.
  • Mutation probes run this round: (a) disabling the new -32603 arm turns the new bridge unit test red (restored, re-green); (b) removing the route rollback turns the definite-rejection server test red with the retry failing as designed (restored, re-green); (c) widening the rollback guard to all spawn errors turns the uncertain-outcome server test red (restored, re-green); (d) dropping auto from the OpenAPI enum turns the docs-contract assertion red (restored, re-green).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check (9 changed files) — passed
  • npx vitest run (packages/acp-bridge) — 48 files, 2228 passed
  • npx vitest run src/serve/server.test.ts (packages/cli) — 1310 passed
  • npx vitest run src/serve/conversations/standalone-session-service.test.ts src/serve/rest-integration-docs-contract.test.ts (packages/cli) — 126 passed
  • npx vitest run src/serve/routes/standalone-sessions.test.ts src/serve/acp-http/dispatch-error.test.ts src/serve/server/error-response.test.ts (packages/cli) — 102 passed
中文说明

本轮摘要 — PR #12308

Critical-only 制动已生效(此前轮次已用尽),因此本轮实现了四条 Critical 发现,以及 Critical 发现本身明确耦合的三条低成本 Suggestion;其余 Suggestion 通过行内回复延后到下一轮。按制动规则,未触碰"已延后的非 Critical"审计区。

已处理

  1. rc:4058288896(Critical,OpenAPI approvalMode 缺少 auto) — 在 docs/developers/daemon-rest-api.openapi.json 的 CreateStandaloneSessionRequest.approvalMode 枚举中加入 auto(现为 ["plan","default","auto-edit","auto","yolo"],与 APPROVAL_MODES 顺序一致),并在 rest-integration-docs-contract.test.ts 中断言已发布枚举等于 [...APPROVAL_MODES],防止再次发布偏窄的列表。
  2. rc:4058288898(Critical,协议文档过度承诺清理结果) — docs/developers/qwen-serve-protocol.md:收窄为"无法确认删除的记录回滚与瞬态启动失败保留 standalone_creation_outcome_unknown,而目录丢弃失败仅记录日志、仍返回确定的拒绝"——与 standalone-session-service.ts:2806-2822 的实现一致。
  3. rc:4058288900(Critical,调用方提供的 sessionId 被残留记录烧掉) — packages/cli/src/serve/routes/session.ts:POST /session 在确定的 startup_config_rejected 且携带调用方 id 时,catch 现在执行 deleteDaemonSessionIfOrphan(尽力而为,失败时 daemonLog 告警),回滚 spawn 持久化的记录,使该 id 可重试;结果不确定的失败仍保留记录。server.test.ts 新增回归测试:确定拒绝用例断言 422 → 记录已删除 → 同 id 重试返回 200(复核审查者 ROW F 的见证);不确定失败用例断言记录被保留。
  4. rc:4058288902(Critical,未注册模型以 -32603 到达 daemon) — packages/acp-bridge/src/session-startup-config.ts:rejectInvalidSelection 现在还把 ACP internalError 线上形状 {code: -32603, message: 'Internal error', data: {details: string}}(正是 SDK 对 switchModel 抛出的普通 Error 的序列化结果)映射为 startup_config_rejected,并以 data.details 作为消息。仅作用于该调用路径;auth(-32000)、transport、timeout 错误保持原样。新增单元测试覆盖被映射的形状与三种不应映射的 -32603 形状(无 data、details 非字符串、data: null)。结果:普通路由现在返回 422(而非 500),standalone 路径进入确定回滚分支,拼错的 modelServiceId 不再烧掉 standalone id。
  5. rc:4058288904(Suggestion,英文设计文档同样的过度陈述) — docs/design/2026-09-15-task-session-startup-config.md:"failed cleanup enters existing containment" 改为 "cleanup whose outcome cannot be confirmed enters existing containment",与中文版"清理无法确认时进入原有隔离流程"恢复一致。
  6. rc:4058288907(Suggestion,OpenAPI 422 过度承诺关闭) — 422 描述改为"以尽力而为的方式关闭新会话并回滚调用方提供 sessionId 的持久记录,因此该 id 可重试;请通过 GET /daemon/status?detail=full 核对实时会话集合"(同时匹配 bridge 吞掉关闭失败的现实与本轮的路由回滚;未引用不存在的 GET /session/{id})。
  7. rc:4058288886(Suggestion,新告警丢失 ${sessionId}) — standalone-session-service.ts:回滚丢弃告警现在与其孪生告警一样插入会话 id(Could not discard the rolled-back standalone directory for ${sessionId}: ...)。拒绝了所建议的日志辅助函数抽取:两处调用不值得在本轮引入抽象。

本轮前已解决(已核实)

  • rc:4057076895(R1-1 Critical,standalone 回滚) — 在 HEAD(f497b740aa)核实:startupPreparationFailed 分支执行 cleanRollbackBeforePersistence(kill + removeSession)并尽力调用 discardEmptyConversationDirectory;standalone-session-service.test.ts:629(rolls back a rejected selection and allows the same standalone id to be retried)断言了 removeSession、目录丢弃与同 id 重试成功;:714 固定了清理失败 → 隔离路径。标记为已解决。

延后到下一轮(已行内回复,线程保持开放)

  • rc:4058288862(R1-13) 带类型、带 id 的 SDK 确认错误——新的公开错误面。
  • rc:4058288866(R1-2) 集成拆除探针应改用 daemonStatus('full')——需要集成环境重新见证。
  • rc:4058288877(R2-11) 在 ACP dispatcher 中把 startup_config_rejected 翻转为 RPC.INVALID_PARAMS——线上映射变更;与 R2-4 的后续配套。
  • rc:4058288872、rc:4058288881、rc:4058288883、rc:4058288889、rc:4058288891、rc:4058288893、rc:4058288895、rc:4058288914(R2-10、R2-12、R2-13、R2-15、R2-16、R2-17、R2-19、R2-9) — 仅测试的覆盖/见证补充。
  • rc:4058288894(R2-18) 以值导出 DAEMON_REASONING_SELECTIONS——扩大 SDK 已发布表面。
  • rc:4058288908(R2-7) schema 级互斥子句;rc:4058288910(R2-8) 从 path 引用 standalone 组件——契约结构调整。

说明

  • 失败的检查 review-pr / review-address:维护者说明将 review-pr 通道的失败归因于 6 小时工具预算上限(审查方),并非代码失败;本轮不处理。
  • 本地未重跑集成测试(本轮预算);路由回滚行为已由使用真实 SessionService 的单元测试覆盖,且下方记录了变异探针。
  • 本轮执行的变异探针:(a) 禁用新的 -32603 分支使新的 bridge 单元测试变红(已恢复并转绿);(b) 移除路由回滚使确定拒绝的服务端测试变红、重试按预期失败(已恢复并转绿);(c) 把回滚条件放宽到所有 spawn 错误使不确定结果的服务端测试变红(已恢复并转绿);(d) 从 OpenAPI 枚举中删除 auto 使文档契约断言变红(已恢复并转绿)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check(9 个变更文件)— 通过
  • npx vitest run(packages/acp-bridge)— 48 个文件,2228 通过
  • npx vitest run src/serve/server.test.ts(packages/cli)— 1310 通过
  • npx vitest run src/serve/conversations/standalone-session-service.test.ts src/serve/rest-integration-docs-contract.test.ts(packages/cli)— 126 通过
  • npx vitest run src/serve/routes/standalone-sessions.test.ts src/serve/acp-http/dispatch-error.test.ts src/serve/server/error-response.test.ts(packages/cli)— 102 通过

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 4 by @qwen-code /takeover from 4, plus 1 change-producing round(s) since). 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.)

中文说明

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

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

🧵 Resolved all 8 selected review thread(s). · 已关闭全部选中的 8 条评审线程。

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/模型 kimi-k3 · CLI 0.24.4

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

中文说明

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

… recordings (#12308)

- session-startup-config: drop the -32603 arm so child-side internal
  errors (auth, credential, timeout, transport) stay uncertain instead
  of a false definite startup_config_rejected; the ACP child now maps
  only its two caller-caused switchModel refusals (unknown model for the
  auth type, media-only primary) to invalid params, so definite
  rejections ride -32602 alone.
- dispatch: roll back the persisted recording of a caller-supplied
  sessionId on the ACP session/new transports after a definite startup
  rejection, mirroring the REST route; uncertain outcomes keep it.
- routes/session: warn once when the recording rollback is refused or
  fails instead of silently discarding the boolean outcome.
- standalone-session-service: extract the shared rollback sequence into
  one helper used by both the startup-config and model_selection_failed
  branches.
- openapi: qualify the 422 close/rollback as best-effort and replace the
  unconfirmable status-view pointer with the self-certifying retry check.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round 6 — address-review summary

Commit: 937928716b fix(serve): keep uncertain startup faults uncertain and roll back ACP recordings (#12308)

All seven actionable findings are resolved in code. The non-Critical items the workflow deferred (R2-7–R2-11, R2-15 and the older reviews) are outside this round by the deterministic brake and were not touched.

Findings

[Critical] R3-1 (rc:4077430752) — ACP session/new never rolled back the persisted recording on a definite startup rejection — FIXED

Reproduced by reading the dispatch path: the REST route rolls the recording back on startup_config_rejected, while AcpDispatcher's session/new (the single point for both POST-to-SSE and the WS upgrade) went straight to the mapped RPC error, permanently burning the caller-supplied id behind a 409.

Fix (packages/cli/src/serve/acp-http/dispatch.ts): the spawnOrAttach rejection path now mirrors the REST route — on a definite startup_config_rejected with a caller-supplied id it best-effort rolls back the orphan (live kill + persisted recording) via the same removeOrphanSession/deleteDaemonSessionIfOrphan helper the ownership-receipt cleanup already uses, then rethrows so the 422 mapping still goes out. Uncertain outcomes keep the recording.

Witnesses (packages/cli/src/serve/acp-http/transport.test.ts): a definite rejection answers the explicit 422 frame, removes the recording, kills the orphan, and a retry of the same id succeeds; an uncertain failure keeps the recording and the retry still answers 409 session_id_conflict (persisted).

[Critical] R2-4 (rc:4077430763) — the -32603 arm reclassified auth/internal failures as definite rejections — FIXED

Root-caused where the reviewer pointed: every plain Error the child throws crosses ACP as -32603 with the message in data.details (verified in the vendored SDK), so the arm also mapped credential/auth-refresh failures to startup_config_rejected.

Fix: the child now owns the discrimination. Session.setModel converts exactly the two caller-caused switchModel refusals (unknown model for the auth type; image/voice/realtime-only model as primary — message-keyed because core throws plain Errors, and a reworded message degrades to internal error, never to a false refusal) to RequestError.invalidParams, and the bridge mapper's -32603 arm is deleted, so -32602 is the only definite-rejection signal. Auth and other internal faults stay -32603 → uncertain (500).

Witnesses: the former "unknown-model internal-error maps to 422" test is inverted into leaves the internal-error wire shape unmapped even with a details string (both refusal-shaped and auth-shaped data.details now rethrow unchanged); new Session.setModel tests pin the four refusal messages → invalidParams (-32602) and a Missing API key fault → rethrown as-is. The inversion is recorded in test-weakening.json because the replaced assertion pinned the defect itself.

[Suggestion] R3-2 (rc:4077430772) — untested rollback .catch — FIXED

Consolidated with R3-3 into one warn path, then pinned by still answers the definite 422 when the recording rollback fails: a throwing removal (non-not-found kill error) still answers 422 and warns with the error attached.

[Suggestion] R3-3 (rc:4077430780) — discarded rollback boolean — FIXED

The REST rollback now binds deleteDaemonSessionIfOrphan's boolean; a refusal (session stayed live) or a failure produces one consolidated daemon-log warn — startup rejection recording rollback was inconclusive; the session id may stay occupied — while the definite 422 still goes out. Pinned by still answers the definite 422 and warns when the recording rollback is refused (phase-gated summaryImpl, refused kill): 422, warn without an attached error, recording survives.

[Suggestion] R3-4 (rc:4077430812) + [Suggestion] R2-6 (rc:4077430824) — 422 description — FIXED

The POST /session 422 description now states the close is attempted (best-effort, like the rollback), notes an incomplete close/rollback is logged while the definite rejection is still returned, and replaces the unconfirmable GET /daemon/status?detail=full pointer with the self-certifying check: retry the create — 409 session_id_conflict means the id is still occupied. (GET /workspace/{id}/sessions is not part of the published OpenAPI document, so the retry check is the honest pointer.)

[Suggestion] R2-14 (rc:4077430837) — duplicated rollback sequence — FIXED

Both branches now call one private rollbackSessionAndDiscardDirectory helper (cleanRollbackBeforePersistence → discard the empty directory, warning on failure → forget the directory pin). The per-branch difference is preserved: the model_selection_failed branch still throws its definite retryable error, the startup branch still rethrows the startup rejection, and the helper does not double-kill (cleanRollbackBeforePersistence owns the close). New test runs the same rollback sequence for a rejected startup selection and an unapplied scheduled-child model instruments kill/remove/discard and asserts the identical sequence on both branches.

Failed-check note

The pre-round CI failure Test (ubuntu-latest Node 22.x) could not be read from this environment (no GitHub credentials), and no failing test reproduces locally: every test file the PR touches passes in full (see Verification), including all round-5 surfaces. The CI workflow itself documents contention flakes on this fleet (VITEST_RETRY: 2, disjoint flakes per shard). The deterministic gate's CI rerun remains the final arbiter.

Mutation probes

Each new guard was negated, the focused tests confirmed red, then the guard was restored and re-confirmed green:

  • R2-4 bridge arm restored → leaves the internal-error wire shape unmapped… failed (1 failed / 28 passed); restored → 29/29 green.
  • R2-4 child conversion negated → all 4 maps the caller-caused switchModel refusal to invalid params tests failed while the daemon-side fault test stayed green; restored → green.
  • R3-1 dispatcher rollback negated → the rollback/retry test failed (1 failed / 16 passed on the session/new filter) while the uncertain-outcome test stayed green; restored → green.
  • R3-2/R3-3 warn negated → both still answers the definite 422… tests failed; restored → green.
  • R2-14 helper skipping the directory discard → the shared-sequence test failed, and both pre-existing branch tests (rolls back a rejected selection…, rolls back a child before prompt admission…) failed; restored → green.

Verification

  • npm run build — passed (sandbox required COREPACK_HOME=/tmp/... because the default corepack cache under $HOME is not writable here)
  • npm run typecheck — passed
  • npm run lint — passed
  • cd packages/acp-bridge && npx vitest run — 50 files / 2319 tests passed
  • cd packages/cli && npx vitest run src/serve/server.test.ts — 1324 passed
  • cd packages/cli && npx vitest run src/serve/acp-http/transport.test.ts — 392 passed
  • cd packages/cli && npx vitest run src/serve/conversations/standalone-session-service.test.ts — 114 passed
  • cd packages/cli && npx vitest run src/serve/rest-integration-docs-contract.test.ts — 13 passed
  • cd packages/cli && npx vitest run src/serve/acp-http/dispatch-error.test.ts — 24 passed
  • cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts — 1053 passed
  • cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts — 791 passed
  • Post-restore sanity reruns of every probed area — all green
中文说明

第 6 轮 —— address-review 总结

提交:937928716b fix(serve): keep uncertain startup faults uncertain and roll back ACP recordings (#12308)

本轮 7 条可执行反馈全部在代码中解决。被工作流延迟的非 Critical 项(R2-7–R2-11、R2-15 及更早的 review)按确定性刹车不属于本轮范围,未做改动。

反馈处理

[Critical] R3-1(rc:4077430752)—— ACP session/new 在确定性启动拒绝后不回滚已持久化的会话记录 —— 已修复

通过阅读 dispatch 路径复现确认:REST 路由会在 startup_config_rejected 时回滚记录,而 AcpDispatcher 的 session/new(POST-to-SSE 与 WS 升级共用的唯一入口)直接返回映射后的 RPC 错误,导致调用方提供的 id 被 409 永久烧毁。

修复(packages/cli/src/serve/acp-http/dispatch.ts):spawnOrAttach 的拒绝路径现在与 REST 路由一致——当确定性 startup_config_rejected 且带有调用方 id 时,先尽力回滚孤儿会话(关闭 live 会话 + 删除持久化记录),复用 ownership-receipt 清理已在使用的 removeOrphanSession/deleteDaemonSessionIfOrphan 助手,然后再抛出错误,422 映射照常返回。不确定的结果保留记录。

见证(packages/cli/src/serve/acp-http/transport.test.ts):确定性拒绝返回明确的 422 帧、删除记录、关闭孤儿会话,且同一 id 重试成功;不确定性失败保留记录,重试仍返回 409 session_id_conflict(persisted)。

[Critical] R2-4(rc:4077430763)—— -32603 分支把 auth/内部错误误分类为确定性拒绝 —— 已修复

根因定位在审查者指出的位置:子进程抛出的任何普通 Error 经 ACP 序列化后都是 -32603,消息放在 data.details(已在 vendored SDK 中验证),因此该分支同样把凭证/auth 刷新失败映射为 startup_config_rejected。

修复:判别职责下沉到子进程。Session.setModel 只把两类由调用方导致的 switchModel 拒绝(auth 类型下未注册的模型;image/voice/realtime-only 模型用作主模型——因 core 抛普通 Error 故按消息匹配,消息改词只会降级为内部错误、绝不会误判为拒绝)转换为 RequestError.invalidParams,并删除桥接映射器中的 -32603 分支,使 -32602 成为唯一的确定性拒绝信号。auth 及其他内部故障保持 -32603 → 不确定(500)。

见证:原先“未知模型 internal-error 映射为 422”的用例被反转为 leaves the internal-error wire shape unmapped even with a details string(无论拒绝形态还是 auth 形态的 data.details 现在都原样重抛);新增 Session.setModel 测试固定四条拒绝消息 → invalidParams(-32602),以及 Missing API key 故障 → 原样重抛。由于被替换的断言固定的正是缺陷本身,该反转已记录在 test-weakening.json 中。

[Suggestion] R3-2(rc:4077430772)—— 回滚 .catch 无测试 —— 已修复

与 R3-3 合并为单一告警路径,并由 still answers the definite 422 when the recording rollback fails 固定:删除操作抛错(非 not-found 的 kill 错误)时仍返回 422,并携带错误信息告警。

[Suggestion] R3-3(rc:4077430780)—— 回滚布尔返回值被丢弃 —— 已修复

REST 回滚现在绑定 deleteDaemonSessionIfOrphan 的布尔结果;回滚被拒绝(会话仍然存活)或失败都会产生一条统一的 daemon 告警——startup rejection recording rollback was inconclusive; the session id may stay occupied——同时确定性 422 照常返回。由 still answers the definite 422 and warns when the recording rollback is refused(分阶段的 summaryImpl、kill 被拒)固定:返回 422、告警无附加错误、记录保留。

[Suggestion] R3-4(rc:4077430812)+ [Suggestion] R2-6(rc:4077430824)—— 422 描述 —— 已修复

POST /session 的 422 描述现在说明关闭是尝试性的(与回滚同为尽力而为),并注明未完成的关闭/回滚会被记录日志、确定性拒绝仍然返回;同时把无法证实的 GET /daemon/status?detail=full 指针替换为自证检查:重试创建——409 session_id_conflict 表示该 id 仍被占用。(GET /workspace/{id}/sessions 不在已发布的 OpenAPI 文档中,因此重试检查才是诚实的指针。)

[Suggestion] R2-14(rc:4077430837)—— 回滚序列重复 —— 已修复

两个分支现在调用同一个私有助手 rollbackSessionAndDiscardDirectory(cleanRollbackBeforePersistence → 丢弃空目录(失败时告警)→ 删除目录钉住状态)。分支差异保持不变:model_selection_failed 分支仍抛出确定性的可重试错误,startup 分支仍重抛启动拒绝,且助手不会重复关闭(关闭由 cleanRollbackBeforePersistence 负责)。新增测试 runs the same rollback sequence for a rejected startup selection and an unapplied scheduled-child model 对 kill/remove/discard 进行插桩,断言两个分支的调用序列完全一致。

失败检查说明

本轮之前的 CI 失败 Test (ubuntu-latest Node 22.x) 在本环境中无法读取(无 GitHub 凭据),且本地无法复现任何失败:PR 触及的每个测试文件都完整通过(见“验证”),包括第 5 轮的全部改动面。CI 工作流本身记录了该集群的争用抖动(VITEST_RETRY: 2,各分片互不相干的抖动)。确定性门禁的 CI 重跑仍是最终裁决。

变异探针

每个新增守卫都经过“取反 → 聚焦测试确认变红 → 恢复 → 重新确认变绿”的验证:

  • 恢复 R2-4 桥接分支 → leaves the internal-error wire shape unmapped… 失败(1 失败 / 28 通过);恢复后 → 29/29 全绿。
  • 取反 R2-4 子进程转换 → 4 个 maps the caller-caused switchModel refusal to invalid params 测试全部失败,而 daemon 侧故障测试保持绿色;恢复后 → 全绿。
  • 取反 R3-1 dispatcher 回滚 → 回滚/重试测试失败(session/new 过滤下 1 失败 / 16 通过),不确定性结果测试保持绿色;恢复后 → 全绿。
  • 取反 R3-2/R3-3 告警 → 两个 still answers the definite 422… 测试均失败;恢复后 → 全绿。
  • R2-14 助手跳过目录丢弃 → 共享序列测试失败,且两个既有分支测试(rolls back a rejected selection…、rolls back a child before prompt admission…)也失败;恢复后 → 全绿。

验证

  • npm run build —— 通过(沙箱需要 COREPACK_HOME=/tmp/...,因为 $HOME 下默认的 corepack 缓存目录在此不可写)
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • cd packages/acp-bridge && npx vitest run —— 50 个文件 / 2319 个测试通过
  • cd packages/cli && npx vitest run src/serve/server.test.ts —— 1324 通过
  • cd packages/cli && npx vitest run src/serve/acp-http/transport.test.ts —— 392 通过
  • cd packages/cli && npx vitest run src/serve/conversations/standalone-session-service.test.ts —— 114 通过
  • cd packages/cli && npx vitest run src/serve/rest-integration-docs-contract.test.ts —— 13 通过
  • cd packages/cli && npx vitest run src/serve/acp-http/dispatch-error.test.ts —— 24 通过
  • cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts —— 1053 通过
  • cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts —— 791 通过
  • 探针恢复后对所有被探测区域的复跑 —— 全部通过

🧪 Gate advisory — this round weakened or removed pre-existing tests (machine-measured, not agent-authored):

  • packages/acp-bridge/src/session-startup-config.test.ts — net 1 assertion(s) removed

The round recorded evidence for each (below, agent-authored). Weakening is sound only when the pinned behaviour itself was wrong or the coverage demonstrably survives elsewhere — read each reason against the diff. · 本轮弱化或删除了既有测试(门自动测量,非 agent 文本)。下列理由由 agent 撰写:仅当被钉住的行为本身有误、或覆盖确有替代时才成立,请对照 diff 逐条审阅。

  • packages/acp-bridge/src/session-startup-config.test.ts: Review finding R2-4 (rc:4077430763) proved the replaced assertion pinned a defect: the -32603 arm mapped auth/credential failures to a false definite startup_config_rejected (probe: fake serve with modelServiceId gpt-5.4(openai) and no OPEN

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 4 by @qwen-code /takeover from 4, plus 2 change-producing round(s) since). 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.)

中文说明

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

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

🧵 Resolved all 7 selected review thread(s). · 已关闭全部选中的 7 条评审线程。

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/模型 kimi-k3 · CLI 0.24.4

@callmeYe

Copy link
Copy Markdown
Collaborator Author

Review convergence / follow-up disposition

This PR has passed five review rounds. The remaining Suggestion threads below are explicitly deferred to separate follow-up work, not to another automatic fix round in this PR. They are not claimed fixed. The current PR remains responsible for all valid Critical findings; their current-head verification is tracked separately.

  • [Suggestion] R1-13: (fix-induced) Hoisting the confirmation assertion out of the recovery try — the fix for R1-13 — means the failure now escapes as a bare Error carrying only stack and message, after the daemon has already created the session with 200. The caller gets no sessionId and no machine-readable code, so the live session it just paid for is unaddressable. Original discussion.
  • [Suggestion] R1-2: (fix-induced) The teardown half of the new applies high over shared none and tears down only rejected startup selections case probes GET /session/:id, which is not an API route — it 404s for a live session too, so the assertion added in response to R1-2 can never fail and the teardown it was written to pin stays unpinned. Original discussion.
  • [Suggestion] R2-10: The only test of the definite-vs-uncertain error boundary drives every error shape into the first setSessionConfigOption call, so the reasoning leg's .catch(rejectInvalidSelection) is never witnessed with an error that must not be classified as a rejection. Original discussion.
  • [Suggestion] R2-11: The new toRpcError branch stamps startup_config_rejected — a deterministic, client-caused rejection it simultaneously labels httpStatus: 422 — with JSON-RPC RPC.INTERNAL_ERROR (-32603), inverting the 4xx→INVALID_PARAMS rule every sibling branch in the same function follows. Original discussion.
  • [Suggestion] R2-12: The startup rollback's best-effort directory-discard failure leg has zero coverage: the catch body at standalone-session-service.ts:2813-2816 is never entered by the 113-test suite, and the only rejection mock for that dependency anywhere in the repo drives the legacy model_selection_failed path. Original discussion.
  • [Suggestion] R2-13: The three new "uncertain startup failure" cases assert nothing that separates the intended close-and-retain path from a terminal quarantine of the whole conversation runtime — both branches produce identical observables for every assertion in the test. Original discussion.
  • [Suggestion] R2-15: The new docs-contract assertion derives one value from core and hardcodes its two siblings: maxLength: 256 is compared against the document's own literal, and SessionStartupConfigApplied's two hand-written enums are checked by nothing at all. Original discussion.
  • [Suggestion] R2-16: The new SDK preflight validateStartupConfigRequest is unwitnessed on both of its sides — 8 of its 10 reject arms are deletable with zero new test failures, and deleting the standalone path's call to it entirely leaves the whole SDK suite byte-identical. Original discussion.
  • [Suggestion] R2-17: Two more reject arms of assertStartupConfigApplied survive deletion with the whole SDK suite green — the modelApplied !== true strictness at :69 and the effectiveReasoning well-formedness arm at :74-75 — because every rejection row in both new suites uses one request shape (reasoningEffort: 'high'), where a different arm always fires first. Original discussion.
  • [Suggestion] R2-18: The new DAEMON_REASONING_SELECTIONS runtime list is not value-exported from either published entry point — unlike its siblings DAEMON_APPROVAL_MODES and DAEMON_ERROR_KINDS — and nothing anywhere pins it to core's REASONING_EFFORT_TIERS, although this diff makes it load-bearing for two new guards. Original discussion.
  • [Suggestion] R2-19: The new tests pin startupConfig confirmation only on the definite HTTP-200 path. None of the five unknown-outcome recovery tests in this file passes a startupConfig, and the recovery payload's declared type structurally cannot carry the confirmation — so the "startup selection × creation recovery" interaction is unpinned. Original discussion.
  • [Suggestion] R2-7: The mutual exclusion between startupConfig and the legacy modelServiceId / single sessionScope is carried only in prose. Both request schemas declare all of those properties side by side under a single additionalProperties: false, which blocks only unknown keys — so a combined body validates against the published schema and the daemon then answers 400. Original discussion.
  • [Suggestion] R2-8: The newly published CreateStandaloneSessionRequest component is referenced by no path in this curated spec, so none of the docs-contract test's path-driven validation reaches it — only two of its properties are asserted anywhere, and its sessionId UUID pattern, required list and modelServiceId bound are unpinned against the route's real behaviour. Original discussion.
  • [Suggestion] R2-9: The new parser's acceptance side is unpinned. The 256-character bound is only proven to reject at 257 and never to accept at 256, trim normalisation is exercised nowhere, and no test drives reasoningEffort: 'default' or 'none' through parseSessionStartupConfig — only the five core tiers are. Original discussion.

Closing these Suggestion discussions records the scope decision, not completion of the follow-up work. No production code is changed by this disposition.

@callmeYe

Copy link
Copy Markdown
Collaborator Author

Independent verification of exact head 9379287 confirms all seven historical Critical findings are fixed.

Verified: standalone recording rollback permits same-ID retry; the approval enum includes auto; cleanup documentation matches best-effort directory discard; REST and ACP definite startup rejections roll back persisted recordings; invalid model selections remain definite parameter errors; ordinary child -32603 auth/internal/transient errors retain their uncertain classification instead of being destructively rolled back.

Isolated build and bundle pass. Focused validation passes: 29 bridge startup tests, 29 CLI tests across five files, and three integration cases against the real bundled daemon (61 total). Unselected tests were not run; this is not a full-suite claim. No source changes or pushes were made during this verification.

All Critical threads are resolved. The 14 remaining Suggestions have been explicitly deferred to separate follow-up work and closed as deferred, not fixed: #12308 (comment). No live human objection was found. The historical bot blocking reviews are now stale; normal required approval and check protections still apply.

@callmeYe
callmeYe dismissed stale reviews from ghost September 23, 2026 08:49

Stale at 9379287: all seven historical Criticals independently verified fixed (standalone/REST/ACP recording rollback, auto enum, cleanup docs and precise rejection classification). Build/bundle and 61 focused/real-daemon tests pass; evidence comment5791850516. Suggestions explicitly deferred in comment5791801846; zero unresolved threads or live human objections. Normal required review remains.

@callmeYe
callmeYe requested a review from wenshao September 23, 2026 08:49
@callmeYe
callmeYe enabled auto-merge September 23, 2026 08:49
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round — no action required

This round's feedback contains no actionable items, so no code changes were made and no commit was created.

What the feedback contained

  • [ic:5791801846] (maintainer, @callmeYe) — review convergence / follow-up disposition. Records the scope decision that the 14 remaining Suggestion threads (R1-2, R1-13, R2-7 through R2-19) are explicitly deferred to separate follow-up work — not to another automatic fix round in this PR. The disposition states: "Closing these Suggestion discussions records the scope decision, not completion of the follow-up work. No production code is changed by this disposition."
  • [ic:5791850516] (maintainer, @callmeYe) — independent verification. Confirms all seven historical Critical findings are fixed at the exact current head 937928716b913f3e8a23027840e521ff88d51e25, with isolated build, bundle, and focused validation (61 tests) passing. No further work requested.

Why no automatic action

  • Critical-only mode is active this window (round counter reached 5): the deterministic filter already excluded all non-Critical feedback from the actionable sections, and the maintainer disposition above confirms those Suggestions are deferred to follow-up work outside this PR.
  • The maintainer's deferral is an explicit scope decision. Re-implementing the deferred Suggestions in this round would directly contradict it and would violate the review-round ratchet guidance in AGENTS.md (after ~5 rounds, land only Critical fixes; defer Suggestions to follow-up).
  • No failed checks are listed, --conflict is false, and the working tree is clean at 937928716b, which matches the independently verified head.

State left behind

  • Branch codex/session-startup-config unchanged at 937928716b; no files modified, staged, or committed.
中文说明

Autofix 本轮 —— 无需处理

本轮反馈不包含任何可执行条目,因此未做任何代码修改,也未创建提交。

反馈内容

  • [ic:5791801846](维护者 @callmeYe)—— 评审收敛 / 后续处置决定。 记录了范围决策:剩余的 14 条 Suggestion 线程(R1-2、R1-13、R2-7 至 R2-19)明确延后到独立的后续工作中处理,而不是在本 PR 中再进行一轮自动修复。该处置说明指出:"关闭这些 Suggestion 讨论记录的是范围决策,并不代表后续工作已完成。此处置不改动任何生产代码。"
  • [ic:5791850516](维护者 @callmeYe)—— 独立验证。 确认在当前确切头提交 937928716b913f3e8a23027840e521ff88d51e25 上,全部七条历史 Critical 发现均已修复;独立构建、打包以及聚焦验证(61 个测试)均通过。未提出进一步工作要求。

为何不采取自动行动

  • 本窗口已处于仅处理 Critical 的模式(轮次计数已达 5):确定性过滤器已把所有非 Critical 反馈排除在可执行区域之外,且上述维护者处置决定确认这些 Suggestion 已延后到本 PR 之外的后续工作。
  • 维护者的延后决定是明确的范围决策。若在本轮重新实现这些已延后的 Suggestion,将直接违背该决策,也会违反 AGENTS.md 中关于评审轮次的指导(约 5 轮之后只落地 Critical 修复,Suggestion 延后处理)。
  • 反馈中未列出任何失败的检查,--conflict 为 false,工作区在 937928716b 处保持干净,与独立验证所针对的头提交一致。

留下的状态

  • 分支 codex/session-startup-config 保持在 937928716b 不变;未修改、暂存或提交任何文件。

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 4 by @qwen-code /takeover from 4, plus 3 change-producing round(s) since). 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.)

中文说明

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

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.4

yiliang114
yiliang114 previously approved these changes Sep 23, 2026

@yiliang114 yiliang114 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 at 93792871 (base 99bf4ce8). Approving — no P0/P1 found.

Input validation is fail-closed at every layer (parseSessionStartupConfig rejects non-objects, unknown keys including __proto__, over-length ids, bad tiers, and conflicts with the legacy modelServiceId/sessionScope shapes), and the model id only ever reaches the child as a JSON string via setSessionConfigOption — same trust level as the pre-existing legacy path, so no new privilege or injection surface. Unknown models map to -32602 → startup_config_rejected → 422 while transport/credential faults stay 500, with a documented definite-vs-uncertain split (uncertain outcomes keep the session-id recording so a retry surfaces 409 instead of lying). The contract is coherent end-to-end: same field names and required/optional semantics across SDK, REST routes, ACP dispatch and bridge, and an omitted reasoningEffort issues exactly one setter call with no default substitution (test-pinned). Capability gating protects SDK callers against old daemons.

Two optional P3s:

  1. Model confirmation accepts any non-empty currentValue echo and the reasoning path only checks echo₁===echo₂, never echo===request — a child that silently substitutes a different model on a successful set would produce a 200 on the wrong model (disclosed via startupConfigApplied.modelServiceId, but SDK-side assertion only checks non-empty). A test pins this as intentional; consider an opt-in strict equality check when the id is already canonical.
  2. DAEMON_REASONING_SELECTIONS and the 256-char bound are duplicated in the SDK instead of single-sourced — a new reasoning tier would make the daemon accept what the SDK rejects client-side, and the docs contract test does not cover SDK↔daemon.

CI green at this head; 0 unresolved threads.

@qwen-code-dev-bot
qwen-code-dev-bot dismissed stale reviews from ytahdn and yiliang114 via 315a535 September 23, 2026 16:17
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round summary

Merged origin/main (the requested conflict resolution) and addressed 8 of the 11 actionable findings: both Criticals, plus 6 Suggestions. The 3 remaining Suggestions — all OpenAPI/docs-contract schema pins — are deferred to the next round (this round is bounded to ~8 findings; Criticals and behavior-carrying Suggestions took priority). Each deferred finding has a reply on its own thread.

Merge conflict resolution (--conflict true)

packages/cli/src/serve/conversations/standalone-session-service.ts conflicted in three regions of createUnderExclusive: main added attempt.diagnostic bookkeeping (phase / cleanupOutcome / cause) while this PR added the startupConfig apply and the rollbackSessionAndDiscardDirectory helper. Resolution keeps both: the model-selection and startup-preparation rollback paths use the PR's helper wrapped in main's diagnostic transitions (unknown → rolled_back), the startup-preparation catch sets attempt.cause like every other main-side failure path, and the non-startup catch keeps main's 4-arg serviceError(..., error) cause-carrying form. No behavior from either side was dropped.

Feedback dispositions

rc:4083321998 (R4-1, Critical) — startup_config_rejected carried no sessionId; rollback gates guessed requestedSessionId — RESOLVED

  • SessionStartupConfigError gains an optional sessionId; isSessionStartupConfigError's Pick widens to include it (the guard stays structural for split bundles).
  • applySessionStartupConfig threads its existing sessionId parameter into all three startup_config_rejected construction sites (the two rejectInvalidSelection wraps and the two unconfirmed-selection throws). invalid_startup_config stays id-less — it is raised before any session exists.
  • Both rollback gates (REST routes/session.ts, ACP dispatch.ts) now name the rejected session as err.sessionId ?? requestedSessionId. One deliberate deviation from the suggested requestedSessionId ?? err.sessionId order: when the child registers under its own id B while the caller supplied A, the recording lives under B — the id the error carries — so preferring the error's id is the only order that also fixes that variant; the pre-existing not-honored path in the same file already removes the orphan by the actual session.sessionId, confirming the convention. When the child honored the request the orders coincide, and when no id was requested the error's id is the only choice.
  • Tests: new REST case (server.test.ts) whose spawn writes chats/<id>.jsonl under a daemon-generated id and asserts the recording is gone after the 422; new ACP twin (transport.test.ts) without _meta['qwen-code/sessionId']; the sibling test's rejection now also asserts sessionId: 'failed'.

rc:4083322009 (R4-2, Critical) — modelApplied presence rule regressed — RESOLVED (prose-only)

Verified against the bridge: the single-scope attach return builds the body field-by-field and never sets modelApplied; the coalescing attach spreads the spawn owner's result. Took the prose-only route (the finding's own preferred fix): restored spawn-scoped wording in the bridgeTypes.ts JSDoc and the OpenAPI Session.modelApplied.description — present only on a fresh spawn that carried modelServiceId/startupConfig; an attach omits it or reports the spawn owner's outcome, and on attach model_switch_failed is the caller's signal. No code change, so no test (as the finding noted).

rc:4083322020 (R4-3, Suggestion) — classifier reverse-engineers core message text — RESOLVED

Added a Session.test.ts case that drives a real ModelsConfig (empty registry; one imageOnly registration) through session.setModel via switchModelSpy.mockImplementation, asserting both real refusals map to RequestError -32602. The hand-written literals stay for the -32602/httpStatus-422 pin. Probe: breaking the classifier pattern turns this test red while the literal cases stay green — the drift hole is closed.

rc:4083322027 (R4-4, Suggestion) — ACP rollback discarded the boolean — RESOLVED

The ACP gate now binds removeOrphanSession's result and writeStderrLines the same "startup rejection recording rollback was inconclusive; the session id may stay occupied" line (with logSafe(id)) the REST twin already logs. Rejection handling inside removeOrphanSession is unchanged. New transport.test.ts pin: refused kill + live summary → the 422 still answers, the log line is written, and a retry of the id answers 409 session_id_conflict. Note: the retry asserts conflict: 'live' rather than the suggested 'persisted' — a refused rollback means the session is still live at retry time, so live is the honest conflict; the occupation signal the finding asked for holds.

rc:4083322031 (R4-5, Suggestion) — unpinned service-boundary guard — RESOLVED

New standalone-session-service.test.ts case: createWithInitialPrompt({ sessionId, startupConfig, modelServiceId }) rejects with { code: 'invalid_startup_config' } and spawnStandaloneSession/setSessionConfigOption are never called.

rc:4083322036 (R4-6, Suggestion) — SDK bare TypeError — RESOLVED

New DaemonStartupConfigError extends TypeError carrying readonly code = 'invalid_startup_config'; thrown by validateStartupConfigRequest and exported from the SDK daemon barrel and root. DaemonClient.test.ts:3636 now asserts rejects.toMatchObject({ code: 'invalid_startup_config' }) (message retained via stringContaining), keeping the pre-transport calls assertion.

rc:4083322061 (R2-9, Suggestion) — parse-boundary test gaps — RESOLVED

The acceptance list widened to [...REASONING_EFFORT_TIERS, 'default', 'none'] as const, and a new case pins both .trim() branches: { modelServiceId: ' ' } throws invalid_startup_config (guard trim), { modelServiceId: ' gpt-5.4(openai) ' } returns the trimmed id (return trim).

rc:4083322081 (R2-11, Suggestion) — startup_config_rejected mapped to INTERNAL_ERROR — RESOLVED

Both startup-config kinds now map to RPC.INVALID_PARAMS; data.httpStatus keeps 400 vs 422, and the SDK transports key on httpStatus, so no client-visible regression beyond the code alignment. dispatch-error.test.ts and the two transport assertions updated.

Deferred to next round (replies posted on each thread):

  • rc:4083322043 (R2-7) — OpenAPI not mutual-exclusion clause + docs-contract assertion.
  • rc:4083322053 (R2-8) — pin sessionId.pattern and modelServiceId bounds in the docs-contract test.
  • rc:4083322077 (R2-15) — interpolate MAX_CRON_TASK_ROUTING_ID_LENGTH into the daemon's 400 message and assert the constant's value beside the schema assertions.

Mutation probes (each new guard/branch verified to have a failing witness)

  • R4-1: dropped the sessionId constructor argument → sibling test red; reverted both gates to requestedSessionId-only → new REST and ACP daemon-generated-id tests red. Restored, all green.
  • R4-3: drifted the classifier pattern (authType → provider) → the new real-core test red. Restored.
  • R4-4: negated the !removed condition → refused-rollback log test red. Restored.
  • R4-5: removed the service-boundary parseSessionStartupConfig call → new boundary test red. Restored.
  • R4-6: dropped code from DaemonStartupConfigError → SDK test red. Restored.
  • R2-9: removed 'default'/'none' from isReasoningSelection → the two new it.each cases red; removed guard .trim() → whitespace-reject red; removed return .trim() → trimmed-return red. Restored.
  • R2-11: flipped the mapping back to INTERNAL_ERROR → dispatch-error.test.ts red. Restored.

Verification

  • npm run build — passed (note: this sandbox needed COREPACK_HOME=/tmp/corepack-cache; the default corepack cache path is not writable here)
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check on all touched files — passed (one formatting fix applied to the new Session.test.ts block)
  • packages/acp-bridge: npx vitest run src/session-startup-config.test.ts — 32 passed
  • packages/sdk-typescript: npx vitest run test/unit/DaemonClient.test.ts — 484 passed
  • packages/cli: npx vitest run src/serve/server.test.ts src/serve/acp-http/transport.test.ts src/serve/acp-http/dispatch-error.test.ts src/serve/conversations/standalone-session-service.test.ts src/acp-integration/session/Session.test.ts src/serve/rest-integration-docs-contract.test.ts — 6 files, 2948 passed
中文说明

本轮总结

合并了 origin/main(即本次要求的冲突解决),并处理了 11 项可执行反馈中的 8 项:两个 Critical,外加 6 个 Suggestion。其余 3 个 Suggestion——全部是 OpenAPI/docs-contract 的 schema 固定——推迟到下一轮(本轮上限约 8 项;Critical 和携带行为变更的 Suggestion 优先)。每个被推迟的发现都在其各自的评论线程中回复了。

合并冲突解决(--conflict true)

packages/cli/src/serve/conversations/standalone-session-service.ts 在 createUnderExclusive 的三个区域冲突:main 增加了 attempt.diagnostic 记录(phase / cleanupOutcome / cause),而本 PR 增加了 startupConfig 应用和 rollbackSessionAndDiscardDirectory 辅助方法。解决方案两者兼得:model-selection 和 startup-preparation 回滚路径使用 PR 的辅助方法,并包上 main 的诊断状态迁移(unknown → rolled_back);startup-preparation 的 catch 像 main 侧其他失败路径一样设置 attempt.cause;非 startup 的 catch 保留 main 的四参 serviceError(..., error) 携带 cause 的形式。两侧的行为都没有丢失。

反馈处理结果

rc:4083321998(R4-1,Critical)——startup_config_rejected 不携带 sessionId,回滚门只能猜测 requestedSessionId —— 已解决

  • SessionStartupConfigError 增加可选 sessionId;isSessionStartupConfigError 的 Pick 扩展包含它(守卫保持结构化以兼容拆分 bundle)。
  • applySessionStartupConfig 把它已有的 sessionId 参数传入全部三个 startup_config_rejected 构造点。invalid_startup_config 保持无 id——它在任何会话存在之前抛出。
  • 两个回滚门(REST 与 ACP)现在以 err.sessionId ?? requestedSessionId 命名被拒绝的会话。与建议的 requestedSessionId ?? err.sessionId 顺序有一个刻意偏差:当子进程注册在自己的 id B 下而调用方提供了 A 时,录制文件在 B 下——也就是错误携带的 id——所以优先错误的 id 是唯一能同时修复该变体的顺序;同文件中已有的 id 不匹配路径也是按实际的 session.sessionId 清理孤儿会话,印证了这一约定。
  • 测试:新增 REST 用例(daemon 生成 id 时录制被回滚)、新增 ACP 对应用例(不带 _meta['qwen-code/sessionId']),sibling 测试的拒绝断言现在也断言 sessionId: 'failed'。

rc:4083322009(R4-2,Critical)——modelApplied 存在性规则回退 —— 已解决(仅文案)

对照 bridge 代码核实:single-scope attach 返回逐字段构造,从不设置 modelApplied;coalescing attach 展开 spawn 发起者的结果。采用纯文案方案(即发现本身倾向的修法):在 bridgeTypes.ts 的 JSDoc 和 OpenAPI 的 Session.modelApplied.description 中恢复了限定 fresh spawn 的措辞。无代码改动,故无测试(发现中也如此注明)。

rc:4083322020(R4-3,Suggestion)——分类器逆向工程 core 的消息文本 —— 已解决

新增 Session.test.ts 用例:通过 switchModelSpy.mockImplementation 让真实的 ModelsConfig(空注册表;一个 imageOnly 注册)驱动 session.setModel,断言两种真实拒绝都映射为 RequestError -32602。探针:破坏分类器模式会让该测试变红,而手写字面量用例保持绿色——漂移缺口已闭合。

rc:4083322027(R4-4,Suggestion)——ACP 回滚丢弃了布尔结果 —— 已解决

ACP 门现在绑定 removeOrphanSession 的返回值,并在结果为假时通过 writeStderrLine 输出与 REST 侧相同的"startup rejection recording rollback was inconclusive"日志(带 logSafe(id))。新增 transport.test.ts 用例:kill 被拒 + 会话仍存活 → 422 照常返回、日志写出、同 id 重试得到 409 session_id_conflict。注意:重试断言的是 conflict: 'live' 而非建议的 'persisted'——回滚被拒意味着重试时会话仍然存活,live 才是诚实的冲突类型;发现所要求的"id 仍被占用"信号成立。

rc:4083322031(R4-5,Suggestion)——服务边界守卫未被测试固定 —— 已解决

新增 standalone-session-service.test.ts 用例:createWithInitialPrompt({ sessionId, startupConfig, modelServiceId }) 以 { code: 'invalid_startup_config' } 拒绝,且 spawnStandaloneSession/setSessionConfigOption 从未被调用。

rc:4083322036(R4-6,Suggestion)——SDK 裸 TypeError —— 已解决

新增 DaemonStartupConfigError extends TypeError,携带 readonly code = 'invalid_startup_config';由 validateStartupConfigRequest 抛出,并从 SDK daemon 桶和根入口导出。DaemonClient.test.ts:3636 现在断言 rejects.toMatchObject({ code: 'invalid_startup_config' })。

rc:4083322061(R2-9,Suggestion)——解析边界测试缺口 —— 已解决

接受用例扩展为 [...REASONING_EFFORT_TIERS, 'default', 'none'] as const,新增用例固定两个 .trim() 分支:{ modelServiceId: ' ' } 抛出 invalid_startup_config(守卫 trim),{ modelServiceId: ' gpt-5.4(openai) ' } 返回 trim 后的 id(返回值 trim)。

rc:4083322081(R2-11,Suggestion)——startup_config_rejected 映射到 INTERNAL_ERROR —— 已解决

两种 startup-config 错误现在都映射到 RPC.INVALID_PARAMS;data.httpStatus 保留 400 与 422 的区分,SDK 传输层以 httpStatus 为准,因此除代码对齐外无客户端可见回归。dispatch-error.test.ts 和两处 transport 断言已更新。

推迟到下一轮(已在各线程回复):

  • rc:4083322043(R2-7)——OpenAPI not 互斥子句 + docs-contract 断言。
  • rc:4083322053(R2-8)——在 docs-contract 测试中固定 sessionId.pattern 与 modelServiceId 边界。
  • rc:4083322077(R2-15)——把 MAX_CRON_TASK_ROUTING_ID_LENGTH 插值进 daemon 的 400 消息,并在 schema 断言旁断言该常量的值。

变异探针(每个新增守卫/分支都验证了有会变红的见证测试)

  • R4-1:去掉 sessionId 构造参数 → sibling 测试变红;把两个门改回仅 requestedSessionId → 新增的 REST 和 ACP daemon 生成 id 用例变红。已恢复,全绿。
  • R4-3:让分类器模式漂移(authType → provider)→ 新增的真实 core 用例变红。已恢复。
  • R4-4:反转 !removed 条件 → 回滚被拒日志用例变红。已恢复。
  • R4-5:移除服务边界的 parseSessionStartupConfig 调用 → 新增边界用例变红。已恢复。
  • R4-6:去掉 DaemonStartupConfigError 的 code → SDK 用例变红。已恢复。
  • R2-9:从 isReasoningSelection 移除 'default'/'none' → 两个新 it.each 用例变红;移除守卫 .trim() → 空白拒绝用例变红;移除返回 .trim() → trim 返回断言变红。已恢复。
  • R2-11:把映射改回 INTERNAL_ERROR → dispatch-error.test.ts 变红。已恢复。

验证

  • npm run build —— 通过(注:此沙箱需要 COREPACK_HOME=/tmp/corepack-cache;默认 corepack 缓存路径在此不可写)
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • 对所有改动文件运行 npx prettier --check —— 通过(对新增的 Session.test.ts 代码块做了一次格式化修正)
  • packages/acp-bridge:npx vitest run src/session-startup-config.test.ts —— 32 通过
  • packages/sdk-typescript:npx vitest run test/unit/DaemonClient.test.ts —— 484 通过
  • packages/cli:npx vitest run src/serve/server.test.ts src/serve/acp-http/transport.test.ts src/serve/acp-http/dispatch-error.test.ts src/serve/conversations/standalone-session-service.test.ts src/acp-integration/session/Session.test.ts src/serve/rest-integration-docs-contract.test.ts —— 6 个文件,2948 通过

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 4 by @qwen-code /takeover from 4, plus 3 change-producing round(s) since). 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.)

  • @callmeYe is at this window's regular-feedback budget — to continue: start your comment with [Critical], submit a Request changes review, or comment @qwen-code /retry for a fresh window. / @callmeYe 本窗口常规反馈预算已用完——继续请以 [Critical] 开头评论、提交 Request changes、或评论 @qwen-code /retry 开新窗口。
中文说明

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

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

🧵 Resolved all 8 selected review thread(s). · 已关闭全部选中的 8 条评审线程。

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/模型 kimi-k3 · CLI 0.24.4

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (3600000ms)) (attempt 1/100) — it will retry on the next scan.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:
Qwen failed during address-review: timeout (3600000ms).

See the Qwen Autofix agent step logs for model/tool output.

中文说明

🤖 AutoFix 在完成前耗尽了时间(timeout (3600000ms))(第 1/100 次尝试)—— 将在下次扫描时重试。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/35921449611


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.4

A modelServiceId carrying a newline slipped past parseSessionStartupConfig
and produced a core refusal message the single-line classifier regexes
could not match, so a definite caller-caused rejection degraded to an
internal error and skipped the recording rollback. Reject control
characters at the parse boundary and make isCallerCausedModelRefusal
newline-tolerant; align the SDK and integration-guide modelApplied
presence rule with the spawn-scoped wording the bridge and OpenAPI
already publish.
@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 round summary — PR #12308

Round commit: 133ad5266a — fix(serve): fail closed on control-character startup model ids (#12308)

Feedback addressed

  • [rc:4087306053] R5-1 (Critical, certifies-falsely) — modelApplied presence rule still contradicted in three artifacts. Fixed exactly as suggested: the spawn-scoped wording this PR already published in packages/acp-bridge/src/bridgeTypes.ts was copied verbatim into packages/sdk-typescript/src/daemon/types.ts (DaemonSession.modelApplied) and condensed into the DaemonSessionClient.modelApplied getter doc; the integration guide's absolute ("a create carrying neither selector has no modelApplied key at all") was qualified in docs/developers/rest-api-integration.md so a create that coalesced with an in-flight spawn is documented as reporting the spawn owner's modelApplied, with model_switch_failed remaining the attach signal. Only the published contract text changed — attach stays a 200 and no rejected attach-time switch became an HTTP error, keeping the guide's 291–293 invariant true.
  • [rc:4087306068] R5-2 (Critical, certifies-falsely) — caller-caused-vs-daemon-fault split read off a single-line message shape. Implemented the prescribed fail-closed interim fix on both ends. (a) isCallerCausedModelRefusal in packages/cli/src/acp-integration/session/Session.ts now uses [\s\S]+ instead of .+, so a refusal whose model id carries a newline still classifies as the definite caller-caused -32602 → startup_config_rejected → recording rollback, instead of degrading to -32603 with a listed, resumable phantom. (b) parseSessionStartupConfig in packages/acp-bridge/src/session-startup-config.ts now rejects a modelServiceId containing control characters (/\p{Cc}/u, matching the repo's existing id-hygiene convention) with invalid_startup_config, so the classifier never sees one from any of the four daemon entrances that share this parser. The reviewer's requested pin was added verbatim to the it.each at Session.test.ts ("Model 'qwen\ntypo' not found for authType 'openai'" asserts -32602), plus a parse-boundary test in session-startup-config.test.ts. The legacy create-time path (applyModelServiceId(...).then(() => true, () => false)) is untouched: a legacy rejection stays a 200 with modelApplied: false.

Deliberately not done

  • Core-side typed/coded switchModel errors (the reviewer's structural direction). The finding itself scoped this as "until then, fail closed" — the fail-closed half is what this round implements. Rewriting core's thrown error shape is a cross-package contract change beyond this round's footprint.
  • R5-2's "not exclusive" half (same media-only string emitted by syncAfterAuthRefresh). The reviewer marked this half unproven ("treat as unproven until it is measured"); no change made.
  • The 12-item deferred list and the 6 already-reported Suggestions in the review body. Those are the convergence-posture audit record / floor deferrals, not requested work this round; no code, thread, or reply was touched for them.

Verification

  • npm run build (run as COREPACK_HOME=/tmp/corepack-cache npm run build — the sandbox denies corepack's default ~/.cache mkdir with EACCES; the redirect is environment-only and changes no command semantics) — passed (exit 0, also regenerated the settings schema with zero drift: git status after the build showed only the seven intended files).
  • npm run typecheck — passed (exit 0).
  • npm run lint — passed (exit 0).
  • npx prettier --check on the 7 changed files — passed ("All matched files use Prettier code style!").
  • cd packages/acp-bridge && npx vitest run src/session-startup-config.test.ts — 33 passed.
  • cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts (full file) — 1055 passed.
  • cd packages/cli && npx vitest run src/serve/rest-integration-docs-contract.test.ts — 13 passed (the guide edit keeps the contract green).

Mutation probes (red before the fix, green after):

  • With the parse guard absent (pre-fix code), the new rejects a modelServiceId containing control characters test failed ("expected function to throw an error, but it didn't"); after adding /\p{Cc}/u to the boundary it passes — the guard has a witness.
  • With the pre-fix .-based regexes, the new it.each parameter Model 'qwen\ntypo' not found for authType 'openai' failed (1 failed | 4 passed in the filtered run) while every pre-existing parameter stayed green; after the [\s\S] change all 5 pass — the classifier change has a witness, and the sibling keeps daemon-side switchModel faults as internal errors stayed green throughout (it is inside the same full-file run, 1055 passed).
中文说明

Autofix 本轮总结 — PR #12308

本轮提交:133ad5266a — fix(serve): fail closed on control-character startup model ids (#12308)

已处理的反馈

  • [rc:4087306053] R5-1(Critical,certifies-falsely)——modelApplied 存在性规则在三个 artifact 中仍然互相矛盾。 完全按建议修复:把本 PR 已在 packages/acp-bridge/src/bridgeTypes.ts 发布的以 spawn 为范围的措辞逐字复制到 packages/sdk-typescript/src/daemon/types.ts(DaemonSession.modelApplied),并以精简形式写入 DaemonSessionClient.modelApplied getter 的文档;同时限定 docs/developers/rest-api-integration.md 中的绝对表述(“两个选择器都不携带的创建完全没有 modelApplied 键”),说明与进行中 spawn 合流的创建会返回 spawn 所有者的 modelApplied,而 attach 上应以 model_switch_failed 事件为准。只修改了发布的契约文本——attach 仍是 200,被拒绝的 attach 期切换不会变成 HTTP 错误,指南 291–293 行的不变量保持为真。
  • [rc:4087306068] R5-2(Critical,certifies-falsely)——“调用方导致 vs 守护进程故障”的划分依赖消息文本的单行形状。 按评审给出的 fail-closed 临时方案在两端落实。(a) packages/cli/src/acp-integration/session/Session.ts 中的 isCallerCausedModelRefusal 改用 [\s\S]+ 替代 .+,模型 id 含换行时仍能归类为确定性的调用方错误 -32602 → startup_config_rejected → 触发录制回滚,而不再降级为 -32603 并留下可列出、可恢复的幽灵会话。(b) packages/acp-bridge/src/session-startup-config.ts 的 parseSessionStartupConfig 现在以 invalid_startup_config 拒绝包含控制字符的 modelServiceId(/\p{Cc}/u,与仓库既有的 id 卫生校验约定一致),使共享该解析器的全部四个守护进程入口都不会把此类 id 传给分类器。按评审原样加入了验收钉:在 Session.test.ts 的 it.each 中加入 "Model 'qwen\ntypo' not found for authType 'openai'"(断言 -32602),并在 session-startup-config.test.ts 增加解析边界测试。legacy 创建期路径(applyModelServiceId(...).then(() => true, () => false))未改动:legacy 拒绝仍是带 modelApplied: false 的 200。

本轮刻意不做的事

  • core 侧为 switchModel 抛出带类型/带 code 的错误(评审指出的结构化方向)。 发现本身将其限定为“在此之前先 fail closed”——本轮实现的正是 fail-closed 那一半;改写 core 抛出的错误形状是超出本轮范围的跨包契约变更。
  • R5-2 的“不互斥”那一半(syncAfterAuthRefresh 会抛出字节相同的 media-only 字符串)。 评审将其标记为未证明(“在实测之前当作未证明”),未做改动。
  • 评审正文中的 12 条延后清单与 6 条已报告 Suggestion。 它们属于收敛姿态下的审计记录/发布下限延后,不是本轮要求的工作;未为其改动任何代码、会话线程或回复。

验证

  • npm run build(以 COREPACK_HOME=/tmp/corepack-cache npm run build 运行——沙箱拒绝 corepack 在默认 ~/.cache 下 mkdir(EACCES);该重定向仅影响环境,不改变命令语义)——通过(退出码 0,同时重新生成了 settings schema 且零漂移:构建后的 git status 只显示预期的七个文件)。
  • npm run typecheck——通过(退出码 0)。
  • npm run lint——通过(退出码 0)。
  • 对 7 个变更文件执行 npx prettier --check——通过(“All matched files use Prettier code style!”)。
  • cd packages/acp-bridge && npx vitest run src/session-startup-config.test.ts——33 个全部通过。
  • cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts(整文件)——1055 个全部通过。
  • cd packages/cli && npx vitest run src/serve/rest-integration-docs-contract.test.ts——13 个全部通过(指南改动保持契约测试为绿)。

变异探针(修复前为红,修复后为绿):

  • 在缺少解析边界校验(修复前代码)时,新增的 rejects a modelServiceId containing control characters 测试失败(“expected function to throw an error, but it didn't”);在边界加入 /\p{Cc}/u 后通过——该防护有测试见证。
  • 在修复前的 . 正则下,新增的 it.each 参数 Model 'qwen\ntypo' not found for authType 'openai' 失败(过滤运行中 1 失败 | 4 通过),而所有既有参数保持绿色;改为 [\s\S] 后 5 个全部通过——分类器改动有测试见证,且同级用例 keeps daemon-side switchModel faults as internal errors 全程保持绿色(它包含在同一次整文件运行中,1055 个全部通过)。

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

🧵 Resolved all 2 selected review thread(s). · 已关闭全部选中的 2 条评审线程。

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/模型 kimi-k3 · CLI 0.24.4

@callmeYe

Copy link
Copy Markdown
Collaborator Author

Scope disposition for the two reopened Suggestion discussions R2-7 and R2-8: these remain deferred to separate follow-up work, as already recorded in #12308 (comment). The schema mutual-exclusion encoding and standalone schema/path assertion coverage are not claimed fixed. They should not be reintroduced into another automatic fix round of this already-reviewed PR. Closing these duplicate discussions records the same deferral; verification of the new Critical fixes is being performed separately against 133ad52.

@callmeYe

Copy link
Copy Markdown
Collaborator Author

Independent verification at 133ad52: build and 78 focused tests pass (bridge33, CLI32, SDK13). Tests used the original configs with only server.host=127.0.0.1 in temporary wrappers because this host cannot resolve localhost; no source changes. This is not a fresh full-suite or bundled-daemon E2E claim.

R4-1 is fixed: definite rejection carries the actual session ID, REST/ACP prefer it for rollback, and generated-ID recording removal is tested alongside caller-ID and uncertain controls. R4-2/R5-1 presence wording now consistently distinguishes fresh spawn, legacy attach omission and coalesced spawn-owner result in bridge, SDK, OpenAPI and integration documentation.

R5-2's measured newline defect is fixed: parser rejects control characters before spawn, while the child classifier handles newline-containing refusal text; credential-fault identity tests remain green. The review explicitly labeled its second auth-refresh claim unproven. Finding the same message at a second throw site is not a reachable counterexample: the media-only precheck occurs before selection/handleModelChange, and refreshAuth synchronously checks the same selected model before its first await. A real built-core probe for image/voice/realtime-only attempts rejected each before the callback and retained the previous chat model. This rebuts that secondary claim as unsupported, not as impossible or a newly fixed defect. Message-classifier coupling remains a recorded nonblocking follow-up.

Other actionable R4 fixes (refused-rollback logging, service parser guard, typed SDK rejection) are covered. Repeated schema Suggestions remain explicitly deferred, including comment5813834668; not claimed implemented. With no unresolved threads or live human blocking objection, the two historical bot CHANGES_REQUESTED reviews are stale. Normal required approval and checks remain in force. No code edits or pushes in this verification.

@callmeYe
callmeYe dismissed stale reviews from ghost September 24, 2026 12:17

133ad52: R4 generated-ID recording rollback and spawn/attach wording corrected; 78 current-head tests and build pass. R5 newline case fixed, speculative secondary claim rebutted with code and real-core probe. Evidence comment5813901376; Suggestions explicitly deferred, zero unresolved threads or live human objection.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round — no change

Outcome: no code change. The single unresolved finding in this round's feedback is already fixed at the PR head (133ad5266a) and was re-verified this round; everything else in the feedback is explicitly deferred and, per the maintainer's instruction, must not be reintroduced into an automatic fix round.

Feedback triage

  • [Critical] R5-2 (packages/cli/src/acp-integration/session/Session.ts:2088, comment 4087306068) — resolved; re-verified at HEAD.
    • Not-exhaustive half (probe-proven by the reviewer): startupConfig.modelServiceId = "qwen\nmax" slipped past the .-based classifier, producing a 500 plus a phantom recording instead of the documented 422. Fixed in 133ad5266a: parseSessionStartupConfig now rejects control characters at the parse boundary (/\p{Cc}/u, packages/acp-bridge/src/session-startup-config.ts:89), and the classifier regexes in Session.ts use [\s\S] so a newline-bearing refusal message still classifies as caller-caused. The exact pin the reviewer requested — "Model 'qwen\ntypo' not found for authType 'openai'" asserting -32602 (Session.test.ts:8089) — is present and green, and the sibling "keeps daemon-side switchModel faults as internal errors" stays green, so the fail-closed direction the reviewer asked for holds both ways.
    • Not-exclusive half (labeled "unproven until measured" by the reviewer): the claim that the byte-identical media-only message thrown from modelsConfig.ts:1103 (syncAfterAuthRefresh) is reachable from inside the new try via handleModelChange → refreshAuth. A code trace refutes it, matching the maintainer's built-core probe: the media-only pre-check in switchModel (modelsConfig.ts:542) throws before onModelChange fires, and refreshAuth reads this.modelsConfig.getModel() and calls syncAfterAuthRefresh synchronously (config.ts:5251-5252) with the same model id that just passed that pre-check — so a media-only id never reaches the callback and the :1103 branch cannot fire for the model being switched to. No code change; the message-classifier coupling remains a recorded non-blocking follow-up.
  • ic:5813834668 (@callmeYe) — acknowledged and honored: R2-7/R2-8 (schema mutual-exclusion encoding, standalone schema/path assertion coverage) remain deferred to separate follow-up work and were not reintroduced.
  • ic:5813901376 (@callmeYe) — independent verification at 133ad5266a noted; it requests no actions.
  • The 8 already-reported Suggestion-level findings and the 7 convergence-posture deferrals (including the deferred fails-closed/new-surface Critical) are recorded-but-not-requested this round and were left untouched.
  • No failed checks to diagnose: the feedback lists none, and the still-red section is empty.

Verification

  • npm run build — passed (with COREPACK_HOME=/tmp/corepack-cache XDG_CACHE_HOME=/tmp/xdg-cache; the sandbox denies corepack's default ~/.cache/node/corepack path).
  • vitest run src/acp-integration/session/Session.test.ts -t 'switchModel refusal' (packages/cli) — 5 passed, including the newline pin.
  • vitest run src/acp-integration/session/Session.test.ts -t 'refusal core actually throws' (packages/cli) — 1 passed (drives the real ModelsConfig, keeping the message contract honest).
  • vitest run src/acp-integration/session/Session.test.ts -t 'daemon-side switchModel faults' (packages/cli) — 1 passed.
  • vitest run src/session-startup-config.test.ts (packages/acp-bridge) — 33 passed, including the control-character parse-boundary gate.
  • Working tree unchanged: git status --short empty before and after; HEAD remains 133ad5266a.
中文说明

Autofix 本轮 —— 无代码改动

结论:无代码改动。 本轮反馈中唯一未决的发现已在 PR 头提交(133ad5266a)修复,本轮已重新验证;反馈中的其余内容均被明确延后,且按维护者指示不得重新带入自动修复轮次。

反馈分诊

  • [Critical] R5-2(packages/cli/src/acp-integration/session/Session.ts:2088,评论 4087306068)—— 已解决;本轮在 HEAD 重新验证。
    • "不穷尽"一半(审查者已用探针证明): startupConfig.modelServiceId = "qwen\nmax" 绕过基于 . 的分类器,导致返回 500 并留下幽灵录制,而非文档承诺的 422。已在 133ad5266a 修复:parseSessionStartupConfig 现在在解析边界拒绝控制字符(/\p{Cc}/u,packages/acp-bridge/src/session-startup-config.ts:89),且 Session.ts 中的分类器正则改用 [\s\S],使带换行的拒绝消息仍被分类为调用方导致。审查者要求的精确验收测试 —— 断言 -32602 的 "Model 'qwen\ntypo' not found for authType 'openai'"(Session.test.ts:8089)—— 已存在且通过;其兄弟测试 "keeps daemon-side switchModel faults as internal errors" 同样保持通过,审查者要求的 fail-closed 方向在两个方向上都成立。
    • "不互斥"一半(审查者自标注"实测前视为未证明"): 声称 modelsConfig.ts:1103(syncAfterAuthRefresh)抛出的字节相同的 media-only 消息可经 handleModelChange → refreshAuth 从新的 try 内到达。代码追踪将其证伪,与维护者的真实 built-core 探针一致:switchModel 中的 media-only 前置检查(modelsConfig.ts:542)在 onModelChange 触发之前抛出,且 refreshAuth 同步读取 this.modelsConfig.getModel() 并调用 syncAfterAuthRefresh(config.ts:5251-5252),使用的正是刚通过该前置检查的同一个模型 id —— 因此 media-only id 永远到不了回调,:1103 分支不可能为正在切换的目标模型触发。无需代码改动;消息分类器耦合仍是已记录的非阻塞后续工作。
  • ic:5813834668(@callmeYe)—— 已确认并遵守:R2-7/R2-8(schema 互斥编码、standalone schema/路径断言覆盖)继续延后至独立后续工作,未被重新带入。
  • ic:5813901376(@callmeYe)—— 已知悉其在 133ad5266a 的独立验证;该评论未要求任何行动。
  • 8 条已报告过的 Suggestion 级发现与 7 条收敛姿态下的延后项(含 1 条按 fails-closed/new-surface 维度延后的 Critical)本轮均为"已记录、不要求处理",未做改动。
  • 没有需要诊断的失败检查:反馈未列出任何失败检查,still-red 一节为空。

验证

  • npm run build —— 通过(使用 COREPACK_HOME=/tmp/corepack-cache XDG_CACHE_HOME=/tmp/xdg-cache;沙箱拒绝 corepack 默认的 ~/.cache/node/corepack 路径)。
  • vitest run src/acp-integration/session/Session.test.ts -t 'switchModel refusal'(packages/cli)—— 5 个通过,含换行锚定用例。
  • vitest run src/acp-integration/session/Session.test.ts -t 'refusal core actually throws'(packages/cli)—— 1 个通过(驱动真实 ModelsConfig,保持消息契约不失真)。
  • vitest run src/acp-integration/session/Session.test.ts -t 'daemon-side switchModel faults'(packages/cli)—— 1 个通过。
  • vitest run src/session-startup-config.test.ts(packages/acp-bridge)—— 33 个通过,含控制字符解析边界门禁。
  • 工作树未变:前后 git status --short 均为空;HEAD 仍为 133ad5266a。

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

🧵 Resolved all 1 selected review thread(s). · 已关闭全部选中的 1 条评审线程。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.4

@wenshao

wenshao commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Verification round 2: PR #12308 at head 133ad52, macOS, over REST, ACP HTTP and ACP WS

Verdict: no blockers; OK to merge. On the current head, all 27 checks pass on a real qwen serve daemon with the real SDK, across all three transports. I ran the same harness on the head verified last round, f497b74. It fails 8 checks, and each one maps to one of the 4 commits added since. Removing each new hunk from the built head turns exactly its own checks red.

  • Verified head: 133ad5266a0871dbd64066957d1d3a145a5b9379, which is the PR head at posting time. git merge-tree against current main (90232f0) has no conflicts. All 57 review threads are resolved, and CI has no failing lanes.
  • Environment: macOS arm64 (Darwin 25.6), Node v24.18.1. The PR's "Tested on" table and the previous round (Linux aarch64) left out macOS and the ACP HTTP/WS transports end-to-end. This round covers both gaps.

What changed since the last verified head

f497b74 → 133ad52 contains one merge of main and 3 PR commits. Excluding the merge, the PR's own diff is 19 files, +370/−50. It changes four behaviors:

  1. Rollback with a daemon-generated session ID. A definite startup_config_rejected now carries the rejected sessionId, so the REST route and the ACP dispatcher roll back the recording even when the caller did not supply an ID. Before, a listed, resumable empty session was left behind.
  2. Control characters in the model ID. /\p{Cc}/u in modelServiceId is now refused at the boundary with 400 invalid_startup_config. isCallerCausedModelRefusal also switched from . to [\s\S].
  3. ACP JSON-RPC error code. startup_config_rejected is now -32602 (invalid params) instead of -32603 (internal error). data.httpStatus still carries 422.
  4. SDK error type. The SDK's local malformed-input rejection is now DaemonStartupConfigError extends TypeError with code: 'invalid_startup_config'.

A/B on the same scenarios: current head vs. the previous verified head

Setup for both arms:

  • Each arm runs its own worktree build of packages/cli/dist and packages/sdk-typescript/dist.
  • The daemon has an isolated HOME/QWEN_HOME/QWEN_RUNTIME_DIR and a fresh workspace.
  • A loopback fake OpenAI provider records every request body.
  • Settings: one model with reasoning (efforts: [low, high]), one model without reasoning, and shared reasoningEffort: 'none'.
  • Each scenario runs through DaemonClient on REST, on AcpHttpTransport and on AcpWsTransport.
Check (×3 transports unless noted) prev f497b74 head 133ad52
S1: {model, high} returns modelApplied: true and the canonical startupConfigApplied ✅ ✅
S1: provider wire has model=qwen-startup-test, reasoning_effort=high on turn 1 and turn 2 ✅ ✅
S2: model-only on the no-reasoning model gives applied keys [modelServiceId] and no reasoning_effort on the wire ✅ ✅
S3: 'max' without a caller ID is rejected with 422 startup_config_rejected ✅ ✅
S3: no phantom session listed or recorded after that rejection ❌ one new listed session + .jsonl per transport ✅ none
S4: modelServiceId: "qwen-startup-test\n(openai)" returns 400 invalid_startup_config ❌ REST: 500 -32603, and the message echoes the raw newline; ACP: 500 internal ✅ 400 on all three
S4: no session left behind ✅ ✅
S5 (ACP HTTP, raw JSON-RPC from a tee of the /acp stream): code for startup_config_rejected ❌ -32603 ✅ -32602 (invalid_startup_config is also -32602)
S6 (SDK): local malformed rejection is a TypeError with code: 'invalid_startup_config' ❌ code undefined ✅
S7: shared settings.json byte-identical after all creations ✅ ✅
Total 19 pass / 8 fail 27 pass / 0 fail

The ACP HTTP/WS rows are the part the previous round did not cover end-to-end. Model/effort confirmation, wire retention across turns, model-only creation and rollback all behave the same on ACP as on REST. The 422 status survives the move to -32602 through data.httpStatus, as the new comment in dispatch.ts says.

Mutation check: are the new hunks load-bearing?

I edited the built head's dist, ran the full harness, then restored each file and confirmed the restore with cmp.

  • M1+M2: in routes/session.js and acp-http/dispatch.js, the rollback ignores err.sessionId. Exactly the 3 "no phantom" checks (REST, ACP HTTP, ACP WS) turn red, and the other 24 stay green. The new sessionId field on SessionStartupConfigError is the only thing that reaches the daemon-generated ID.
  • M3: in acp-bridge/session-startup-config.js, drop the /\p{Cc}/u check. Exactly the 3 S4 checks turn red. They now return 422 startup_config_rejected, not 400. The previous head, which has neither hunk, returned 500. So the [\s\S] fix in Session.ts makes a newline-bearing ID a caller refusal (422), and the boundary guard turns it into 400 before any session exists. I confirmed both hunks independently.

Gates at head (macOS)

Gate Result
acp-bridge session-startup-config.test.ts 33/33
sdk-typescript DaemonClient + DaemonClientStandalone + acpRouteTable 636/636
cli: 8 PR-touched serve/session test files 3034/3038 in the parallel run. The 4 failures were timeouts, all in non-startup server.test.ts cases. Run on its own, server.test.ts passed 1332/1332 in 5 of 7 runs. The other 2 runs had 1–2 intermittent failures. The one I captured by name, isolates webhook configs across workspace sources, already exists on main and is not part of this PR's diff. The PR's startup tests in that file (-t startup, 8 tests) passed every time.
integration-tests/cli/qwen-serve-routes.test.ts -t startup, real daemon on the bundled dist/cli.js 3/3. CI's "Integration Tests (CLI, No Sandbox)" lane skips this PR, so this is the local substitute.

Findings

No blockers.

One observation, not blocking (doc accuracy / transport parity): the new DaemonStartupConfigError JSDoc says:

one code-based catch covers both this local rejection and the daemon's 400 invalid_startup_config response

That holds on REST, where the body has body.code. Over ACP HTTP and ACP WS, the SDK's DaemonHttpError.body is { error, data: { errorKind, httpStatus } } and has no top-level code (measured, row "S3 error body shape" in image 01). A caller on a negotiated ACP transport needs body.data.errorKind. That body shape comes from synthesizeResponse in the ACP transports, which predates this PR and applies to every ACP error. Either of these would be enough:

  • a doc note naming both locations, or
  • a follow-up that mirrors errorKind into code in the ACP synthesized body.

Not covered

  • Real external providers. All traffic went to a controlled loopback fake.
  • Windows.
  • Fallback, reload, skill-override, cold-restore and transport-disconnect edges. The PR lists these as not exhaustively exercised.
  • Standalone creation over ACP. The SDK routes standalone through REST, and the previous round already covered standalone rollback and same-ID retry.
  • SDK preflight against an old daemon and composer-equivalence on the wire. The previous round verified both, and the delta doesn't touch either.

Reproduce

The harness is two self-contained scripts, and every PASS/FAIL line comes from a scripted comparison:

Steps:

  1. Create worktrees at 133ad52 and f497b74.
  2. In each, run pnpm install --frozen-lockfile && npm run build.
  3. Run node harness.mjs <worktree> <label>.
中文版

第 2 轮验证:PR #12308 在 head 133ad52 上的结果(macOS,REST / ACP HTTP / ACP WS)

结论:无阻塞问题,可以合并。 在当前 head 上,用真实 qwen serve daemon 加真实 SDK 跑三种 transport,27 项检查全部通过。同一套装置跑在上一轮验证过的 head f497b74 上,失败 8 项,每一项都对应此后新增的 4 个提交中的某一个。从构建产物里逐个删掉新增的 hunk,恰好只有它自己负责的检查变红。

  • 验证的 head:133ad5266a0871dbd64066957d1d3a145a5b9379,即发帖时的 PR head。与当前 main(90232f0)做 git merge-tree 无冲突。57 个评审 thread 全部已解决,CI 没有失败的 lane。
  • 环境:macOS arm64(Darwin 25.6),Node v24.18.1。PR 的 "Tested on" 表和上一轮(Linux aarch64)都没有覆盖 macOS 和 ACP HTTP/WS 端到端,本轮补上这两块。

自上次验证以来的变化

f497b74 → 133ad52 包含一次合并 main 和 3 个 PR 提交。不算合并,PR 自身的改动是 19 个文件,+370/−50,改了四处行为:

  1. daemon 生成 ID 时的回滚。 确定的 startup_config_rejected 现在带上被拒会话的 sessionId,所以调用方没有提供 ID 时,REST 路由和 ACP dispatcher 也会回滚持久记录。之前会残留一个能被列出、能被恢复的空会话。
  2. 模型 ID 中的控制字符。 modelServiceId 里含 /\p{Cc}/u 时,现在在入口处直接返回 400 invalid_startup_config。isCallerCausedModelRefusal 的正则也从 . 改成了 [\s\S]。
  3. ACP JSON-RPC 错误码。 startup_config_rejected 从 -32603(内部错误)改为 -32602(参数错误),data.httpStatus 仍然是 422。
  4. SDK 错误类型。 SDK 本地的格式错误拒绝改为 DaemonStartupConfigError extends TypeError,并带 code: 'invalid_startup_config'。

同场景 A/B:当前 head 对上一轮验证的 head

两个臂的装置相同:

  • 每个臂用各自 worktree 构建出的 packages/cli/dist 和 packages/sdk-typescript/dist。
  • daemon 使用隔离的 HOME/QWEN_HOME/QWEN_RUNTIME_DIR 和全新的工作区。
  • 回环假 OpenAI provider 记录每一个请求体。
  • 配置:一个支持思考的模型(efforts: [low, high]),一个不支持思考的模型,共享默认 reasoningEffort: 'none'。
  • 每个场景分别通过 REST、AcpHttpTransport、AcpWsTransport 上的 DaemonClient 跑一遍。
检查(未注明的均为三种 transport 各一次) 上一轮 f497b74 当前 133ad52
S1:{model, high} 返回 modelApplied: true 和规范的 startupConfigApplied ✅ ✅
S1:provider 实际收到的请求在第 1、2 轮都是 model=qwen-startup-test、reasoning_effort=high ✅ ✅
S2:无思考模型只选模型时,applied 只有 [modelServiceId],请求里没有 reasoning_effort ✅ ✅
S3:不带调用方 ID 的 'max' 被拒,返回 422 startup_config_rejected ✅ ✅
S3:被拒之后不残留幽灵会话(列表和持久记录中都没有) ❌ 每种 transport 各多出一个可列出的会话和一个 .jsonl ✅ 没有
S4:modelServiceId: "qwen-startup-test\n(openai)" 返回 400 invalid_startup_config ❌ REST 返回 500 -32603,错误消息原样带出换行符;ACP 返回 500 internal ✅ 三种 transport 都是 400
S4:不残留会话 ✅ ✅
S5(ACP HTTP,对 /acp 流做 tee 抓到的原始 JSON-RPC):startup_config_rejected 的错误码 ❌ -32603 ✅ -32602(invalid_startup_config 也是 -32602)
S6(SDK):本地格式错误拒绝是带 code: 'invalid_startup_config' 的 TypeError ❌ code 为 undefined ✅
S7:所有创建完成后,共享 settings.json 逐字节不变 ✅ ✅
合计 19 通过 / 8 失败 27 通过 / 0 失败

ACP HTTP/WS 这几行是上一轮没有端到端覆盖的部分。模型和思考档位的确认、跨轮次保持、只选模型、回滚,在 ACP 上的行为都和 REST 一致。错误码改成 -32602 之后,422 状态仍然通过 data.httpStatus 还原出来,和 dispatch.ts 新注释的说法一致。

变异检验:新增的 hunk 是否承重

我在当前 head 构建出的 dist 里做改动,跑完整装置,然后还原每个文件并用 cmp 确认还原成功。

  • M1+M2:在 routes/session.js 和 acp-http/dispatch.js 里,让回滚忽略 err.sessionId。恰好 3 项「不残留幽灵会话」检查(REST、ACP HTTP、ACP WS)变红,其余 24 项保持绿色。能拿到 daemon 生成的 ID 的只有 SessionStartupConfigError 上新增的 sessionId 字段。
  • M3:在 acp-bridge/session-startup-config.js 里删掉 /\p{Cc}/u 检查。恰好 3 项 S4 检查变红,返回的是 422 startup_config_rejected,而不是 400。上一轮的 head 两处改动都没有,返回的是 500。所以 Session.ts 里的 [\s\S] 修复让带换行的 ID 被识别为调用方拒绝(422),入口处的检查则在创建任何会话之前就把它拦成 400。两处改动我都分别验证过。

门禁(当前 head,macOS)

门禁 结果
acp-bridge 的 session-startup-config.test.ts 33/33
sdk-typescript 的 DaemonClient、DaemonClientStandalone、acpRouteTable 636/636
cli:PR 改动的 8 个 serve/session 测试文件 并行跑 3034/3038。4 个失败都是超时,全部在 server.test.ts 中与启动配置无关的用例里。单独跑 server.test.ts 共 7 次,其中 5 次是 1332/1332,另外 2 次有 1–2 个偶发失败。我抓到名字的那个是 isolates webhook configs across workspace sources,它在 main 上本来就有,不在这个 PR 的改动里。这个文件里 PR 的启动配置用例(-t startup,8 个)每次都通过。
integration-tests/cli/qwen-serve-routes.test.ts -t startup,真实 daemon 加打包后的 dist/cli.js 3/3。CI 的 "Integration Tests (CLI, No Sandbox)" 在这个 PR 上是跳过的,本地这次运行作为替代。

发现

没有阻塞问题。

一个观察项,不阻塞(文档准确性 / transport 一致性): 新增的 DaemonStartupConfigError JSDoc 写道:

one code-based catch covers both this local rejection and the daemon's 400 invalid_startup_config response

这在 REST 上成立,因为响应体里有 body.code。但在 ACP HTTP 和 ACP WS 上,SDK 的 DaemonHttpError.body 是 { error, data: { errorKind, httpStatus } },没有顶层 code(实测结果见图 01 的 "S3 error body shape" 行)。走协商出来的 ACP transport 的调用方需要读 body.data.errorKind。这个响应体形状来自 ACP transport 里的 synthesizeResponse,在这个 PR 之前就是这样,对所有 ACP 错误都适用。以下任一做法即可:

  • 在文档里写明两个位置;或
  • 后续 PR 在 ACP 合成的响应体里把 errorKind 同时写到 code。

未覆盖

  • 真实外部 provider。所有流量都走受控的回环假 provider。
  • Windows。
  • fallback、reload、skill 覆盖、冷恢复、transport 断连等边界。PR 自己也注明这些没有充分覆盖。
  • 通过 ACP 创建 standalone 会话。SDK 的 standalone 固定走 REST,上一轮已验证过 standalone 回滚和同 ID 重试。
  • SDK 对旧 daemon 的能力预检、与输入框行为在请求层面的一致性。上一轮已验证,本次增量也没有涉及。

复现

装置是两个独立脚本,每一行 PASS/FAIL 都是脚本里的比较结果:

步骤:

  1. 在 133ad52 和 f497b74 各建一个 worktree。
  2. 在每个 worktree 里运行 pnpm install --frozen-lockfile && npm run build。
  3. 运行 node harness.mjs <worktree> <label>。

Evidence

01: head 133ad52, 27/27 across REST / ACP HTTP / ACP WS
01

02: control arm, previous head f497b74, 19/8 (the 8 failures are this round's fixes)
02

03: mutation check. Each new hunk removed turns exactly its checks red, and the dist is restored afterwards
03

04: focused unit and integration gates at head
04

…onfigError JSDoc (#12308)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@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 round — PR #12308

One feedback item was newer than the last evaluation: @wenshao's verification-round report (ic:5831222985), verdict "no blockers; OK to merge", carrying one non-blocking observation. No inline review comments, no failed checks, no requested conflict resolution (--conflict false).

Feedback dispositions

  • [ic:5831222985] Doc accuracy / transport parity on DaemonStartupConfigError JSDoc — addressed (implemented). The JSDoc claimed one code-based catch covers both the SDK's local rejection and the daemon's 400 invalid_startup_config response. Verified the claim before editing: on REST the daemon replies { error, code } (packages/cli/src/serve/server/error-response.ts, isSessionStartupConfigError branch), so body.code holds; over ACP HTTP/WS the daemon puts the kind in the JSON-RPC error's data.errorKind (packages/cli/src/serve/acp-http/dispatch.ts) and the SDK transports synthesize the HTTP body as { error, data } (synthesizeResponse in packages/sdk-typescript/src/daemon/AcpHttpTransport.ts / AcpWsTransport.ts), so the code lives at body.data.errorKind — no top-level code. The observation is accurate. The maintainer offered two resolutions ("a doc note naming both locations" or "a follow-up that mirrors errorKind into code"); implemented the doc note. The alternative is a behavior change to the pre-existing synthesizeResponse path that applies to every ACP error, sits outside this PR's footprint, and was framed as a follow-up — declining it here per the either/or, not deferring, since the doc note fully resolves the observation as stated.

Changes

  • packages/sdk-typescript/src/daemon/session-startup-config.ts — JSDoc on DaemonStartupConfigError now names both locations: REST carries the code at body.code; the ACP transports synthesize the error body as { error, data } with the code at body.data.errorKind. Comment-only change (3 lines added); no runtime behavior, guard, or branch added, so no mutation probe applies.

No conflicts with main were merged or resolved (none requested).

Verification

  • npm run build — passed (exit 0). First attempt failed on infrastructure only: corepack could not mkdir /home/github-runner/.cache/node/corepack/v1 (EACCES; HOME is root-owned, process runs as uid 1000). Re-ran with COREPACK_HOME=/tmp/corepack-home; the full workspace build then succeeded, including the previously missing packages/acp-bridge/dist. An earlier pipe through tail masked this failure; all results below were re-run with real exit codes.
  • npm run typecheck — passed (exit 0).
  • npm run lint — passed (exit 0).
  • npx prettier --check packages/sdk-typescript/src/daemon/session-startup-config.ts — passed.
  • cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts — 484 passed / 484 (exit 0).
  • npm run generate:settings-schema — not applicable (no settings source changed); the build's built-in schema generation produced a byte-identical settings.schema.json (not listed by git status).
  • Integration tests — not applicable; the change is a JSDoc comment with no runtime behavior, so nothing is newly exercised through the bundled CLI or integration harness.
  • Pre-commit hook (prettier --write, eslint --fix --max-warnings 0) ran on the staged file during commit with no modifications.
中文说明

Autofix 本轮汇总 — PR #12308

比上次评估更新的反馈只有一条:@wenshao 的验证轮报告(ic:5831222985),结论为「无阻塞问题,可以合并」,其中带一个不阻塞的观察项。没有行内评审评论,没有失败的检查,也没有请求解决冲突(--conflict false)。

反馈处理结论

  • [ic:5831222985] DaemonStartupConfigError JSDoc 的文档准确性 / transport 一致性 — 已处理(已实现)。 该 JSDoc 声称一个基于 code 的 catch 可以同时覆盖 SDK 本地拒绝和 daemon 的 400 invalid_startup_config 响应。修改前先核实了该说法:REST 下 daemon 返回 { error, code }(packages/cli/src/serve/server/error-response.ts 的 isSessionStartupConfigError 分支),所以 body.code 成立;而 ACP HTTP/WS 下 daemon 把错误类别放在 JSON-RPC 错误的 data.errorKind 里(packages/cli/src/serve/acp-http/dispatch.ts),SDK 的 transport 又把 HTTP 响应体合成为 { error, data }(packages/sdk-typescript/src/daemon/AcpHttpTransport.ts / AcpWsTransport.ts 里的 synthesizeResponse),所以 code 位于 body.data.errorKind,顶层没有 code。该观察属实。维护者给出了两种解决方式(「在文档里写明两个位置」或「后续 PR 在 ACP 合成响应体里把 errorKind 同时写到 code」),本轮实现了文档注释这一种。另一种方案要改动早于本 PR 就存在的 synthesizeResponse 路径、影响所有 ACP 错误,超出本 PR 的改动范围,且维护者将其表述为后续工作 —— 按二选一处理,本轮不实现也不另立延后项,因为文档注释已完整解决该观察项。

改动

  • packages/sdk-typescript/src/daemon/session-startup-config.ts — DaemonStartupConfigError 的 JSDoc 现在写明两个位置:REST 把 code 放在 body.code;ACP transport 合成的错误响应体为 { error, data },code 位于 body.data.errorKind。仅注释改动(新增 3 行),不涉及任何运行时行为、守卫或分支,因此无需做变异检验。

未合并 main,也未解决任何冲突(无人请求)。

验证

  • npm run build — 通过(退出码 0)。第一次尝试仅在基础设施层面失败:corepack 无法创建 /home/github-runner/.cache/node/corepack/v1(EACCES;HOME 归 root 所有,进程以 uid 1000 运行)。改用 COREPACK_HOME=/tmp/corepack-home 后,整个工作区构建成功,包括此前缺失的 packages/acp-bridge/dist。此前一次通过管道接 tail 的运行掩盖了该失败;以下所有结果均以真实退出码重新运行。
  • npm run typecheck — 通过(退出码 0)。
  • npm run lint — 通过(退出码 0)。
  • npx prettier --check packages/sdk-typescript/src/daemon/session-startup-config.ts — 通过。
  • cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts — 484 通过 / 共 484(退出码 0)。
  • npm run generate:settings-schema — 不适用(未改动任何设置来源);构建内置的 schema 生成产出了逐字节一致的 settings.schema.json(git status 未列出)。
  • 集成测试 — 不适用;本次改动是 JSDoc 注释,无运行时行为,打包后的 CLI 或集成装置没有任何新行为需要覆盖。
  • 提交时的 pre-commit 钩子(prettier --write、eslint --fix --max-warnings 0)已对暂存文件运行,未产生额外修改。

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/模型 kimi-k3 · CLI 0.24.5

wenshao added a commit to wenshao/qwen-code that referenced this pull request Sep 25, 2026
@wenshao

wenshao commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Verification round 3: PR #12308 at head f4f5edd

Verdict: no blockers; OK to merge. The only change to the PR's own code since round 2 is the JSDoc fix for the round-2 observation. I re-ran the full round-2 harness on the new head because the main merge touched the PR's core files. The new head passes 27/27, and its output is line-for-line identical to 133ad52's, ignoring session IDs and ports. The mutation checks also give the same result as last round.

  • Verified head: f4f5eddeaae5368899d77fbff609cc6c3a382d75, which is the PR head at posting time. Environment: macOS arm64, Node v24.18.1, fresh worktree with pnpm install --frozen-lockfile && npm run build && npm run bundle.

What changed since 133ad52

  • 7820525, the PR's own change: 3 comment lines in DaemonStartupConfigError's JSDoc. It now says the daemon's code sits at body.code on REST and at body.data.errorKind on the ACP transports. That matches what this harness measures on the new head: rest body.code=startup_config_rejected, acp-http/acp-ws body.data.errorKind=startup_config_rejected (the "S3 error body shape" rows in image 05). The round-2 observation is resolved. Mirroring errorKind into code in synthesizeResponse was declined as out of scope. That's reasonable: the transport code predates this PR and applies to every ACP error.
  • f4f5edd, the merge of main: this brought in 74 commits. The merge tree equals git merge-tree c9a9a8a 7820525 byte for byte (4048f6d…), so no conflicts were resolved by hand. However, main changed 12 of the PR's files, including Session.ts, routes/session.ts, capabilities.ts and the SDK's DaemonClient.ts/types.ts. That's why I re-ran the behavioral checks instead of carrying round 2's result forward.

Results on f4f5edd

Check Result
Harness: real qwen serve + SDK over REST / ACP HTTP / ACP WS against a loopback fake provider. Covers model+high confirmation and wire retention over 2 turns, model-only creation, no phantom session after a rejection without a caller ID, control-character model ID → 400, raw ACP code -32602, the SDK DaemonStartupConfigError code, and settings.json staying unchanged. 27/27
Control arm 133ad52 (round 2's verified head), same harness, same machine 27/27; output identical to the new head's after replacing session IDs and ports
M1+M2: in the built routes/session.js and acp-http/dispatch.js, the rollback ignores err.sessionId exactly the 3 "no phantom" checks turn red (24/3); files restored and confirmed with cmp
M3: in the built acp-bridge/session-startup-config.js, drop /\p{Cc}/u exactly the 3 S4 checks turn red (422 instead of 400) (24/3); file restored and confirmed with cmp
acp-bridge session-startup-config.test.ts 33/33
sdk-typescript DaemonClient + DaemonClientStandalone + acpRouteTable 637/637
cli: 7 PR-touched serve/session files 1710/1710
cli server.test.ts, one run on its own 1335/1335
integration-tests/cli/qwen-serve-routes.test.ts -t startup on the bundled CLI 3/3

CI

All 57 review threads are resolved. At posting time, 24 checks pass, 2 are pending (web-shell E2E Smoke, review-pr), and 1 has failed: Integration Tests (no-AK, No Sandbox). That failure is infrastructure, not code. The job log shows runner ecs-qwen-hk4-18 got The runner has received a shutdown signal at 13:36:44, 71 seconds into the job, while npm run build was still running. No test ran. A rerun of that job should clear it. The PR's own integration tests passed locally above.

Not covered (unchanged from round 2)

Real external providers, Windows, fallback/reload/cold-restore/disconnect edges, standalone creation over ACP, and old-daemon preflight. The previous rounds covered the last two, and neither this round's PR commit nor anything the main merge brought in changes that behavior.

中文版

第 3 轮验证:PR #12308 在 head f4f5edd 上的结果

结论:无阻塞问题,可以合并。 自第 2 轮以来,PR 自身代码的唯一改动是针对第 2 轮观察项的 JSDoc 修正。因为合并 main 动到了 PR 的核心文件,我在新 head 上把第 2 轮的整套装置重跑了一遍。新 head 27/27 全过,输出和 133ad52 逐行一致(忽略会话 ID 和端口号)。变异检验的结果也和上一轮相同。

  • 验证的 head:f4f5eddeaae5368899d77fbff609cc6c3a382d75,即发帖时的 PR head。环境:macOS arm64,Node v24.18.1,全新 worktree,执行 pnpm install --frozen-lockfile && npm run build && npm run bundle。

自 133ad52 以来的变化

  • 7820525,PR 自身的改动:DaemonStartupConfigError 的 JSDoc 增加 3 行注释,写明 daemon 返回的错误码在 REST 上位于 body.code,在 ACP transport 上位于 body.data.errorKind。这和本装置在新 head 上的实测一致:rest body.code=startup_config_rejected,acp-http/acp-ws body.data.errorKind=startup_config_rejected(见图 05 中 "S3 error body shape" 这几行)。第 2 轮的观察项已解决。 在 synthesizeResponse 里把 errorKind 同步写到 code 的方案被判为超出本 PR 范围,没有采纳。这个判断合理:这段 transport 代码在本 PR 之前就存在,并且作用于所有 ACP 错误。
  • f4f5edd,合并 main:带入了 74 个提交。合并后的 tree 与 git merge-tree c9a9a8a 7820525 自动生成的结果逐字节相同(4048f6d…),没有手工解决的冲突。但 main 改动了 PR 涉及的 12 个文件,包括 Session.ts、routes/session.ts、capabilities.ts 以及 SDK 的 DaemonClient.ts/types.ts。所以我重跑了行为检查,而不是直接沿用第 2 轮的结论。

f4f5edd 上的结果

检查 结果
装置:真实 qwen serve 加 SDK,分别走 REST / ACP HTTP / ACP WS,provider 为回环假服务。覆盖:模型加 high 的确认及两轮请求中的保持、只选模型、调用方不带 ID 被拒后不残留幽灵会话、带控制字符的模型 ID 返回 400、ACP 原始错误码 -32602、SDK DaemonStartupConfigError 的错误码、settings.json 不被改动 27/27
对照臂 133ad52(第 2 轮验证过的 head),同一装置、同一台机器 27/27;把会话 ID 和端口号替换掉之后,输出与新 head 完全一致
M1+M2:在构建产物 routes/session.js 和 acp-http/dispatch.js 里,让回滚忽略 err.sessionId 恰好 3 项「不残留幽灵会话」检查变红(24/3);文件已还原并用 cmp 确认
M3:在构建产物 acp-bridge/session-startup-config.js 里删掉 /\p{Cc}/u 恰好 3 项 S4 检查变红,返回 422 而不是 400(24/3);文件已还原并用 cmp 确认
acp-bridge 的 session-startup-config.test.ts 33/33
sdk-typescript 的 DaemonClient、DaemonClientStandalone、acpRouteTable 637/637
cli:PR 改动的 7 个 serve/session 测试文件 1710/1710
cli 的 server.test.ts,单独跑一次 1335/1335
integration-tests/cli/qwen-serve-routes.test.ts -t startup,使用打包后的 CLI 3/3

CI

57 个评审 thread 全部已解决。发帖时 24 项通过,2 项仍在运行(web-shell E2E Smoke、review-pr),1 项失败:Integration Tests (no-AK, No Sandbox)。这是基础设施问题,不是代码问题。job 日志显示,runner ecs-qwen-hk4-18 在 13:36:44(job 开始 71 秒后)收到 The runner has received a shutdown signal,当时 npm run build 还在执行,一个测试都没有跑。重跑这个 job 应该就能通过。PR 自己的集成测试已在上面本地跑过。

未覆盖(与第 2 轮相同)

真实外部 provider、Windows、fallback/reload/冷恢复/断连等边界、通过 ACP 创建 standalone 会话、旧 daemon 能力预检。后两项之前几轮已验证过,本轮的 PR 提交和合并进来的 main 改动都没有影响这部分行为。

Evidence

05: head f4f5edd, 27/27 across REST / ACP HTTP / ACP WS
05

06: control arm identical after the main merge; mutations M1+M2 and M3 each turn exactly their checks red
06

07: focused unit and integration gates at f4f5edd
07

The harness is unchanged from round 2: harness.mjs, fake-provider.mjs.

@wenshao

wenshao commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Independent Critical-only review — head f4f5edde

Not approving, on budget alone. I want to be explicit that the recorded evidence here is as strong as any PR I have approved this week, and that the only thing missing is my own complete read of the diff.

What I verified myself

Nothing behavioural has changed since the last clean audit. Round 6 ran at 133ad526 (2026-09-24T13:48:40Z) with floor:"c" and returned total 0, CRITS 0, posted 0, fresh 0. Since then this PR has exactly one commit of its own, 78205258, which I checked file by file: one file, packages/sdk-typescript/src/daemon/session-startup-config.ts, +3/-0 — a JSDoc addition naming both transport error-code locations. No production logic changed. Head f4f5edde is a merge of main into the feature branch, and the 300-file delta between the two is upstream movement (76 merged PRs), not this PR's work.

The function that produced three separate Criticals is correct at head. session-startup-config.ts's rejectInvalidSelection was the anchor for R2-14 (only JSON-RPC -32602 mapped, an unregistered startup option falling elsewhere), R2-4 (a -32603 arm reclassifying every child-side internal error, including credential failures, as caller-caused) and R4-1 (a definite startup rejection unable to name the recording it should roll back). At this head:

if (error === null || typeof error !== 'object' || !('code' in error)
    || !('message' in error) || typeof error.message !== 'string') {
  throw error;
}
// Only a deterministic parameter rejection is a definite, client-caused
// refusal … Internal errors (-32603) also carry auth, credential,
// timeout and transport failures — uncertain outcomes that stay unmapped.
if (error.code === -32602) {
  throw new SessionStartupConfigError('startup_config_rejected', error.message, sessionId);
}
throw error;

Only -32602 becomes the definite client-caused refusal. -32603 propagates unchanged, which is the correct answer for a code that also carries auth, credential, timeout and transport failures — mapping it would report an uncertain daemon-side outcome as the caller's fault. A malformed error (not an object, missing code or message, non-string message) is rethrown untouched rather than being coerced into a refusal. The function is typed never and every path throws, so no caller can mistake a rejection for success. Both setSessionConfigOption calls route their failures through it.

The record is clean. All 57 review threads are resolved with none outstanding. wenshao approved at this exact head f4f5edde at 2026-09-25T16:04:08Z. CI is green at head with only review-pr in progress, which is not a gate. Rounds 1-5 filed 1, 4, 2, 2 and 2 Criticals, and the ledger demonstrably carried standing blockers forward rather than dropping them — round 2 restated R1-1, round 3 carried R2-4, round 4 carried R4-2 into R5-1 — so round 6's zero is evidence those eleven closed, not evidence the round skipped them.

Why I am still not approving

My standard for an approval is that I have read the diff, and I have not. This is 3,084 additions across 42 files, and beyond rejectInvalidSelection I read only the commit-level metadata and the normalizer's modelServiceId.trim(). Unread by me at head: standalone-session-service.ts (R1-1's site — a failed startupConfig preparation reporting a definite creation failure), session-control-plane.ts around the POST /session path where R2-13 found a rejected startupConfig closing a live channel, acp-http/dispatch.ts where R3-1 found the session/new mapping never rolling back a persisted recording, bridgeTypes.ts and sdk-typescript/src/daemon/types.ts where R4-2/R5-1 turned on the modelApplied presence rule, Session.ts where R5-2 found the caller-caused-versus-daemon-fault split hand-rolled from core's human-readable text, the published approvalMode enum and protocol sentence behind R2-11/R2-12, and the control-character fail-closed validation that 133ad526 itself added.

Those are precisely the places this PR's own history concentrated, and several of them are the class where a wrong answer is silent — a presence rule that mis-states when a field appears, or a fault split that blames the caller for a credential failure. I am not willing to certify them on the strength of an audit round I did not run, even one I have good reason to trust, when I have two minutes left and a merge head.

I am applying the same standard I applied to #9661 earlier today, where I also declined to approve a diff whose latest audit round was clean at the head and whose threads were all resolved. The difference here is that I did verify the single highest-risk function myself, and that the post-audit delta is provably documentation — so this is a narrower gap than that one, and it should not be read as doubt about the code.

CI

Green at head with review-pr in progress; no failure attributable to this diff, and CI state is not the basis for this verdict either way.

Verdict: COMMENT — No Critical found, none recorded open, and the one function I read is correct. The outstanding gate is an independent Critical-only pass over the remaining ~3,000 lines, concentrated in the six files named above. Next step: re-request review at this head with budget for a full pass; on the recorded evidence — a zero-finding audit at a behaviourally identical commit, a maintainer approval at this head, and 57 of 57 threads resolved — this PR is close to the line and needs a reader, not a fix.

@wenshao

wenshao commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@callmeYe
callmeYe added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit be1a0a1 Sep 25, 2026
100 of 102 checks passed
wenshao added a commit to doudouOUC/qwen-code that referenced this pull request Sep 25, 2026
…s-p0-p8

Brings in main through 53afcb6: QwenLM#12308 (model and reasoning selection
at session creation) and QwenLM#12693 (durable Managed Session journal and
failover), the latter split from this branch.

QwenLM#12308 merges as is. Its startup configuration rides the branch's spawn
path, including the deferred start used with paired execution engines.

From QwenLM#12693, the review fixes are taken:
- a harness-requested event of any kind needs an activation subject,
  while a trusted entry's message.committed does not; the commit digest
  rejects an empty or oversized event list before encoding;
- sealing a Managed lease checks the commit proof before it reads the
  transcript;
- title and source reads skip the Managed probe for a transcript already
  known to be legacy, and prefer the committed Managed record otherwise;
- legacy rename, fork, recording and TUI resume refuse a Managed
  transcript up front; a restore reports an engine refusal as -32024
  with the Session id, and the daemon answers it with 409.

Where the branch already enforces the same rule, its implementation
stays: the event and commit digests keep the Managed Tool canonical
encoder (same bytes; main's pinned digest still matches), the actor
check keeps its harness-subject helper, and loadCliConfig applies main's
up-front legacy check only to a legacy host, since a Managed host
restores through its own execution engine check.
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.

6 participants