OpenMuse 持久化任务引擎原理:SQL 租约、检查点与中断恢复如何做到任务永不丢失
【免费下载链接】openmuseA personal agent with a browser, terminal, files, and work that keeps going built with CopilotKit and AG-UI.项目地址: https://gitcode.com/gh_mirrors/op/openmuse
OpenMuse 是一个自带浏览器、终端、文件和持续工作的个人智能体,基于 CopilotKit 与 AG-UI 构建。它的持久化任务引擎把每个任务的状态、租约和检查点都写入 SQL 数据库,即使进程崩溃、服务器重启或任务被暂停,工作也不会丢失——这就是"任务永不丢失"的核心承诺。
为什么个人智能体的任务容易"丢"?
大多数智能体的工作是"对话式"的:你发消息,它回复,对话结束状态就留在内存里。一旦进程重启,正在执行的任务、执行到一半的步骤、已经保存的中间结果全部蒸发。
OpenMuse 的解决思路很直接:任务不是内存对象,而是数据库记录。所有任务(tasks)、监控(monitors)、事件(run-events)都落在一张records表里,用 JSONB 存储,配合乐观锁(Compare-And-Swap)保证多 worker 并发下的正确性:
UPDATE records SET data = data || $patch::jsonb WHERE owner=$1 AND kind=$2 AND id=$3 AND data @> $expected::jsonb RETURNING data这段 SQL 是整台引擎的基石——"只有当我看到的数据和我预期的完全一致时才更新",实现了无锁的并发控制。实现见 db.ts。
第一步:任务入队即持久化
当你委派一个任务时,服务端并不是"启动一个内存协程",而是先往数据库插入一条AgentTask记录(初始状态queued)。任务的完整生命周期状态定义在 agent.ts:
| 状态 | 含义 |
|---|---|
queued | 排队等待 worker 领取 |
running | 某个 worker 持有租约,正在执行 |
scheduled | 定时任务,等待nextRunAt到期 |
waiting_input/waiting_approval | 等你补充信息或批准动作 |
paused/succeeded/failed/cancelled | 暂停 / 终态 |
任务创建入口在 service.ts,注意新建任务时leaseId和leaseUntil都初始化为null——没有租约的任务永远处于"可被安全领取"的状态。
核心机制一:SQL 租约(Lease)——谁持有任务说了算
后台的 TaskWorker 每秒扫描一次任务表,找出到期任务(queued、scheduled到点、running但租约已过期、waiting_approval),然后用一次 compare-and-swap 原子地"抢占"任务:
- 生成唯一的
leaseId(UUID) - 写入
leaseUntil = now + 60s - 状态置为
running,attempts + 1
关键代码在 worker.ts。抢占的"期望条件"包含旧的leaseId和旧的leaseUntil,意味着:
- 两个 worker 同时抢同一个任务,只有一个能成功(CAS 天然互斥);
- 租约过期后,任务自动变回"可领取"——这就是崩溃 worker 的兜底。
持有任务后,worker 还会启动一个心跳定时器(间隔为租约时长的一半,约 20 秒),不断续期leaseUntil(worker.ts)。如果心跳的 CAS 失败——说明任务被暂停、取消或被别的 worker 接管——当前执行会被立即abort,不再写任何数据。
核心机制二:检查点(Checkpoint)——每走一步就存档一次
任务执行函数(execute)拿到的不是一个"裸状态",而是一个TaskContext,其中checkpoint(patch)方法(worker.ts)做两件事:
- 验证租约仍在自己手里(CAS 条件里带
leaseId),失败就抛出LostLeaseError; - 把中间进度(
state、evidence、plan、artifactIds等)合并写回数据库。
以文档填写任务为例:找到源 PDF 后立即 checkpoint(service.ts),填完表单生成副本后再 checkpoint(service.ts)。这意味着即使进程在"准备回复邮件"这一步被杀掉,下次恢复执行时直接从数据库读回"PDF 已找到、副本已生成"的状态,不会重复下载或重复填写。
执行中的每个关键节点还会写一条不可变的RunEvent记录,最终在任务详情页按时间线回放给用户(service.ts)。
核心机制三:中断恢复——崩溃、重启、被接管之后怎么办
引擎把"执行中断"分成两类处理(worker.ts):
① 租约丢失(优雅中断):任务被用户暂停/取消、或已被别的 worker 接管。此时不记错误,只是把状态改回queued并清空租约——任务毫发无损,随时可以被再次领取。
② 真实执行错误:记录一条error事件,状态置failed并保存错误详情,同时把本次runs记录标记为interrupted/failed。用户可以在界面上直接重试。
③ 进程级重启(最硬的场景):
- 正在
running的任务租约必然过期,下一轮 tick 会被新 worker 重新领取,从最近的 checkpoint 继续; - 启动时 recoverInterruptedActions() 会把卡在
executing的敏感动作批量标记为outcome_unknown,防止"邮件可能已发出也可能没发出"的歧义被静默吞掉; - 每 60 秒一次的 maintain() 维护循环负责"善后对账":补发漏掉的通知、复活被中断的监控任务、修复"已接受但任务未创建"的 Ideas。
这套机制的验收证据记录在 docs/VERIFICATION.md:真实的 PGlite 重启、双 worker 租约竞争、过期租约恢复、暂停/恢复、审批公平性等场景都有自动化测试覆盖。
完整生命周期一图流
创建任务 ──▶ queued(入库) │ worker tick 每秒扫描 │ CAS 抢占成功(写入 leaseId + leaseUntil) ▼ running ──心跳续租(~20s)──▶ checkpoint 存档 ──▶ 完成:succeeded │ │ 租约丢失/被接管 ──▶ 回 queued 等待重领 真实错误 ──▶ failed + 错误事件 │ 进程崩溃 ──▶ 租约过期 ──▶ 下一个 worker 重新领取,从检查点继续动手阅读源码
- 存储层与乐观锁:db.ts
- 租约、心跳与恢复:worker.ts
- 任务编排与检查点示例:service.ts
- 任务状态机定义:packages/domain/src/agent.ts
- 功能总览与验收记录:docs/FEATURES.md、docs/VERIFICATION.md
💡一句话总结:OpenMuse 的"永不丢失"不靠运气,而是靠三条纪律——状态永远在数据库里、写入必须带租约校验、恢复必须从检查点开始。理解了这三点,你就理解了绝大多数可靠任务系统的骨架。
【免费下载链接】openmuseA personal agent with a browser, terminal, files, and work that keeps going built with CopilotKit and AG-UI.项目地址: https://gitcode.com/gh_mirrors/op/openmuse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考