Qwen Code 多守护进程共享会话:Relaxed Standalone Daemon Ownership 设计与实现解析
2026/9/15 11:49:11 网站建设 项目流程

Qwen Code 多守护进程共享会话:Relaxed Standalone Daemon Ownership 设计与实现解析

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

导读

在 Qwen Code 的qwen serve架构中,Conversations 运行时(承载 Standalone 会话、Live Voice 与定时任务)过去由"用户级全局进程 owner"把持,导致 Web Shell 连到第二个守护进程时即使只是浏览会话目录也会收到503 conversation_runtime_in_use。本文基于设计文档 2026-09-02-relaxed-standalone-daemon-ownership.md,系统讲解 Qwen Code 如何通过"取消进程级 owner + 强制会话级 writer lease + 本地身份域限定回收 + Live 精确发布者准入"四层机制,实现多个守护进程在同一用户、同一台机器上并发服务不同会话的会话分区并发契约(session-partitioned concurrent-use contract)。读完本文,你将掌握该机制的完整行为契约、错误码语义、源码实现落点以及升级回滚注意事项。

背景阅读:该设计是 standalone-daemon-sessions.md(Issue #8908)中"跨守护进程拥有权"要求的演进,其落地与验证追踪见 2026-09-06-conversations-ownership-cutover.md。

背景与动机:为什么不能保留全局 owner

在旧模型下,qwen serve进程在处理 standalone 或 Live 请求前,会先获取一个用户全局的进程 owner 记录conversations/runtime-owner.json,内含versionpidinstanceNonce)。这带来一个实际问题:

  • 第一个碰过 standalone/Live 的守护进程会阻塞其他所有存活守护进程的 standalone 访问;
  • 连接在第二个守护进程上的 Web Shell 即使只需要共享目录、新建聊天或操作另一个会话,也会收到503 conversation_runtime_in_use

而普通 workspace 并没有这种进程级 runtime owner——多个守护进程可以同时指向同一个持久化 workspace。设计文档因此主张更简单、更一致的策略:让每个守护进程独立服务各自的会话,把真正共享写入的边界交给既有的跨进程会话 writer 协议

核心决策:从进程级 owner 到会话级 lease

本设计做出的核心决策是:

允许每个更新的qwen serve进程在 legacy 兼容检查通过后,惰性地为共享 Conversations 根目录创建自己的 Conversations 运行时;守护进程在服务 standalone API 前不再获取用户级进程 owner;每个 standalone 会话在写入前必须获取既有的跨进程会话 writer lease。

对齐效果是:standalone 启动方式与普通 workspace 启动方式一致——多个守护进程可指向同一持久化 workspace,但各自保留自己的 ACP bridge、子进程、live-session 索引、缓存与 runtime generation。

需要特别强调的是,这是会话分区并发使用契约,而非无限制的多主支持

  • 更新的守护进程可以并发列出共享目录、创建会话、承载不同的会话 ID;
  • 同一会话的竞争写入者与生命周期变更仍由该会话的 writer lease 串行化;
  • 本变更新增跨守护进程路由、共享 live-state 索引、原子多会话操作或分布式缓存失效。

Live 激活仍然是一个"机器全局例外":只有 PID 与 instance nonce 精确匹配稳定 Live 发现记录的守护进程才能启动或替换 Live 通话;其他更新的守护进程仍可服务 standalone 会话,但其/live/start/live/new请求继续返回可重试的503 conversation_runtime_in_use,Host 发起的 toggle/new 动作也会在通话激活前被拒绝。

行为契约详解

