全解析:daemon 级 admission 契约与 REST/ACP/SDK 实践)
qwen-code 调用方指定会话 IDCaller-Supplied Session ID全解析daemon 级 admission 契约与 REST/ACP/SDK 实践【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code本文深入剖析 qwen-code daemon 的调用方指定会话 IDCaller-Supplied Session ID能力它允许客户端在创建会话前自主选定会话 ID从而把会话身份与自身工作流状态原子化地持久化。文章从设计契约、UUID 校验规则、daemon 级冲突准入admission、REST/ACP 双传输行为、SDK/MCP 能力协商到错误契约逐一展开并辅以仓库源码与单元测试佐证。读完你将掌握如何通过POST /session { sessionId }、ACPsession/new._meta[qwen-code/sessionId]及 TypeScript/Java SDK 安全地指定会话 ID理解冲突检测、运行时替换与跨工作区唯一性保障的底层原理。一、背景为什么需要创建前就定好 IDdaemon 的客户端有时需要在会话真正创建之前就确定会话 ID——这样它们可以把会话身份与自身工作流状态原子化地一起持久化。例如一个自动化流水线在记录本次任务对应哪个会话时如果 ID 是创建响应里才返回的那么先写状态、后拿 ID与先拿 ID、后写状态之间总存在竞态窗口。在引入本设计之前REST 实现虽然已经转发过可选 ID但唯一性与恢复协调只局限在某一条路由、某一个工作区运行时内ACP、各语言 SDK、运行时替换runtime replacement以及直接 stdio agent 入口观察到的行为各不相同。本设计把调用方指定 ID提升为daemon 全局统一契约同时不改变核心会话格式、也不引入持久化的全局索引。对应的设计文档见 docs/design/2026-08-01-caller-supplied-session-id.md核心实现位于 packages/cli/src/serve/session-id-admission.ts。二、契约可选字段与严格的 UUID 校验2.1 字段语义调用方指定 ID 是可选的undefined与null表示未提供 ID。提供的值必须是字符串形式的 RFC 变体 UUID v1–v5。daemon 会将其统一规范化为小写。明确拒绝nil UUID、不支持的版本号、非 RFC 变体、路径字符、Arena-agent-*后缀、以及一切非字符串值。2.2 内部验证器与调用方验证器的差异内部会话 ID 验证器继续接受既有的 Arena 后缀即-agent-*形式而调用方验证刻意更窄。原因是公开 ID 必须能被已持久化会话与CLI resume路径寻址到因此调用方 ID 必须是内部 ID 的严格子集。源码中这两套正则非常直观见 packages/cli/src/config/session-id.ts// 内部 ID允许可选的 -agent-* Arena 后缀 const INTERNAL_SESSION_ID_REGEX /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(-agent-[a-zA-Z0-9_.-])?$/i; // 调用方 ID严格 RFC UUID v1-v5无后缀 const CALLER_SUPPLIED_SESSION_ID_REGEX /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;正则中的[1-5]限定版本号 1–5[89ab]限定变体位共同保证了RFC 变体 UUID v1–v5这一约束。而normalizeSessionIdForLookup只在命中调用方正则时才做小写化内部 Arena ID 与历史遗留 ID 保持原有拼写不变packages/cli/src/config/session-id.ts。解析入口parseCallerSuppliedSessionId返回三态结果absent | invalid | validundefined/null为absent非字符串或正则不匹配为invalid合法则返回规范化后的小写 ID同文件第 34–45 行。2.3 指定 ID不等于幂等附加提供一个 ID 的含义是用这个 ID 创建一个全新的独立线程会话thread session而不是幂等的 attach 操作。因此在创建响应出现歧义例如网络中断、超时之后调用方应当用已知 ID 走load / resume路径来恢复状态而不是再次 create。三、所有权边界各层职责设计文档用一张职责表明确了各层的分工边界这也是理解整个架构的钥匙层职责共享解析器公开 UUID 校验与小写规范化内部 Arena 兼容性保持独立RequestedSessionIdAdmissiondaemon 全局的 live / pending / persisted 三方冲突检测create 与 restore 共用REST 与 ACP 分发器解析协议字段、副作用前获取准入、错误映射、校验返回的 IDACP bridge强制直接调用方指定的 create 进入 thread 作用域、转发 ID、纳入 fresh-session 容量准入stdio ACP agent访问设置/文件系统前先校验、同 ID 启动串行化、出错时保留共享子进程SDK 与 MCP 客户端协商session_id_override能力、序列化字段、校验成功响应容量准入与 ID 唯一性刻意分离既有的总会话数准入控制器只负责容量与 drain 状态ID 唯一性由 admission 组件独立负责。这样两个策略不会误释放或误计数对方的 reservation。四、Daemon 级准入机制Admission4.1 内存状态模型admission 组件持有一张以规范化会话 ID 为键的内存 Map见 packages/cli/src/serve/session-id-admission.tsclaim 分为两类create由某一个 bridge 世代独占restore由某一个 bridge 世代持有带引用计数允许该世代上的并发 load/resume 调用共享同一次恢复。4.2 create 的五步流程枚举 daemon 提供的每一个 live bridge包括正在 draining、以及尚未完成关闭的被替换世代getBridges()拒绝任何 live owner 或 pending claim在第一次异步持久化、branch 或 worktree 操作之前同步安装 pending create claim——这保证了即便后续扫描失败同一 ID 也无法被并发请求再次抢占在会话归档协调器SessionArchiveCoordinator的共享锁下扫描每一个当前已注册的工作区每个工作区用其运行时捕获的sessionRuntimeBaseDir固定一个SessionService实例若发现active、archived 或 worktree 支撑的持久化历史则拒绝否则返回一个绑定身份的 reservation。其中第 4 步的持久化存在性检查requestedSessionIdPersistenceExists同文件第 72–98 行会做两件事用findSessionIdIgnoringCase做大小写不敏感的查找并把SessionIdCaseConflictError也视为占用即仅大小写不同的 transcript 也算冲突随后对active、archived两种归档状态的 worktree 路径做access探测非ENOENT错误向上抛出。4.3 restore 语义restore不做磁盘扫描因为它的目的是打开既有历史。它的规则是拒绝其他 bridge 世代上的 live 或 pending owner仅当已存在的 restore claim属于同一个 bridge 对象时才共享同 bridge 上不同调用之间工作区拼写可能不同否则安装一个新的 restore claim。4.4 幂等释放与陈旧释放防护Reservation 是幂等的只有当 Map 中仍是它捕获的那个精确状态对象时才删除状态。这个身份比对防止了一次延迟的 release 误删同一 ID 上更新的 claim。restore 引用逐个递减归零时移除状态同文件第 212–229 行的createReservation。4.5 失败语义fail closedbridge 枚举或持久化检查出错时fail closed返回可重试的session_id_admission_unavailable普通的SessionNotFoundError只表示该 bridge 不拥有此 ID不属于失败持久化读取失败不继承SessionService的读错当存在行为扫描会把它们暴露为可重试的503 session_id_admission_unavailable而不是409冲突客户端应限制 503 重试次数——一个永久不可读的 transcript 目录会持续返回 503永远不会自行恢复。测试套件 packages/cli/src/serve/session-id-admission.test.ts 覆盖了这些语义包括扫描完成前同步 claim第 117 行、检查每一个 live bridge 与每一个注册的持久化目标第 144 行、仅大小写不同的 transcript 视为持久化占用第 227 行、restore claim 仅在同一 bridge 世代共享第 251 行、混合大小写 UUID 视为同一 restore claim第 290 行、陈旧 release 不删除新 claim第 357 行、扫描失败返回可重试 unavailable 并释放第 384 行、bridge 不可枚举时 fail closed第 469 行。五、运行时替换与工作区范围5.1 动态 bridge provider生产环境向 admission 传入一个由运行时生命周期数组支撑的动态 bridge provider。被替换的 bridge 在确认关闭之前一直留在数组中因此当旧世代还在 draining 时新世代无法创建同一个 ID——从机制上堵死了替换窗口内的重名。启用运行时替换/移除runtime replacement / removal时若没有该 provider则属于启动错误。在 packages/cli/src/serve/server.ts 中可以看到生产装配getBridges来自workspaceRegistry.listManaged().map(runtime runtime.bridge)getPersistenceTargets使用每个运行时的sessionRuntimeBaseDirgetBridgeWorkspaceId则用于在冲突响应中标注外部 owner 的工作区 ID。5.2 跨工作区唯一性持久化扫描使用当前工作区注册表与每个运行时固定的 base 目录不依赖环境中的存储上下文。这保证了对当前注册在 daemon 下的所有工作区而言 ID 是唯一的同时避免了引入新的全局磁盘索引。历史重复数据不会被迁移或重命名如果某个包含历史重复 ID 的工作区稍后才被注册workspace 限定的路由workspace-qualified routing继续负责消歧admission 的保证仅相对于准入时刻已注册的运行时成立。六、传输行为REST 与 ACP6.1 请求形态RESTPOST /session { sessionId }——在 packages/cli/src/serve/routes/session.ts 中路由先parseCallerSuppliedSessionId(body[sessionId])invalid立即返回400 invalid_session_id并给出示例550e8400-e29b-41d4-a716-446655440000valid则调用requestedSessionIdAdmission.reserveCreate(...)错误经sendRequestedSessionIdAdmissionError映射。ACPsession/new._meta[qwen-code/sessionId]。两者使用同一个 admission 实例包括主 ACP 挂载与 workspace 限定的 ACP 挂载REST 与 ACP 的 load/resume 也共享 restore claim从而关闭了跨传输竞态。6.2 强制 thread 作用域与 ID 核实两条 create 路径都会强制sessionScope: threadACP 分发器在 packages/cli/src/serve/acp-http/dispatch.ts 附近无论客户端参数如何都固定发送sessionScope: thread。bridge 返回后分发器比对实际 ID 与请求 ID不一致返回session_id_not_honored并在释放准入前删除新建的 live 与持久化孤儿避免留下脏状态。6.3 stdio ACP agentstdio agent 在加载设置之前重复校验。它用每个子进程的 pending 集合守卫指定 ID 的 create与非 live 的 load/resume 启动重复启动返回结构化的 ACPINVALID_PARAMS错误绝不退出进程、不损害兄弟会话核心Config保持不变仍接收throwOnSessionIdConflict作为最终的磁盘冲突防线。七、公共兼容性与 SDK 行为7.1 能力协商capability gatedaemon 对外宣告session_id_override能力见 packages/cli/src/serve/capabilities.ts{ since: v1 }。TypeScript、Java 与 daemon MCP 客户端在发送带指定 ID 的 create 变更之前必须先确认该能力防止旧版 daemon 静默忽略新增字段。7.2 各 SDK 的字段映射SDK / 客户端映射方式TypeScriptCreateSessionRequest.sessionId按活动传输映射为 REST JSON 字段或 ACP_meta发送前requireCapability(session_id_override)packages/sdk-typescript/src/daemon/DaemonClient.tsJavaCreateSessionRequest.Builder.sessionId(String)发送前检查capabilities.supports(session_id_override)MCPsession_create.session_id7.3 成功响应的二次校验每个 SDK 都会对成功响应再做一次检查TypeScript校验响应中的sessionId与请求一致Java请求 ID 会被toLowerCase(Locale.ROOT)后与返回 ID 比对不一致时抛出SessionCreationOutcomeUnknownException——因为意外会话可能已被创建packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/daemon/DaemonClient.java同样Java 在 IO 异常、传输异常以及歧义突变状态isAmbiguousMutationStatus下也会抛出SessionCreationOutcomeUnknownException同文件第 184–199 行语义是创建结果未知请用 ID 走 load/resume。Web UI 消费方继承 TypeScript 的可选字段但不主动设置Python SDK 没有 daemon 客户端保持不变。八、错误契约设计文档给出了完整的 REST / ACP 错误映射表这是客户端落地时必须对齐的契约条件RESTACP请求 ID 非法400 invalid_session_idINVALID_PARAMSdata.httpStatus400create 与 live / pending / persisted 状态冲突409 session_id_conflictINVALID_PARAMSdata.httpStatus409restore 属于另一运行时世代409 session_workspace_conflictINVALID_PARAMSdata.httpStatus409无法检查 live 或持久化所有权503 session_id_admission_unavailableretryable: trueinternal errordata.httpStatus503retryable: true下游返回了不同 ID500 session_id_not_honoredinternal errordata.httpStatus500admission 层的错误类型在源码中被建模为RequestedSessionIdAdmissionError其code精确对应三种错误码session_id_conflict、session_workspace_conflict、session_id_admission_unavailabledetails中携带conflict种类live | pending | persisted、工作区路径与 ID、以及retryable标记packages/cli/src/serve/session-id-admission.ts。九、被否决的替代方案设计文档明确记录了两个被否决的方向理解它们有助于把握最终取舍持久化的 daemon 全局 ID 索引会让未来工作区注册更易推理但引入了事务性恢复、迁移与陈旧条目清理——而这一特性本就可以通过当前 live bridges 既有会话存储强制实现因此拒绝。每路由独立 Map局部更小但无法关闭 REST/ACP 或跨工作区竞态拒绝。把 create 视为幂等 attach会掩盖歧义的变更结果并与 ACPsession/new的语义冲突拒绝。十、验证体系10.1 单元覆盖清单单元测试覆盖见 packages/cli/src/serve/session-id-admission.test.tsUUID 版本与变体、规范化、Arena 兼容性同步 claim扫描未完成即拒绝并发全部 live bridge 世代与固定的持久化目标pinned persistence targetsrestore 引用计数、失败释放、陈旧释放结构化 stdio 错误、作用域强制scope forcing、孤儿清理能力门控、传输映射、SDK 响应校验。10.2 手工 daemon 场景设计文档给出的端到端手工验证流程通过raw REST、TypeScript REST/ACP、Java、MCP分别创建混合大小写的固定 ID对胜出会话发起 prompt 并持久化重启 daemon 并 restore该会话验证跨工作区与跨传输的冲突均被正确拒绝确认非法的直接 ACP metadata 既不创建文件也不终止共享子进程。结语调用方指定会话 ID 是 qwen-code daemon 为客户端原子化持久化会话身份场景提供的一项全局契约。它用一套内存级、同步安装 claim、幂等释放的 admission 机制在不引入全局磁盘索引的前提下同时保障了跨工作区、跨 REST/ACP 传输、跨运行时世代的 ID 唯一性再辅以能力协商、返回 ID 二次校验与完整错误映射让 TypeScript、Java、MCP 各端客户端都能安全可靠地使用该能力。无论是编排流水线预登记会话身份还是构建需要先定 ID 再落状态的工作流都可以直接对照本文的契约与源码路径落地。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考