qwen-code Daemon 会话生命周期与身份机制完全指南:从创建、附加到恢复、终止
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
qwen-code 的 daemon 进程(qwen serve)把一次逻辑对话建模为一个会话(session),并由 ACP 桥接层统一管理其创建、附加、加载、恢复、关闭、死亡与回收。本文以docs/developers/daemon/08-session-lifecycle.md为骨架,结合packages/acp-bridge、packages/cli/src/serve与packages/sdk-typescript的源码实现,完整讲解会话状态机、X-Qwen-Client-Id客户端身份规则、心跳与断线回收守卫、全部生命周期端点及可调配置,帮助你掌握如何在多客户端并发、重启恢复、worktree 隔离等场景下正确使用 daemon 的会话能力。
会话(Session)与客户端(Client)两个核心概念
daemon 中的session是一条逻辑对话,严格绑定到唯一一个 ACPsessionId。桥接层为每个会话维护一个SessionEntry(见 03-acp-bridge.md),把 ACP 子进程连接与 HTTP 侧的簿记状态耦合在一起,包括:
- prompt FIFO 队列与 model-change FIFO 队列;
- 事件总线(EventBus);
- 待处理的权限请求(pending permissions);
- 已附加的客户端(attached clients);
- 心跳状态(heartbeats);
- 恢复状态(restore state);
- 终端帧墓碑(terminal-frame tombstones)。
而 daemonclient则由 HTTP 请求头X-Qwen-Client-Id标识——这是一个由 daemon 校验的、不透明的字符串,由调用方在请求上盖章。桥接层跟踪哪些客户端附加到了哪些会话,并利用发起者(originator)的 client id 驱动designated权限策略、审计追踪与事件归属。
daemon 的核心职责可归纳为:铸造(mint)、附加(attach)、恢复(restore)、回收(reap)会话;校验并拒绝非法X-Qwen-Client-Id;跟踪每个会话的多个附加客户端(clientIds: Map<string, count>与attachCount);在出站事件上盖章originatorClientId;运行心跳供仪表盘判断客户端是否在线;暴露可运维设置的会话元数据(displayName,通过PATCH /session/:id/metadata);驱动session_died、session_closed、client_evicted、stream_error等终端帧。
架构:生命周期涉及的四个关键类型
| 关注点 | 源码位置 | 说明 |
|---|---|---|
SessionEntry | packages/acp-bridge/src/bridge.ts#L1123 | 每个会话的内部结构体,完整字段清单见下文"State & Lifecycle" |
BridgeSession(公开类型) | packages/acp-bridge/src/bridgeTypes.ts#L190 | { sessionId, workspaceCwd, currentCwd?, attached, clientId?, createdAt?, hasActivePrompt?, ... },返回给 HTTP 处理器 |
BridgeSessionState | packages/acp-bridge/src/bridgeTypes.ts#L520 | LoadSessionResponse \| ResumeSessionResponse,缓存在 entry 的restoreState字段上 |
DaemonSession(SDK 侧) | packages/sdk-typescript/src/daemon/types.ts#L1196 | { sessionId, workspaceCwd, currentCwd?, attached, clientId?, createdAt?, ... },与BridgeSession保持字段同步 |
另外两个生命周期关键实现点:
- client-id 校验:位于 packages/acp-bridge/src/bridge.ts 的
spawnOrAttach附近,非法时抛出InvalidClientIdError。 - 会话断线回收器(disconnect-reaper):位于 packages/cli/src/serve/server.ts,使用
attachCount与spawnOwnerWantedKill跟踪 spawn 所有者的断线。
状态机:一次会话的完整流转
Attach 与 Spawn 的区别
在默认的sessionScope: 'single'下,桥接层的defaultEntry被所有接入的客户端共享:
- 当
POST /session到达且defaultEntry已存在时,返回attached: true,不会重新 spawn 一个新的 ACP 子进程; - 桥接层同步递增
attachCount,并把调用方的X-Qwen-Client-Id注册进clientIds。
源码级佐证(bridge.ts#L9842-L9866):在effectiveScope === 'single'且defaultEntry存在时,代码在任何 await 之前同步执行existing.attachCount++并调用registerClient(existing, req.clientId)。注释明确说明:这个同步递增是为了让 spawn 所有者的断线回收器(requireZeroAttaches: true)在桥接层让出(yield)时也能看到这次附加,从而避免"快速断线的 spawn 所有者每隔一次重连就摧毁一个健康会话"。
在sessionScope: 'thread'下,每个线程可以铸造成独立的会话;调用方仍然受maxSessions上限约束。
身份(Identity):X-Qwen-Client-Id 的设计与校验
X-Qwen-Client-Id可选但强烈推荐。daemon 不会替调用方生成该 id——客户端自己选定并在后续请求中复用它,这样 daemon 才能归因投票(votes)、审计事件并检测重连。
命名建议:每个独立的控制器(controller)应使用一个不同且稳定的 id。Web Shell 出于兼容性保留历史webui_前缀。仅当一个宿主(host)与内嵌的 Web Shell 有意作为同一逻辑控制器时,才应共享同一个 id;一旦共享,daemon 日志将无法区分是哪一方发起的请求。
校验规则(来自文档,与源码一致):
- 字符集:
[A-Za-z0-9._:-]; - 长度:1–128;
- 超出该集合:抛出
InvalidClientIdError(HTTP 400)。
源码侧的登记与校验机制:
registerClient(bridge.ts#L4507):若请求携带的 id 已在该会话的clientIds中,则引用计数 +1;否则铸造一个新 id:createClientId()生成`client_${randomUUID()}`(bridge.ts#L4505)。resolveTrustedClientId(bridge.ts#L4583):会话级请求若携带clientId,必须已登记在该会话的clientIds集合中,否则抛出InvalidClientIdError。该错误类定义在 packages/acp-bridge/src/bridgeErrors.ts#L327,错误消息为Client id "..." is not registered for session ...。
出站事件盖章originatorClientId的三个条件(缺一不可):
- 触发该事件的请求携带了
X-Qwen-Client-Id,且 - 该 id 当前已登记在会话的
clientIds集合中,且 - 会话存在
activePromptOriginatorClientId(内联的sessionUpdate与permission_request会继承当前活动 prompt 的发起者)。
匿名调用方(不带X-Qwen-Client-Id)与权限策略的兼容矩阵:
first-responder策略:匿名调用方可以正常投票;designated策略:拒绝匿名投票,返回permission_forbidden{ reason: 'designated_mismatch' };consensus策略:同样拒绝,原因相同——匿名者不在发起时刻的votersAtIssue快照中;local-only策略:唯一接受匿名回环(loopback)投票者的策略。
工作流详解
创建或附加(Create or attach)
spawnOrAttach(bridge.ts#L9780)还会在req.sessionId提供时强制使用'thread'作用域,并支持modelServiceId、approvalMode、worktree、branch、parentSessionId、sourceType/sourceId等扩展字段(见 bridgeTypes.ts#L134 的BridgeSpawnRequest)。附加时若携带modelServiceId且当前会话运行的是不同模型,桥接层会调用setSessionModel对齐模型,并发布model_switched事件让所有附加客户端可见。
加载与恢复(Load / resume)
POST /session/:id/load:恢复持久化会话,并返回当前有界的回放快照窗口(session/load通知或响应式回放在响应返回前就已播种)。POST /session/:id/resume:恢复但不带回放(底层为connection.unstable_resumeSession,对外以稳定的session_resumedaemon 能力暴露;unstable_session_resume保留为废弃别名)。
两者共同的行为:
- 在 channel 上使用每会话的
pendingRestoreIds集合,使并发恢复调用合并(并发者收到RestoreInProgressError); - 在 entry 上缓存
restoreState,让迟到的附加者拿到与最初恢复者相同的载荷。
Part 4A worktree 会话的完整性门控:恢复这类持久化会话时,sidecar 必须显式标识请求的 workspace 根,checkout 必须规范地包含在对应的.qwen/worktrees/目录之下,且其 marker 必须是包含确切恢复会话 id 的单链接常规文件。daemon 仅在上述检查通过后才迁移空闲的已恢复子进程;活动子进程仅在报告 cwd 已等于 worktree 时才被接受。worktree 所有权转移路由POST /session/:id/worktree-reset(能力标签session_worktree_reset_v1)用于 Channel 任务重置:daemon 在根 workspace 生成一个线程作用域的新替代会话,将其迁移进已校验的 checkout,链接 sidecar 对(旧会话先写supersededBy,再写替代会话的supersedes),在每 checkout 路由锁与准入屏障下把 marker 翻转为替代会话,随后切断被取代会话的客户端注册与内存 worktree 关联。若被取代会话的子进程仍持有后台工作而存活,屏障保持武装,并在响应中如实报告supersededSessionLive: true;被准入屏障拒绝的写者收到409 worktree_reset_active。完整失败分类(含409 worktree_session_superseded、409 worktree_reset_interrupted、409 worktree_marker_missing及各自的修复路径)记录在 qwen-serve-protocol.md 的路由目录中。
心跳(Heartbeat)
POST /session/:id/heartbeat无条件更新sessionLastSeenAt;若请求携带已登记的X-Qwen-Client-Id,则clientLastSeenAt.set(clientId, Date.now())也会更新。v1不实现按客户端驱逐(per-client eviction);撤销(revocation)策略计划在 F 系列 Wave 5 落地。当前心跳仅提供可观测性。
源码佐证:recordHeartbeat在 bridge.ts#L12403-L12405 同时更新两个时间戳;测试 bridge.test.ts#L5782-L5878 验证了"未登记的伪造 id 不会更新 lastSeenAt"以及客户端注销(unregisterClient)会同步删除其clientLastSeenAt条目,避免长期运行的 daemon 累积过期时间戳。
元数据(Metadata)
PATCH /session/:id/metadata接受{displayName?}。校验规则:
- 最大长度:
MAX_DISPLAY_NAME_LENGTH = 256(bridge.ts#L1483); - 不得包含控制字符:
hasControlCharacter(bridge.ts#L2473)拒绝码点 ≤ 0x1f 或 == 0x7f 的字符; - 违规时抛出
InvalidSessionMetadataError(HTTP 400),具体校验逻辑见 bridge.ts#L12027-L12035。
更新成功后向所有订阅者广播session_metadata_updated事件。
终止(Termination)与终端帧
| 终端帧 | 触发条件 |
|---|---|
session_closed | DELETE /session/:id(client_close)或程序化关闭 |
session_died | channel.exited因任何原因触发(崩溃、子进程被杀);使用 OS 退出路径时携带exitCode?+signalCode? |
client_evicted | 每个订阅者的事件总线队列溢出(见 10-event-bus.md)。不是会话级终止——只关闭该订阅者 |
stream_error | SubscriberLimitExceededError或其他路由级流失败 |
每条终止路径都会通过mediator.forgetSession(sessionId)把待处理的权限解析为{kind:'cancelled', reason:'session_closed'}(这是 ACP 的要求:被取消的 prompt 必须以outcome.cancelled解析其未决的requestPermission)。
断线回收守卫(Disconnect-reaper guard)
当 spawn 所有者的 HTTP 响应无法写入(如握手中途 TCP reset)时,路由调用killSession({ requireZeroAttaches: true })。若此时已有其他客户端附加(attachCount > 0),守卫短路,会话继续存活。设置spawnOwnerWantedKill = true记住这一意图,以便后续某个detachClient()把attachCount带回 0 时完成延迟回收。这一机制的目的正是防止"快速断线的 spawn 所有者每隔一次重连就摧毁一个健康会话"。
源码佐证:spawnOwnerWantedKill字段定义在 bridge.ts#L1416,注释(BkwQP)详细描述了该墓碑语义;实际调用点位于packages/cli/src/serve/acp-http/dispatch.ts(如 #L1135 与 #L2128-L2131 的requireZeroAttaches: true)。
生命周期关键字段:SessionEntry 全景
以下字段直接驱动会话生命周期(完整结构体见 bridge.ts#L1123):
| 字段 | 类型 | 含义 |
|---|---|---|
clientIds | Map<string, number> | 已登记 client id → 登记引用计数 |
attachCount | number | 该 entry 上spawnOrAttach返回attached: true的次数 |
activePromptOriginatorClientId | string? | 当前运行中 prompt 的发起者 |
restoreState | BridgeSessionState? | 缓存的 load/resume 响应,保证迟到的附加者看到一致载荷 |
spawnOwnerWantedKill | boolean | 延迟回收墓碑(见上文断线回收守卫) |
sessionLastSeenAt | number? | 任意客户端最近一次心跳(epoch ms) |
clientLastSeenAt | Map<string, number> | 每个客户端的最近心跳 |
pendingPermissionIds | Set<string> | 当前待处理的 ACP requestIds——取消/关闭时用于将其解析为 cancelled |
此外,attachRefs: Map<string, number>(bridge.ts#L1403)是每 clientId 的附加引用账本:detachClient只能通过释放该账本中的引用递减attachCount,spawn 所有者与恢复发起者等 owner 型登记刻意不进入账本,因此带 owner clientId 的 detach、重复/未知/匿名的 detach 都无法窃取其他附加者的计数。
扩展会话端点
以下端点扩展了基础生命周期面,多数带对应能力标签:
非阻塞 Prompt(non_blocking_prompt)
POST /session/:id/prompt现在返回 HTTP202及{ promptId, lastEventId },而非阻塞到 prompt 完成。实际结果通过 SSE 的turn_complete/turn_error到达,promptId字段把事件与 202 响应关联起来。DaemonSessionClient.prompt()在存在活动事件订阅时自动走非阻塞路径,并从 SSE 流透明匹配结果。
会话回顾(session_recap)
POST /session/:id/recap请快速模型生成一行"我上次进行到哪里"摘要,返回{ sessionId, recap: string | null };null表示历史过短或模型临时失败。该端点尽力而为(best-effort)。
会话旁问(session_btw)
POST /session/:id/btw在不打断主对话流的前提下,基于会话上下文问一个一次性问题。实现在缓存路径上使用runForkedAgent做单轮、无工具的 LLM 调用,返回{ sessionId, answer: string | null };实现强制执行BTW_MAX_INPUT_LENGTH、跨会话泄漏防护与超时处理。
Shell 命令执行
POST /session/:id/shell直接在 daemon 宿主上执行 shell 命令,不经过 LLM。输出经会话 SSE 总线以user_shell_command/user_shell_result事件流式返回,命令与结果同时注入 LLM 对话历史。响应为{ exitCode, output, aborted }。对活动的次级 workspace 会话,该单一 REST 路由会解析会话所有者并在其 runtime 的桥接层执行,使命令从所属 workspace 的 cwd 启动。该路由不提供路径沙箱;workspace 限定的 ACP 客户端可继续在所属 workspace 连接上使用_qwen/session/shell。
会话回退(Session Rewind)
GET /session/:id/rewind/snapshots与POST /session/:id/rewind解析所属的活动 workspace runtime。持久化会话必须先 load 或 resume 才能回退。Rewind 截断对话历史,并可选地恢复edit与write_file跟踪的文件;不撤销shell 命令、Git、脚本或手工修改。文件恢复是尽力而为的,因此响应可能在历史已移动后报告rewound: false与filesFailed[]。SDK 的 rewind 调用始终使用 owner-aware REST(即使客户端其他时候走 ACP 传输),因为该变更必须保留严格的 REST 认证。
会话分离(Session Detach)
POST /session/:id/detach通过递减attachCount显式分离一个客户端;其本身不关闭会话。若分离后没有其他附加或订阅者残留,会话被回收。端点返回 204。
批量会话删除
POST /sessions/delete接受{ sessionIds: string[] }(最多 100 个 id),关闭桥接会话并删除活动或已归档的 transcript 文件。若同一 id 的活动与归档 JSONL 都存在,硬删除会同时移除两者以清除冲突。它清理活动与归档 worktree sidecar,但保留 file-history 快照、subagent transcript 与 runtime sidecar。使用Promise.allSettled保证韧性,返回{ removed, notFound, errors }。
会话归档(Session Archive)
POST /sessions/archive:把非活动会话 JSONL 从chats/移动到chats/archive/。若目标会话存活,daemon 先进入每会话归档门并执行严格关闭(要求 ACP 子进程 flushChatRecordingService);关闭或 flush 失败时归档保留 JSONL 原样。POST /sessions/unarchive:把归档 JSONL 移回chats/。这只是存储状态迁移;客户端之后必须调用session/load或session/resume。归档会话对 load/resume 返回409 session_archived,与归档迁移竞争的变更返回409 session_archiving。
空、损坏与孤儿常规 transcript 文件即便无法作为对话加载,也仍可参与这些生命周期操作;所有权安全检查可能故意 fail-closed 并需要运维介入。密封握手证明之后被修改的文件抛出SessionTranscriptChangedError;首条超过有界所有权读取窗口的 JSON 形状记录抛出SessionTranscriptIdentityUnavailableError。广告session_storage_conflict_repair能力时,archive/unarchive 接受resolveConflicts: true:归档保留归档副本,取消归档保留活动副本;不传该选项时冲突双方都不会被移动、删除或覆盖,而是出现在批量errors数组中。Workspace 限定的生命周期路由现在使用 HTTP 200 批量信封而非旧的409 session_conflict。
上下文用量(session_context_usage)
GET /session/:id/context-usage返回结构化的上下文窗口用量;?detail=true返回按工具、记忆、技能细分的更细粒度用量。
会话统计(session_stats)
GET /session/:id/stats返回用量统计:模型指标(输入/输出 token、缓存读写、总成本)、每工具调用次数与延迟、文件编辑次数、本会话内每技能调用次数。skills块仅反映本会话内的技能体加载与技能斜杠命令,不是跨会话的活动聚合。
会话任务(session_tasks)
GET /session/:id/tasks返回代理任务、shell 任务、监控任务及其生命周期状态的背景任务快照。由其他子代理派生的代理条目携带可选的血统字段(parentAgentId、parentName、depth),客户端可据此把嵌套子代理渲染成树;负载示例见 qwen-serve-protocol.md。session_monitor_tool_correlation能力额外保证监控条目携带toolUseId,使客户端能把 transcript 工具调用与其任务详情关联。
会话 LSP 状态(session_lsp)
GET /session/:id/lsp为 daemon 客户端返回净化后的每会话 LSP 状态:启停状态、服务器总数聚合、不可用/初始化状态,以及每服务器的name、status、languages、transport、command、error。禁用或不可用的 LSP 以 HTTP 200 状态数据表示,而非传输错误。
压缩回放(Compacted Replay)
POST /session/:id/load现在返回可包含compactedReplay?: BridgeEvent[]、liveJournal?: BridgeEvent[]、lastEventId?: number的BridgeRestoredSession(类型见 bridgeTypes.ts#L528)。这些字段是 daemon 对存活会话的有界内存回放窗口,不是完整 transcript API。默认窗口上限为每个存活会话 4 MiB(--compacted-replay-max-bytes),启动时拒绝非法上限;硬上限 256 MiB。常量定义在 packages/acp-bridge/src/replayWindowLimits.ts#L7-L8,CLI 校验(必须是 [1, 268435456] 的正安全整数)见 packages/cli/src/serve/fast-path.ts#L212-L223。
compactedReplay由TurnBoundaryCompactionEngine产出:在回合边界把连续的文本/思考块折叠、把工具调用序列折叠到最终状态、丢弃瞬时信号,产出 O(turns) 的回放日志而非 O(tokens) 日志(通常 25–30 倍缩减)。当旧回放条目被挤出字节窗口时,compactedReplay[0]是合成的无 idhistory_truncated标记,携带{reason: 'replay_window_exceeded', truncatedEvents, retainedEvents, maxBytes, truncatedTurns?, fullTranscriptAvailable: boolean}。fullTranscriptAvailable为 true 表示客户端可用GET /session/:id/transcript分页读取完整持久化 transcript;false 表示只有有界回放可用。客户端应将其渲染为状态并正常应用保留回放,绝不能触发 resync 循环。回放引擎曾在某点失败时置replayDegraded: true,此时客户端应优先完整 transcript。
ACP 子进程预热(Preheat)
bridge.preheat()仍对显式嵌入者开放;qwen serve启动后也会为兼容性尝试预热受信任的主子进程。预热失败非致命,下一条运行时命令或会话会重试;受信任的次子进程首次使用时才启动。Workspace Runtime 在工作活动期间拥有子进程。在所有会话与管理租约排空后,省略或为 0 的channelIdleTimeoutMs会立即回收子进程;纯预热本身保留给首次使用且不会武装该回收器。正值配置延迟或活动 keepalive 会让子进程在更长的剩余窗口内保持可复用。公开的 Workspace Runtimeensure命令增加可续期的十分钟 workspace 租约,每次成功调用都会重置该窗口(即使 channel 已存活)。
配置项
BridgeOptions.maxSessions(默认 32)—— 会话数上限。BridgeOptions.sessionScope(默认'single';可选'thread')。BridgeOptions.initializeTimeoutMs(默认 10s)—— ACP 子进程启动截止时间(Channel 工厂 +initialize握手)与默认请求超时。BridgeOptions.sessionRestoreTimeoutMs(默认 60s)—— ACPloadSession/unstable_resumeSession截止时间。默认 60s;显式配置的初始化超时可抬高它,但绝不会降低它。BridgeOptions.channelIdleTimeoutMs(未设置或0在运行时工作排空后回收,但纯预热保留给首次使用;正值或活动 keepalive 延迟回收,较长的窗口胜出)。- 能力标签清单:
session_create、session_id_override、session_scope_override、session_load、session_resume、unstable_session_resume(废弃别名)、session_list、session_info、session_close、session_metadata、session_set_model、client_identity、client_heartbeat、session_recap、session_generation、session_btw、session_context_usage、session_tasks、session_monitor_tool_correlation、session_stats、session_lsp、session_resources、session_status、non_blocking_prompt。
CLI 侧对应的 serve 快路径旗标映射见 packages/cli/src/serve/fast-path.ts#L48-L57:--compacted-replay-max-bytes、--channel-idle-timeout-ms、--session-restore-timeout-ms等。
无状态生成(session_generation)
POST /session/:id/generate接受{ "prompt": string },返回请求作用域的 SSE 流,事件为started、可选thinking、delta、done或error。请求不读取对话历史、不记录回合、不暴露工具。ACP 子进程在可用时使用配置的有效快速模型,否则使用会话主模型。
已知限制与注意点
connection.unstable_resumeSession在 ACP 层可能仍不稳定,但 daemon 以session_resume广告已承诺的 v1 路由契约;unstable_session_resume仅保留为废弃兼容别名。- v1没有按客户端驱逐,只有按会话与按订阅者终止。撤销策略在 F 系列 Wave 5 / PR 24。
client_evicted是每订阅者而非每会话的;SSE 订阅者被驱逐的客户端可以重连。- 匿名客户端(无
X-Qwen-Client-Id)在designated与consensus策略下不能投票。
依赖与延伸阅读
- ACP 层:
connection.newSession、connection.unstable_resumeSession、connection.loadSession。 - 03-acp-bridge.md:桥接层整体架构。
- 04-permission-mediation.md:originator 与身份如何驱动权限策略决策。
- 10-event-bus.md:终端帧投递。
- qwen-serve-protocol.md:路由目录(wire reference)。
参考源码
- packages/acp-bridge/src/bridge.ts:
SessionEntry定义与spawnOrAttach、registerClient、resolveTrustedClientId、killSession等核心实现。 - packages/acp-bridge/src/bridgeTypes.ts:
HttpAcpBridge、BridgeSession、BridgeSessionState、BridgeRestoredSession。 - packages/acp-bridge/src/bridgeErrors.ts:
InvalidClientIdError、InvalidSessionMetadataError等错误类型。 - packages/acp-bridge/src/replayWindowLimits.ts:压缩回放窗口默认值与硬上限。
- packages/acp-bridge/src/bridge.test.ts:心跳、元数据校验等生命周期行为测试。
- packages/sdk-typescript/src/daemon/types.ts:
DaemonSession、DaemonSessionSummary等 SDK 类型。 - packages/sdk-typescript/src/daemon/DaemonSessionClient.ts:SDK 客户端(
prompt()的非阻塞路径等)。 - packages/cli/src/serve/fast-path.ts:serve 快路径参数解析与校验。
- packages/cli/src/serve/acp-http/dispatch.ts:断线回收(
requireZeroAttaches)等路由层逻辑。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考