设计文档给出了一条完整的可观察行为清单,核心条款如下:

  • GET /standalone/session-options与所有/standalone/sessions路由可以在另一个更新守护进程存活时初始化本地 Conversations 运行时。
  • 更新后的守护进程不再仅因对方进程存活而产生conversation_runtime_in_use;迁移期间,若存在存活的 legacy runtime-owner 记录,仍返回该错误直到旧守护进程退出。
  • 若守护进程 A 已加载会话 S,守护进程 B 仍可列出共享目录、新建 standalone 会话,并可创建/恢复/使用未被其他 writer 持有的另一个会话。不同会话 ID 可以同时在多个守护进程中处于活跃状态。
  • 客户端始终附着在它创建或恢复活跃会话的那个守护进程上;prompt、cancel、permission、status、heartbeat、detach、SSE 事件路由不会转发到其他守护进程。
  • 创建或恢复 standalone 会话时无条件获取会话 writer lease(与用户experimental.sessionWriterLease设置无关);第二个守护进程尝试打开同一活跃会话时收到既有的409 session_writer_conflict。单会话重命名/修复使用同一顶层响应;批量生命周期路由保持200结果外壳,但在受影响条目的errors[]中保留 writer 错误类型。
  • 由 Conversations 运行时承载的所有其他会话(包括 Live 与定时任务会话)同样强制使用 lease,防止后台 keepalive 或任务再水合静默恢复已活跃的 transcript。
  • Live 发现与 Live 激活保持单发布者;非发布者守护进程的/live/start/live/new与 Host 发起动作全部失败。
  • 未密封的活跃锁仅当新守护进程能证明记录属于同一本地身份域(同一 hostname;Linux 上还要求同一 boot 与 PID namespace)且记录的进程已退出或 PID 被复用时才可回收;存活进程(包括停滞进程)永远被 fenced。
  • lease 持有者是 ACP writer 进程本身,而非其守护进程父进程。若守护进程父进程死亡但子进程以匹配身份存活,其他守护进程仍被 fenced,直到该 writer 退出或被终止。
  • 持久化会话切换到另一守护进程的正常路径是协作式 writer 交接:显式关闭会话或正常 idle reap 释放 lease;优雅的受管守护进程关机会密封它,使继任者走既有的 certified-takeover 协议。非协作退出是"身份限定回收"这一狭窄例外。两条路径都执行冷恢复(cold restore),而非热迁移。
  • 守护进程只有在解析到相同 Conversations 根与 runtime base 时才共享持久化 standalone 会话;自定义 runtime base 保持与普通 workspace 相同的存储隔离。
  • 支持的拓扑:一台机器上、同一 OS 用户下的多个守护进程。跨物理机器共享稳定状态基(存放 Live locator、legacy owner 工件、删除日志)或共享 runtime 基(存放 transcript 与 writer 锁)不在契约之内——这是承载性边界而非装饰性说明:能被多台机器访问的稳定基会把 Live locator 从"机器全局选举"变成"文件系统全局选举",且 Darwin/Windows 的回收规则没有 boot/namespace 分量来区分共享同一 hostname 的两台主机。
  • 跨进程目录变更通过既有持久化会话缓存最终可见;live state 只从接收方守护进程合并,因此守护进程 B 可能把 A 中活跃的会话列为"已持久化但未激活",直到恢复尝试返回session_writer_conflict
  • 源隔离不变:standalone 的列出、恢复、生命周期路由只接受顶层 standalone 记录,对 Live-owned 或其他外来记录保持 fail closed。
  • 并发后台清理同一删除日志条目时,由该会话的生命周期 writer lease 串行化;遇到session_writer_conflict的扫描把该条目视为由另一守护进程处理,跳过该 UUID 继续,不把争用转换为 compromised-record 结果。
  • 两个守护进程同时再水合同一定时任务绑定会话时,强制 writer lease 只允许一个驻留会话;失败的再水合通过既有再水合结果与错误上报路径记录恢复失败,然后按既有 keepalive 退避,且不会触发该会话的任务。

同会话排他性覆盖完整的活跃会话生命周期,而非仅重叠的 HTTP 请求:只要会话仍加载在守护进程 A 中,即便 A 在请求间隙空闲,也不能通过守护进程 B 继续或变更它。

保留的安全基线

移除进程 owner 并不会把 Conversations 根目录降级为普通用户选择的 workspace。实现仍保留:

  • 精确的 Conversations-root 校验与目录身份检查;
  • 内部运行时隔离与禁止回退到主运行时(primary-runtime fallback);
  • 每守护进程 runtime generation、活动排空与终末隔离(terminal quarantine);
  • 各守护进程内的每会话生命周期协调;
  • 持久的 standalone 删除日志与恢复检查;
  • 生命周期变更既有 writer lease;
  • 整个 Conversations 运行时的强制活跃会话 writer lease;
  • 可证明同域陈旧活跃锁的身份限定回收;
  • 迁移期 legacy runtime-owner 兼容检查;
  • Live discovery 的单发布者记录与校验;
  • 只接受精确稳定 locator 发布者的 Live-start 准入。

守护进程本地的创建准入、live owner 索引、生命周期协调器、回收 singleflight 与缓存失效都不是跨进程权威——它们可能让远端 live state 看似不活跃或延迟目录新鲜度,但不决定写入所有权。工作目录身份 pin 仅在其匹配的本地 bridge 会话 generation 驻留期间、或在该生命周期操作获取会话 lease 后的一次变更内具有权威性;没有匹配本地 generation 的 pin 会在再次检查目录前被丢弃,且不会把另一守护进程安全重建的目录误判为working_directory_compromised

实现剖析

设计文档用一张表明确了三个行为门(gate)及其替换方案:

ConsumerReplacement
Standalone 访问共享运行时无进程级门;会话 writer 与生命周期操作使用强制 writer lease
定时任务激活绑定会话的强制 writer lease,加上下述"未绑定任务资格与绑定事务"
机器全局 Live 激活每次 Live 启动路径前的精确稳定 locator 发布者准入

同时,acquire 的状态目录引导(bootstrap)与稳定 Live 交接副作用被分别移到删除日志与 Live 发布路径;没有任何 consumer 再隐式依赖被移除的 lifetime owner。

1. 外层 owner 替换为 legacy 兼容检查

ConversationRuntimeManager不再接受长期存活的ConversationRuntimeOwnership依赖,也不再在ensure()中调用acquire()。在创建第一个本地 Conversations 运行时之前,它只调用只读的 legacy runtime-owner 兼容检查;运行时创建仍会在发布本地受管运行时前重新校验精确根目录。

