ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

qwen-code 定时任务绑定空会话前的持久化设计:让无对话记录的空会话在守护进程重启后可恢复

2026/9/12 11:05:52 拓冰建站 浏览量
qwen-code 定时任务绑定空会话前的持久化设计:让无对话记录的空会话在守护进程重启后可恢复 qwen-code 定时任务绑定空会话前的持久化设计让无对话记录的空会话在守护进程重启后可恢复【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读本文聚焦 qwen-code 中定时任务scheduled task与既有会话绑定场景下的一个关键缺陷修复通过 REST 接口创建的、尚无任何对话记录的空会话在绑定定时任务后一旦客户端断开、守护进程重启将因缺少 transcript 而无法被 keepalive 恢复导致任务悬空。本文完整解读对应的设计文档 docs/design/2026-08-26-scheduled-task-empty-session-persistence.md并结合 scheduled-tasks.ts、bridge.ts、acpAgent.ts 与 chatRecordingService.ts 等源码讲清持久化空会话默认源元数据这一方案的动机、实现、错误语义与验证方式。读完你将掌握 qwen-code 定时任务会话绑定链路的完整原理以及零模型调用、零新 schema 恢复空会话的设计技巧。背景定时任务如何绑定一个既有会话qwen-code 的定时任务体系REST 路由与cron_create工具允许两种会话来源省略会话omitted-session创建由 REST 路径直接创建一个由任务独占task-owned的分离会话其sourceType被标记为scheduled_task参考 scheduled-tasks.ts绑定既有会话existing sessioncreateScheduledTaskWithExistingSession接收一个sessionId将该会话与持久化任务绑定任务以sessionOwnedByTask: false记录参考 scheduled-tasks.ts。绑定既有会话的典型场景是用户希望定时任务继续使用当前正在活跃的空闲会话任务触发时在该会话上下文内执行 prompt。问题无 transcript 的空会话在重启后无法恢复缺陷链路REST 路由可以绑定一个已存在的、空闲的 default 会话。但存在一个边界情况客户端通过POST /session创建会话时未携带sourceType且未附带 prompt该会话此时没有任何 transcript 记录绑定成功持久化任务写入了该 session id客户端分离detach随后守护进程daemon重启重启后的 keepalive 恢复流程无法恢复缺失的 transcript——会话不可用但任务仍然绑定着这个不可用的会话 id形成悬空任务。也就是说会话在可恢复这一点上依赖 transcript 中存在可被恢复的持久化记录一个从未产生过任何记录的空会话跨重启后没有任何恢复锚点。为什么不能走先让用户发条消息或伪造 prompt设计文档明确否定了两种直觉方案要求用户先发送消息这会把持久化实现细节泄漏到产品流程里用户被迫执行一个与任务无关的动作发送合成 promptsynthetic prompt副作用更严重——会调用模型、执行 hooks、消耗 token并在对话中留下可见内容污染会话历史。设计目标因此被约束为在不产生任何用户消息、模型调用、hook 执行、不引入新 transcript schema 的前提下让空会话获得可恢复的持久化锚点。设计绑定前通过 workspace bridge 持久化默认源元数据核心思路在 REST 路径提交显式命名既有会话的任务之前所解析工作区对应的 workspace bridge 会请求该会话的子进程持久化记录其默认源default source元数据。其关键点在于复用两套既有机制既有的session_sourcetranscript 记录ChatRecord的system类型、session_source子类型ChatRecordingService的**写入者租约writer lease**机制。由此得到的结果是一个可恢复的 transcript其诞生过程不包含用户消息、不调用模型、不触发 hooks、不需要任何新 transcript schema。桥接操作是刻意窄化的设计上这个 bridge 操作被有意限制到最小范围只对活跃live会话持久化sourceType: default只有在子进程回执persisted: true后才算成功由于路由层已经只允许无 parent、无 sourceId、sourceType 为 default的会话进入该分支其他会话种类根本不会到达此操作。与既有检查的顺序关系持久化操作运行在任务文件锁task-file lock之前锁内的既有资格检查assertReusableScheduledTaskSession仍然是最终裁决者持久化之后还有一道generation 检查如果工作区 generation 正在排空draining则不允许提交该任务防止把任务绑定到正在退役的工作区上。源码级实现从 REST 路由到子进程记录第一步路由内的资格预检在 scheduled-tasks.ts 中assertReusableScheduledTaskSession会依次拒绝场景错误码说明会话有挂起的交互/活跃 promptsession_busy先解决挂起交互再绑定会话已保留给定时任务session_already_bound禁止重复保留会话有 parentSessionId、sourceId 或 sourceType 非 defaultsession_source_ineligible该来源不能拥有定时任务也就是说能够进入持久化流程的会话必须是default source、无 parent、无 sourceId的普通活跃会话。第二步REST 分支调用 bridge 持久化createScheduledTaskWithExistingSession中options.source rest分支在写任务文件之前执行持久化scheduled-tasks.tsif (options.source rest) { const ensurePersisted target.bridge?.ensureDefaultSessionPersisted; if (!ensurePersisted) { throw new ExistingSessionScheduledTaskCreateError( 409, session_binding_unavailable, Session persistence is not available for this workspace, ); } try { await ensurePersisted.call(target.bridge, input.sessionId); } catch (error) { target.assertGenerationOpen?.(); if (error instanceof SessionNotFoundError) { throw new ExistingSessionScheduledTaskCreateError( 404, session_not_found, Session ${input.sessionId} was not found, ); } // 记录 stderr 诊断日志 throw new ExistingSessionScheduledTaskCreateError( 500, session_persistence_failed, Failed to persist the requested session, ); } target.assertGenerationOpen?.(); }可以看到三次assertGenerationOpen?.()调用分别出现在持久化前、异常路径、成功后配合锁内updateCronTasks的assertCanCommit与失败时的rollbackCronMutation构成完整的 generation 一致性防护。第三步bridge 向子进程发起sessionSource控制调用workspace bridge 的ensureDefaultSessionPersisted实现在 bridge.tsasync ensureDefaultSessionPersisted(sessionId) { const entry byId.get(sessionId); if (!entry) throw new SessionNotFoundError(sessionId); const result (await withTimeout( Promise.race([ entry.connection.extMethod(SERVE_CONTROL_EXT_METHODS.sessionSource, { sessionId, sourceType: default, }), getTransportClosedReject(entry), // 连接关闭时提前失败 ]), initTimeoutMs, ensureDefaultSessionPersisted, )) as { persisted?: unknown }; if (result?.persisted ! true) { throw new Error(Session ${sessionId} could not be persisted); } }要点会话不存在未注册到 bridge 的byId表时抛出SessionNotFoundError被路由映射为 404session_not_found通过SERVE_CONTROL_EXT_METHODS.sessionSource即qwen/control/session/source见 status.ts扩展方法调用子进程Promise.race与withTimeout双保险传输关闭或超时都会终止等待只有回执persisted true才算成功否则一律按失败处理。第四步子进程侧 ACP handler 落盘子进程daemon 侧 agent在 acpAgent.ts 处理sessionSource控制方法case SERVE_CONTROL_EXT_METHODS.sessionSource: { // 校验 sessionId / sourceTypeparseSessionSource // reserved standalone 类型仅允许 daemon 拥有会话创建时使用 const recording session.getConfig().getChatRecordingService(); let ok false; if (recording) { ok await recording.recordSessionSource(source.sourceType, source.sourceId); } return { sessionId, sourceType: source.sourceType, persisted: ok }; }第五步ChatRecordingService幂等落盘核心落盘逻辑在 chatRecordingService.ts 的recordSessionSourceasync recordSessionSource(sourceType: string, sourceId?: string): Promiseboolean { if (this.currentSourceType ! undefined) { // 已记录过幂等——相同源直接成功不同源返回 false return this.currentSourceType sourceType this.currentSourceId sourceId; } // 追加 system/session_source 记录appendRecordStrict 保证严格顺序写入 const record: ChatRecord { ...this.createBaseRecord(system), type: system, subtype: session_source, systemPayload: { sourceType, ...(sourceId ! undefined ? { sourceId } : {}) }, }; await this.appendRecordStrict(record); this.currentSourceType sourceType; this.currentSourceId sourceId; return true; // 持久化成功后返回 }这段实现直接印证了设计文档的两点声明幂等性一旦currentSourceType已设置重复记录相同源会直接成功、不再追加第二条记录——因此已填充会话重复绑定天然幂等持久化语义appendRecordStrict与 writer lease 保证记录是严格落盘的persisted: true不会出现在一次静默失败的写入之后。失败语义与错误映射失败场景HTTP 状态错误码含义bridge 未捕获ensureDefaultSessionPersisted能力409session_binding_unavailable该工作区不提供会话持久化能力fail closed会话在 bridge 侧已消失404session_not_found会话不存在持久化写失败或回执非persisted: true500session_persistence_failed未能持久化会话并同步输出 stderr 诊断行关于持久化成功但任务写入失败的取舍设计文档明确了一个重要的不做回滚决策如果持久化成功、但随后的任务文件写入失败无害的 default-source 记录会保留。理由是该会话是**调用方所有caller-owned**的删除其 transcript 记录不安全——你无法确认删掉的是刚才这条 source 记录还是用户已有的历史记录。作用域与不变式selected-runtime 专用、不回退主运行时该持久化操作是selected-runtime 作用域的只使用任务所解析工作区捕获的 bridge绝不回退fall back到 primary runtimebridge 能力缺失时按上述session_binding_unavailable关闭失败。这一点与scheduled-tasks.ts中的ScheduledTaskTarget.bridgescheduled-tasks.ts类型定义一致ensureDefaultSessionPersisted?(sessionId: string): Promisevoid是可选的、按工作区注入的能力。哪些路径保持不变设计文档明确划定了三条不改动的边界受信任的cron_create当前会话路径它运行在活跃 prompt 之内transcript 天然已存在若在这个私有回调里再做一次 daemon→子进程的持久化往返会引入不必要的重入re-entrancy风险因此保持原样省略会话的 REST 创建仍然创建sourceType: scheduled_task的任务独占分离会话与既有行为一致从未被显式绑定的普通会话不会被提前持久化避免无谓的磁盘写入。兼容性与范围声明无 schema 变更任务 schema、REST 请求 schema、能力开关capability flag、UI 流程、transcript 记录格式全部不变幂等已有填充记录的会话因recordSessionSource的幂等短路重复绑定不会产生重复记录不自动修复历史孤儿任务重启后无法安全区分从未持久化的会话与被故意删除或损坏的 transcript故不做自动修复副作用说明被显式绑定的空会话持久化后会出现在会话历史中——这是设计上接受的结果因为调用方已显式让该会话成为定时任务的持久化所有者。验证单元测试 真实进程回归设计文档要求的验证覆盖与仓库中的测试实现一一对应bridge 确认bridge.test.ts 覆盖ensureDefaultSessionPersisted的成功回执、失败回执与会话缺失SessionNotFoundError路径失败持久化与 REST 排序/错误映射scheduled-tasks.test.ts 中通过 stub bridge 分别验证能力缺失session_binding_unavailable见delete (h.bridge as PartialStubBridge).ensureDefaultSessionPersisted、持久化抛错session_persistence_failed等分支同时验证了持久化发生在任务文件写入之前的次序cron-tool 路径不变断言 cron-tool 来源不触发持久化往返。此外设计文档描述了一个真实进程回归测试创建一个空的无源会话 → 绑定定时任务 → 分离客户端 → 重启 daemon → 验证同一会话被恢复且全程未产生任何 user/model 消息。该回归直接证明了核心价值空会话的可恢复性不再依赖用户主动发消息或模型调用。总结定时任务绑定空会话前先持久化默认源元数据这一设计用一次窄化的、幂等的、复用既有 transcript 机制的bridge 调用闭环了一个跨重启的数据完整性问题。它的工程启示在于复用而非新建不新增 transcript schema不新增能力开关只复用session_source记录与 writer lease无副作用恢复空会话获得恢复锚点不付出模型调用、hook、token 代价失败关闭fail closed能力缺失、会话消失、写失败都有明确错误码且绝不回退到不安全的运行时边界清晰cron-tool 私有回调、省略会话创建、未绑定普通会话三个路径保持不变避免重入风险与无谓写入。对需要理解 qwen-code 定时任务与会话生命周期关系的读者建议顺带阅读 scheduled-tasks.test.ts 与 chatRecordingService.ts 中的recordSessionSource可更完整地看到资格预检 → bridge 持久化 → 锁内复检 → generation 守卫 → 落盘提交的完整链路。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考