从源码看,conversation-runtime-manager.ts 的ensureOnce()在未发布 runtime 时首先执行this.options.checkLegacyOwner()(第 114 行),之后才revalidateRoot()并发布;而 conversation-runtime-ownership.ts 导出的checkLegacyConversationRuntimeOwner()复用了 owner 目录身份检查、目录锁(.runtime-owner.lock)、严格 version-1 记录解析、PID 存活规则、精确记录清理、持久化 sync 与交接宽限期。其语义为:

  • 有效记录且 PID 仍存活 →conversation_runtime_in_use
  • 有效陈旧记录 → 在目录锁下移除并重新检查后继续;
  • 畸形、不安全或不确定状态 → 保持既有的 compromised / unavailable 失败。

该检查从不写入新的 owner 记录,也不参与 Live discovery 交接;它是该守护进程 generation 的一次性检查,运行时发布后不再重复(因此无法探测之后才创建的 legacy owner 记录——设计文档在"排空并切换"(drain-and-cutover)要求下接受该局限,而不是引入 owner 轮询或运行时只读降级)。createServeApp只构造兼容检查器,不再向ServeAppLifecycleController挂接 Conversations owner——生命周期控制器在关停后不再有 Conversations 拥有权需要释放。

2. 强制 Conversations 运行时的 writer lease

强制点选在**运行时来源边界(runtime-provenance boundary)**而非会话源边界:

  • 当守护进程构建验证来源为live-conversation的 workspace 运行时,其 bridge 会向该运行时的 ACP 子进程环境添加一个私有、仅启用的 marker;primary、secondary、scratch 与其他普通 workspace bridge 不接收它。
  • CLI 入口点在首次 await 或加载环境文件之前捕获并删除该私有 marker(与既有私有父能力并列)。它仅在 ACP 模式、能力存在且值精确匹配启用值时接受;sandbox 重启动会随私有能力一起携带已接受的 marker,普通重启动则不会。入口点把结果作为内部布尔值传给runAcpAgent,而不是让 agent 读取可变进程环境。
  • runAcpAgent把该布尔值并入既有的进程启动 writer 快照:有效值为"受信 runtime marker 被接受"或"用户启动设置启用 lease"二者之一。每次请求的设置重载继续使用该冻结值,因此一个 ACP 进程不会混用 leased 与 legacy writer
  • 对于携带 Conversations marker 的受信受管子进程,runAcpAgent设置reclaimPolicy: 'local'并保留takeoverPolicy: 'certified';其他受信受管 workspace 子进程保持reclaimPolicy: 'never'。源码证据见 acpAgent.ts(conversationsRuntimeProvenance布尔同时驱动强制 lease 与回收策略选择,如第 14476-14480 行附近this.conversationsRuntimeProvenance ? 'local' : 'never')。
  • 守护进程的共享子进程环境覆盖默认移除marker,然后live-conversationbridge 把未定义值替换为启用值;同时把该键加入硬编码的项目环境排除列表,防止 workspace.env或设置重载重新引入它。CLI 入口点在初始设置与环境加载后再次删除该键,防止用户级.env值泄漏到工具或后续子进程。
  • 这使 standalone独立于用户配置:不引入公开设置或命令行标志;marker 限定在一个 bridge 上,从而覆盖专用运行时承载的所有来源(standalone、Live、定时任务 controller 与 run 会话),因为基于 source 的覆盖会漏掉持久化 source 非standalone的后台会话。

createAcpSessionBridge从两个条件合取推导出不可变的强制 lease 证明(attestation):冻结的子进程环境覆盖携带精确 Conversations marker,且所选channelFactory携带转发能力(由createSpawnChannelFactoryscrubChildEnv合并第二参数并打上包所有、不可变的转发能力标记)。每次 spawn 前 bridge 将冻结覆盖与新鲜私有父能力合并进工厂参数映射。

ConversationRuntimeManager在其 owned-runtime 校验中包含该证明:缺失或错误的证明是静态运行时契约违例——新候选被拒绝并 dispose;等价既有已注册运行时被终末隔离,隔离的终末原因固定为不可重试的conversation_root_compromised,后续每次ensure()或当前运行时断言都重新抛出同一错误,而不降级为可重试的conversation_runtime_unavailable。源码证据见 conversation-runtime-manager.ts:隔离原因为missing_mandatory_lease_attestation(第 21 行),该原因映射到conversationRootCompromisedError()(第 88-93 行),并在ensure()的终末分支与运行时校验(第 222 行runtime.bridge.mandatoryLeaseAttested !== true)中强制执行。

ACP 会话必须在配置初始化期间、报告创建或恢复成功之前获取 lease,并持有到会话关停,复用既有的session_writer_conflictsession_writer_lostsession_transcript_changedsession_writer_unavailable映射。

StandaloneSessionService的工作目录身份 pin 限定为一个本地驻留 bridge 会话 generation:借助agentBound.eventEpoch,只有当 bridge 仍报告同一 standalone 会话与 epoch 时才把 pin 复用作expected身份;终末关闭留下的裸{ pinned }、idle reap 后缺失的会话或不同 epoch 均为孤儿 pin,必须在恢复/修复/删除检查等维护路径检查目录前移除。若没有匹配的本地 generation 剩余,open/repair 应用既有精确根、直接子、所有权、非符号链接与权限检查(不带陈旧expected身份),采纳当前安全目录身份,ACP 子进程再获取 writer lease;生命周期操作关闭本地会话时丢弃会话 pin、获取父侧生命周期 lease,然后才为文件系统变更捕获新鲜的操作本地目录身份。既有受驻留 generation 或操作本地身份的变更仍是working_directory_compromised,只有孤立的守护进程本地 pin 可被替换。StandaloneSessionService的父侧生命周期与维护获取(含删除日志回收)同样选择加固后的local策略;普通 workspace 生命周期路由与其他受管运行时保持never

值得强调的是:lease 保护一个 transcript 及其生命周期,不协调会话列表读取、随机 ID 生成、守护进程本地受管目录状态、SSE 路由或 Live discovery;且 fence 只覆盖更新后、协作的、由标记 Conversations 运行时承载的 writer——不获取协议 lease 的 legacy 守护进程或其他进程仍可绕过它。lease 是完整性协议,不是操作系统访问控制边界

3. 只回收可证明陈旧的本地活跃 writer

在把local回收策略用于受管 Conversations writer 之前,Core 先加固它。源码证据见 session-writer-lease.ts:

  • Linux 上,活跃 schema-version-2 记录在既有 hostname 与process_start_identity(已含 boot ID 与进程启动 ticks)之外,新增可选pid_namespace_id;writer 在平台暴露时记录两个身份。读者把缺少任一身份的旧记录或新记录都视为存活,绝不自动回收。
  • 复用 Core 的readPidNamespaceId()readLocalBootId()与保守 PID 存活行为,同时保留 writer lease 既有持久化process_start_identity格式。EPERM/EACCES仍视为存活;只有"PID 被证明不存在"、"经校验的僵尸"或"可读的启动身份不匹配"能在身份域检查通过后支撑陈旧判定。
  • 判定逻辑见lockStateForRecord()(第 499-530 行):hostname 不匹配 → live;Linux 上 boot ID 或 PID namespace 缺失/不匹配 → live;PID 不存在 → stale;无启动身份 → live;启动身份可读且不同 → stale,否则 live。实现复用既有回收守卫、精确记录重读、原子陈旧记录移动与判定后的 transcript 校验;从不依据锁年龄、获取时间、心跳缺失或守护进程响应性作决定。
  • Darwin 与 Windows 保留各自的进程启动探测,要求精确 hostname 与已记录启动身份:PID 确定不存在 → stale;PID 存活但当前启动身份可读且不同 → stale;任一判定所需身份不可用或平台不支持身份探测 → 记录对回收而言保持存活。因此这两个平台依赖行为契约中的单机拓扑:无 boot/namespace 分量时精确 hostname 就是整个身份域,共享 hostname 与 runtime base 的两台主机可能把对方存活的 writer 判定为本地缺失 PID;Linux 额外用 boot ID 与 PID namespace 隔离。
  • 启动身份匹配存活的 PID 保持session_writer_conflict(即使事件循环停滞);外来 host/boot/namespace、无身份、畸形、非普通文件与不确定记录一律 fail closed。local策略只作用于未密封的活跃记录;密封记录仍需 certified transcript takeover。任何过渡声称(transition claim,包括残余的)都绝不基于进程存活被回收。

正常每会话关闭会移除其精确活跃记录;优雅受管关停持久化密封记录,其他受管守护进程必须校验 transcript 证明后才能接管。首个切换版本保留该回收边界,并在发布说明中记录:Linux 重启或容器 PID namespace 变更可能让未密封活跃记录无限期 fenced,且409 session_writer_conflict并不证明其 writer 仍存活。跨 boot 自动回收、新机器身份字段、基于过期的接管与公开 force-unlock API 都需要单独的设计决策。

强制 lease 还使优雅受管关停对每个活跃 Conversations transcript 执行密封与哈希;实现不静默延长既有子进程终止截止时间,而是要求以代表性最大活跃会话/transcript 规模衡量并行密封是否落入该预算。

4. 停止写入 owner 但不破坏迁移与状态目录安全

行为变更不再构造、获取、释放长期存活的文件 owner,但保留旧版本所需的最小"检查并退役"路径;第一个补丁不删除剩余 owner 实现与聚焦测试(迁移窗口关闭后再清理)。具体做法:

  • StandaloneDeletionJournal所需的一小部分路径创建与校验逻辑移入该类,不新增替代 owner 服务或仅此一个消费者的 standalone 抽象;日志从状态父目录直接派生,保留私有目录创建、身份校验与持久化检查,但没有owner 记录、进程存活检查、锁、宽限期、acquire 或 release。
  • 保留目录权限不对称:POSIX 上conversations/叶子及其日志子树要求0700,新建目录使用该模式;稳定状态根与既有祖先必须是同属、非符号链接且规范身份稳定的目录,但不要求历史模式为0700也不修改——尤其既有0755~/.qwen依然有效。
  • StandaloneDeletionJournal在每次读、恢复、清空、写路径上先校验父目录:读取把缺失状态目录视为空,首次写入安全创建;不安全的或被替换的父目录使日志操作 fail closed。
  • 既有回收 singleflight 仍是守护进程本地的:每个日志 UUID 在读取或变更 transcript 前进入其会话生命周期 lease;后台扫描若输掉获取则跳过该 UUID 继续触发操作,不把普通 lease 争用归类为日志损坏;同一会话的直接生命周期请求保持正常结构化冲突响应。
  • 新守护进程永不创建或替换conversations/runtime-owner.json,只在既有conversations/.runtime-owner.lock下检查:存活的旧版 owner 保持conversation_runtime_in_use;精确重校验的陈旧记录被持久化移除。该锁只是此兼容操作的瞬态守卫,不跨运行时生命周期持有。
  • 服务器保留conversation_runtime_in_use仅用于旧版迁移与 Live-start 非发布者准入;更新后的守护进程不会仅因另一更新守护进程挂载了 Conversations 就发出它。conversation_runtime_unavailableconversation_root_compromised与守护进程本地运行时不变式失败保持不变。

5. Live discovery 保持独立,激活改为发布者准入

discovery.ts 中的 owner 协议保持不变:owner 记录选出一个可发现的 Live 宿主并保护该端点发布。但发布本身目前并不门控/live/start/live/new或 Host 快捷键动作的激活——旧实现由被移除的 Conversations acquire 间接提供该激活门,因此替代实现必须同时覆盖发布与每条启动路径:

  • 发布路径:在每次writeLiveDiscoveryFile()之前,对所有目标基(含稳定基)调用handoffLiveDiscoveryOwner()。源码中该函数(第 478 行起)以commitOwner回调与宽限语义工作;发布路径为所有目标传入 no-opcommitOwner(不再有 Conversations owner 记录可提交),保留默认waitForHandoffGrace。在陈旧 owner 回收时,handoffLiveDiscoveryOwner()持 Live 锁移除已校验死 locator、释放锁、再执行既有交接宽限;随后发布调用writeLiveDiscoveryFile()重取 Live 锁并拒绝竞争中的活跃发布者。no-op 刻意移除旧的跨记录排序依赖,同时保留锁后 Live 宽限。
  • 只读 Live-start 准入:使用与稳定基相同的目录身份检查、锁、安全记录解析与精确 owner 比较。发布等待完成后、通话激活前,重新读取稳定 locator,要求当前协议版本且{ pid, instanceNonce }与本地守护进程匹配;绝不创建、替换、移除或回收记录。有效但不同的发布者 → 既有可重试conversation_runtime_in_use;缺失或瞬态不可读 locator →conversation_runtime_unavailable;畸形或不安全状态 → 不可重试conversation_runtime_ownership_compromised。这些失败是 Live 局部的,不会隔离 Conversations 运行时或禁用 standalone 路由。
  • 路由发起的/live/start/live/new与 Host 发起的toggle/new动作必须在LiveHostCoordinator.start()之前进入同一异步准入接缝;被拒绝的 Host 动作发布既有非机密 Live unavailable/error 状态,且不得启动 Live 会话协调器、麦克风采集或 Appshot 采集。由于接缝是异步的,准入在 start 进入接缝时捕获协调器动作 generation,此后任何 Hoststop/toggle/new与任何/live/start/live/new/live/stop请求都推进该 generation;接缝在start()前重查 generation,被取代的 start 直接丢弃——pending 准入期间到达的 stop 绝不能在准入解析后跟随着启动通话。协调器既有的 action epoch 只在通话创建时推进,无法表达该预启动窗口排序,因此 generation 与 epoch 并存。

最终状态:Live discovery 与激活保持单发布者,而 Conversations writer lease 是会话粒度的——当选发布者上的 Live 会话可以与另一守护进程上的不同 standalone 会话同时运行;另一守护进程触及同一 transcript 时由 writer lease 拒绝,standalone 路由无论 lease 可用与否都继续拒绝 Live-owned 记录。

6. 定时任务激活加固:不加第二个 lease

定时任务的持久化、keepalive 与 boot 再水合保持不变。Controller 与 run 会话在恢复成功前从 Conversations 运行时继承强制 writer lease;丢失 lease 的守护进程无法让绑定会话驻留,boot 再水合记录失败,keepalive 使用既有重试退避。多个守护进程可读取同一任务文件并同时尝试恢复同一绑定会话,lease 获取发生在该会话调度器激活之前,因此恰好一个恢复可驻留并触发绑定任务;失败的恢复不得执行或预订任务,其冲突保留在既有再水合结果与onError路径,而非追加为一次任务运行。既有跨进程任务文件变更锁与持久化lastFiredAt状态不变。这防止仅由并发恢复引起的重复,但并不把调度器升级为 exactly-once:prompt 已派发但 fired 状态尚未持久化的既有 at-least-once 窗口保持不变。

未绑定持久任务需要单独的准入规则:两个 keepalive worker 最初可能铸造不同的 controller 会话 ID,此时会话 writer lease 无法在它们之间选举。因此,在标记的 Conversations ACP 运行时中,持久加载前安装调度器资格谓词,拒绝触发任何未绑定持久任务;守护进程 keepalive 仍是唯一绑定此类任务的组件,其既有updateCronTasks事务在跨进程任务文件锁下重查未绑定状态、提交一个 controller 会话 ID 并拆除失败的孤儿。默认runQwenServe路径启用守护进程受管绑定 worker;选择 Conversations marker 但未启用该 worker 的嵌入宿主会让未绑定 Conversations 任务休眠,且不得回退到 legacy lock-owner 执行。产品边界不变:持久 cron 任务在 standalone 会话中仍不受支持,通用定时任务路由不能在 Conversations workspace 创建新会话。

7. 同一版本内交付最小 Web Shell 降级

不新增activeElsewhere列表字段或新的客户端 owner 发现协议。后端与 Web Shell 可分别合入 PR,但发布必须等客户端本地呈现结果状态:

  • standalone 列表失败与过渡期conversation_runtime_in_use在 Recents 区域渲染一次,而不是每次导航触发 refetch 都走全局错误 toast;
  • 打开会话时的session_writer_conflictsession_writer_unavailable渲染在受影响行/区域并带重试动作。

activeElsewhere提示仍被推迟,因为列表没有权威的跨守护进程 live-state 索引——客户端改为响应既有 attach 时错误契约。

8. 不加路由与协调

变更不引入守护进程间代理、重定向、共享 owner 索引、心跳、全局生命周期锁或分布式缓存失效;既有会话路由继续只在接收守护进程内部的运行时中解析 owner。不引入新设置或命令行标志——同时支持排他与宽松两种模式会保留整个旧子系统并制造混合模式兼容问题。

错误契约速查

条件结果
另一更新守护进程加载了不同会话列出、新建与对另一会话的操作继续
另一更新守护进程为 open/rename/repair 加载了该会话该会话返回409 session_writer_conflict
批量 archive/delete 包含另一守护进程加载的会话既有200批处理外壳,该项errors[].codesession_writer_conflict
同本地身份域内可证明已死的良好活跃 owner回收 → 权威 transcript 重载 → 继续
活跃 owner 存活/停滞/外来/缺失回收身份该会话409 session_writer_conflict
writer 记录畸形/非普通文件,或残留 transition claim该会话503 session_writer_unavailable
有效密封记录 + 匹配 transcript 证明Certified takeover → 权威重载 → 继续
密封记录 transcript 证明不再匹配该会话409 session_transcript_changed
Conversations bridge 缺少强制 lease 证明新候选拒绝并 dispose,或既有运行时终末隔离;一切请求保持不可重试503 conversation_root_compromised
安全工作目录身份变更且无匹配本地 generation 残留丢弃孤儿 pin,采纳当前身份,走正常 writer/lifecycle lease 继续
工作目录身份在本地 generation 驻留期间或 leased 生命周期操作捕获后变更既有working_directory_compromised
存活的 legacy runtime-owner 记录迁移期 standalone 表面503 conversation_runtime_in_use
legacy 拥有权状态畸形/不安全/不确定既有conversation_runtime_ownership_compromisedconversation_runtime_unavailable
非稳定 Live locator 发布者的守护进程尝试启动 Live/live/start/live/new返回503 conversation_runtime_in_use;standalone 仍可用
Live-start 时稳定 locator 缺失/不可读/畸形/不安全Live 启动 fail closed(unavailable 或 ownership-compromised);standalone 仍可用

对每个 writer 状态行,批量生命周期路由在既有200每项结果中保留相同错误类型,而不是改变批处理的传输状态。

兼容性与发布

REST 与 SDK 对象形状不变。守护进程批量序列化器必须停止把 session-writer 错误折叠进standalone_session_operation_failed,并在受影响项字符串code中保留既有 writer 错误类型;standalone_sessions_v1standalone_session_options_v1继续描述 API 支持,且不为并发保证新增能力。

发布是"排空并切换"(drain-and-cutover),不是滚动混版升级:先停止所有能在无强制 lease 下承载 standalone 会话的守护进程版本,确认其会话已关闭,再启动带本变更的守护进程。若在旧→新方向违反该顺序,新守护进程会尊重存活的 legacy runtime-owner 记录并返回503 conversation_runtime_in_use;旧守护进程退出后,下一次运行时初始化移除陈旧精确记录并无须重启继续。兼容守卫降低失败模式,但不支持滚动混版部署——一次性兼容检查在运行时发布后不重复,无法探测反向(更新守护进程挂载后旧守护进程才启动:它找不到 owner 记录、写一条、可能在 leased writer 旁运行未 lease 的 ACP writer)。因此共享同一 Conversations 根的守护进程必须一起升级;owner 记录格式不扩展 lease-aware marker,因为旧严格读者会把新形状判为 compromised。

writer-lock schema-version-2 owner 记录接受可选pid_namespace_id:更新后的 Linux writer 在可用时填充,更新后的回收器要求它与既有启动身份共同存在。缺完整身份的既有或新活跃记录对冲突检测兼容,但不能自动回收——升级预检必须排空这些 writer,或在权威外部 writer fence 后显式清理残余记录。回滚必须在旧的非参与守护进程启动前关闭/排空所有更新后的 Conversations writer,并确认无活跃、密封、claim 或身份扩展记录残留;仅优雅进程退出可能有意留下密封交接记录。

发布说明要点包括:多个守护进程可为同一用户暴露 standalone 会话;活跃会话是守护进程本地的;支持拓扑是一台机器、同一 OS 用户下若干守护进程(跨物理机共享稳定基或 runtime base 不受支持,Darwin/Windows 的陈旧 writer 回收依赖该边界);不同会话 ID 可跨更新守护进程并发活跃,而同一会话的第二 writer 或生命周期变更被 fenced;standalone 会话始终使用 writer lease(即使实验设置缺失或为 false),Live 与定时任务 writer 同样如此;只有精确稳定 Live locator 发布者能激活 Live;同时刻的定时任务再水合只接纳一个驻留 owner,且不提供超出调度器既有持久化语义的 exactly-once;未绑定任务在跨进程任务文件事务提交其 controller 绑定前不能从 Conversations 会话触发;可证明同域死亡活跃 writer 被自动回收(Linux 要求相同 hostname、boot 与 PID namespace),存活或不可验证的 writer 保持 fenced;lease 提供同会话冲突 fence,而非多主支持。

验证策略

单元测试

覆盖:无长期 legacy owner 的服务器启动;存活/陈旧 legacy-owner 兼容检查;严格畸形 owner 失败;无 lifetime ownership 的运行时初始化;无 owner release 的关停;保留的 root/generation/quarantine 失败;删除日志状态父目录创建与 compromise 检测;历史非0700状态根接受 + 非私有conversations/叶子拒绝;无 owner 获取的日志启动;日志读/恢复/写路径父目录重校验;后台回收中"争用即跳过";运行时来源 writer-lease 选择。

writer 矩阵覆盖:Conversations 运行时用户设置关闭、普通运行时关闭、所有运行时开启——Conversations 选local而普通受管子进程保持never;无私有父能力时 marker 拒绝;环境文件加载前捕获;用户级与项目级环境擦洗;sandbox 传播;primary/secondary/Conversations bridge 子进程环境隔离。工厂覆盖验证默认工厂与createSpawnChannelFactory返回的每个配置工厂都携带转发能力并经scrubChildEnv合并 bridge 第二参数。Bridge 证明覆盖:精确 marker + 该工厂或故意证明的测试 fake 被接受;忽略第二参数的未证明工厂 + marker 被拒绝;能力工厂但 marker 缺失或错误被拒绝。运行时发布覆盖:合格 Conversations bridge 被接受;不合格新live-conversation候选被拒绝并 dispose;等价既有注册运行时被拒绝并隔离,且后续每次访问保持不可重试conversation_root_compromised。调度器覆盖验证捕获的 marker 安装未绑定持久任务跳过、绑定后允许触发、普通 workspace lock-owner 行为不变。

Core lease 覆盖记录并校验 Linux PID namespace、缺回收身份视为存活、仅同 hostname/boot/namespace 回收死亡与 PID 复用 owner、拒绝匹配存活/停滞/外来 host/boot/namespace/legacy 无身份/畸形/密封/残留 claim。Darwin/Windows 覆盖对应 hostname 与进程启动规则。

受管关停与双进程集成

受管关停覆盖用代表性高活跃会话数与大 transcript 衡量并行密封是否满足既有终止截止时间,并验证强制超时在 writer 进程仍存活时保留精确活跃锁、绝不留下残余 claim 或不确定过渡,且该精确进程退出后只有同身份域竞争者可恢复良好活跃记录。

双进程守护进程集成测试(同一 home、runtime base 与 Conversations 根)验证 15 项核心场景:

  1. 守护进程 A 与 B 的GET /standalone/session-optionsGET /standalone/sessions都返回200
  2. 任一守护进程的 standalone 路由不因另一进程存活而返回conversation_runtime_in_use
  3. 存活 legacy runtime-owner 记录返回503 conversation_runtime_in_use,其进程退出后下一次请求无须重启即退役陈旧记录并返回200
  4. A 保持会话 S 活跃时,B 仍可列目录、新建聊天 T、恢复并使用不同持久化会话;
  5. A 保持 S 活跃时,B 恢复/重命名/修复 S 收到409 session_writer_conflict;archive/delete 保持200批处理外壳并报告 S 的session_writer_conflict
  6. 显式关闭释放 A 的 lease 后,B 通过普通获取恢复 S;独立地,A 优雅关停密封另一会话后,B 通过 certified takeover 恢复;
  7. A 关闭或 idle reap S 后,B 可安全重建其工作目录(新身份)并释放 S,之后 A 可 open/repair/delete S 而不把 A 的孤儿 pin 视为 compromise;A 匹配 generation 驻留期间替换目录仍 fail closed;
  8. A 的锁持有 ACP writer 在稳定活跃态被杀后,同本地身份域中的 B 回收锁、权威重载 transcript 并继续,无须手工清理;只杀 A 的父进程而 writer 仍存活时仍返回冲突;
  9. 匹配存活或停滞的 A,以及外来 host/boot/PID namespace 记录保持409 session_writer_conflict;无身份与残余 claim 状态 fail closed;
  10. 关停任一守护进程不移除或失效另一方的本地运行时;
  11. 两个空闲守护进程同时再水合同一定时任务绑定会话时经 writer lease 选出一个驻留会话,输家记录恢复失败并退避,只有赢家触发该槽位的绑定任务;
  12. 两个 keepalive worker 观察到同一未绑定任务可能铸造竞争 controller 会话,但 Conversations 会话在未绑定时绝不触发它;任务文件事务提交恰好一个绑定、清理输家孤儿,只有绑定 controller 具备触发资格;
  13. 两个守护进程回收同一 prepared 删除时选出一个 lease 持有者,竞争者跳过该 UUID 而不使无关触发操作失败或报告日志损坏;
  14. 两个守护进程都启用 Live 时,只有精确稳定 locator 发布者能激活通话:另一守护进程的/live/start/live/new、Host toggle 与 Host new 在 Live 会话协调器或采集设备启动前失败;该守护进程仍服务不同 standalone 会话,standalone 路由继续拒绝 Live 记录;
  15. root compromise 与不可用 generation 仍 fail closed,不回退到主 workspace。

聚焦StandaloneSessionService覆盖:终末关闭不留可复用 generation pin;detach 保留同 generation 的 pin;idle-reaped 或被替换的 event epoch 在复用前被惰性丢弃;删除回收从不提供孤儿 pin;驻留 generation 期间的替换与 leased 生命周期操作捕获后的身份变更保持working_directory_compromised;外来摘要或不确定 bridge 探测绝不授权重新 pin。

Live 回归覆盖:discovery 仍至多一个发布者;发布路径对稳定基与自定义基都执行既有校验交接;陈旧稳定基记录可在前任 owner 退出后被回收;no-opcommitOwner交接;宽限在交接锁释放后、发布前运行;该间隙的竞争发布者被发布锁拒绝。启动准入只接受当前协议版本与精确稳定 locator PID/instance nonce,拒绝有效不同发布者(可重试错误),对缺失/畸形/不安全/不可读 locator 状态 fail closed 而不隔离 standalone;两条 HTTP 启动路由与两条 Host 启动动作共用同一准入,拒绝时不执行任何会话或采集工作;准入线性化证明:pending 准入期间被后续 stop/toggle/new 意图取代的 start,在解析后不执行任何会话或采集工作,只有最新意图可激活通话。standalone 可用性独立于 discovery 发布失败与发布记录者。

Web Shell 回归覆盖:切换会话不因 standalone 列表失败弹 toast;过渡期conversation_runtime_in_use在 Recents 区域出现一次;打开时的session_writer_conflict/session_writer_unavailable附着于受影响会话/区域并带重试动作;不需要activeElsewhere列表字段。

测试必须证明边界的两个侧面:不同会话 ID 可跨更新守护进程并发使用,而同一会话在其完整加载生命周期内保持排他;不得暗示全局新鲜 live state、跨守护进程事件路由或原子多会话操作。

被否决的替代方案

  • 守护进程间代理(proxying):保留单一运行时 owner,但需要对所有 standalone 与 owner-routed 会话 API(含 SSE 与权限)做认证转发。
  • 客户端重定向到 owner:引入发现、token、origin 与重连行为,并保留本变更要移除的全局 owner。
  • 每守护进程独立 standalone 存储:简单,但使守护进程看不到同一持久化会话目录。
  • 依赖用户设置启用 writer lease:允许关闭设置的守护进程绕过 fence,无法保护共享 standalone 持久化。
  • 只对sourceType=standalone启用 lease:漏掉同 Conversations 运行时承载的 Live 与定时任务来源,包括无 standalone API 请求的后台再水合。
  • 把单发布者 discovery 当作激活 fence:发布与激活是两条独立路径,无每条启动路径前的显式准入,直连非发布者守护进程的客户端仍可启动第二个 Live host。
  • 现在就给列表加activeElsewhere:需要权威共享 live-state 索引或 owner 发现协议;最小 Web Shell 行为可用既有 attach 时错误契约实现。
  • 共享路由或全局事务平面:跨守护进程事件路由、全局新鲜 live state 与跨会话原子操作是不同可靠性契约,对"由既有会话 writer lease 分区的并发使用"并非必需。
  • 无资格限定的陈旧 writer 回收:仅 PID/hostname/年龄/不活跃无法证明共享存储上的受管 writer 已死;自动恢复限定于匹配本地身份域与稳定进程身份,一切不确定状态保持 fenced。
  • 保留受管never+ 手工解锁:普通非协作 writer 死亡会让每个已加载会话不可用,直到操作员发现并移除内部锁文件;身份限定本地回收自动处理常见安全情形,手工恢复仅保留给外部 writer fence 后的不确定过渡或存储状态。

小结

Relaxed Standalone Daemon Ownership 将 Qwen Code 的 Conversations 会话共享模型从"一进程全局独占"演进为"会话粒度排他 + 身份限定恢复":更新后的守护进程并发服务不同会话,同一会话始终由跨进程 writer lease 串行化,Live 激活保持机器全局单发布者,而一切不安全、无法证明或跨身份域的状态严格 fail closed。这一设计既解决了多守护进程场景下503 conversation_runtime_in_use的可用性痛点,又没有引入跨守护进程路由、全局锁或分布式协调等更重的可靠性契约,是一个以既有 writer 协议为支点的会话分区并发契约。部署时请务必遵循排空并切换的升级顺序,并在多守护进程拓扑下按本文错误契约核对客户端呈现。

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询