Cron 与 Heartbeat
对应源码
| 主题 | 源码 |
|---|---|
| Cron 数据模型 | nanobot/cron/types.py |
| 调度、持久化和执行 | nanobot/cron/service.py::CronService |
| 绑定 Agent 的运行边界 | nanobot/cron/bound_runner.py、nanobot/cron/session_turns.py |
| 活跃 Session 协调 | nanobot/agent/cron_turns.py::CronTurnCoordinator |
| Gateway 生命周期接入 | nanobot/cli/commands.py::_run_gateway |
学习目标
读完后,你应当能够:
- 说明 Cron job 保存在哪里;
- 区分用户 reminder、Heartbeat 和 Dream;
- 解释 session-bound Cron 如何重新进入 AgentLoop;
- 说明活跃 Session 时为什么要延后 Cron Turn。
前置知识
- 理解 Gateway;
- 理解 InboundMessage 和 Session Key;
- 知道基本的 interval/cron schedule。
1. CronService 是 workspace-scoped
Gateway 创建:
<workspace>/cron/jobs.json并将其传给 CronService。
切换 workspace 会看到不同的 Cron jobs,这是设计行为,不是任务丢失。
Cron 目录还可能包含:
action.jsonlruns/- lock 文件。
2. Cron 数据模型
主要类型:
CronScheduleCronPayloadCronRunRecordCronJobStateCronJobCronStore
位置:
nanobot/cron/types.py
Schedule 支持的形式由当前源码决定,包括:
- 一次性时间;
- every interval;
- cron expression。
3. 原子保存和损坏保护
CronService 保存 jobs.json 时使用:
临时文件
→ flush + fsync
→ os.replace
→ 目录 fsync(Windows 可跳过)如果现有 store 无法解析:
- 保留损坏文件;
- 使用
.corrupt-<timestamp>等策略; - 不把解析失败静默当成空列表后覆盖原任务。
这个错误处理保护的是“解析失败不等于没有任务”。
4. 用户 Cron job 必须绑定 Session
Agent Turn 类型的用户任务需要完整投递上下文:
- session key;
- origin channel;
- origin chat id;
- channel metadata。
无法绑定具体 Session 的旧任务会被迁移或禁用,避免任务执行后不知道结果应发给谁。
5. Session-bound Cron Turn
run_bound_cron_job() 将 Job 转成普通 InboundMessage:
flowchart LR CS["CronService 到点"] --> BR["run_bound_cron_job"] BR --> IM["InboundMessage<br/>带 cron metadata 和 session override"] IM --> CC["CronTurnCoordinator.submit"] CC --> AL["AgentLoop"] AL --> OM["OutboundMessage"] OM --> REC["记录 run / 投递来源 Channel"]
因此 Cron 执行仍复用正常:
- ContextBuilder;
- Provider;
- Tool;
- Session;
- Runtime Event。
6. 为什么活跃 Session 时要延后
如果目标 Session 正在处理用户 Turn,Cron 同时进入会:
- 与用户输入竞争;
- 改变历史顺序;
- 让 proactive reply 插到错误位置。
CronTurnCoordinator 可以根据 metadata 将 Cron Turn 放进 deferred queue,等 Session idle 后逐个发布。
这与普通用户 follow-up 的 pending injection 不同:
- follow-up 可能需要进入当前 Turn;
- Cron 通常应等当前用户 Turn 完成。
7. Heartbeat
Heartbeat:
- 是受保护的 system job;
- 读取
<workspace>/HEARTBEAT.md; - 重点检查
## Active Tasks; - 由 Gateway 持续运行;
- 使用同一个 CronService;
- 将有用结果发送到最近活跃聊天目标;
- 不能像普通用户 job 一样被 Cron Tool 删除。
启用配置:
gateway.heartbeat.enabled
具体字段参考 官方 Gateway Heartbeat 配置。
8. Dream
Dream 也是 system job,但执行路径不同:
- 由 Gateway 的 Cron callback 识别
job.name == "dream"; - 直接构造 Dream prompt 和专用运行;
- 不通过普通用户任务 payload;
- 更新长期 Memory。
三种任务对照
| 类型 | 谁创建 | 主要输入 | 结果 |
|---|---|---|---|
| 用户 reminder | Cron Tool/用户 | session-bound message | 回原 Session/Channel |
| Heartbeat | Gateway 系统配置 | HEARTBEAT.md | 最近活跃目标 |
| Dream | Gateway 系统配置或 /dream | Memory history + 长期文件 | 更新长期 Memory |
运行前提
以下功能要求 nanobot gateway 持续运行:
- 用户 Cron 到点执行;
- Heartbeat;
- 周期 Dream;
- Channel 长连接;
- WebUI/WebSocket。
只运行 nanobot agent -m 不会让后台调度器持续存在。
观察任务
- 启动隔离 Gateway;
- 创建一个短延迟 reminder;
- 查看
<workspace>/cron/jobs.json; - 查看 run record;
- 在原 Session 进行长请求,观察 Cron 是否被延后;
- 检查 system job 是否受保护;
- 停止 Gateway,确认后台任务不会自行运行。
源码与测试
nanobot/cron/types.pynanobot/cron/service.pynanobot/cron/bound_runner.pynanobot/cron/session_turns.pynanobot/agent/cron_turns.pynanobot/cli/commands.py::_run_gatewaytests/cron/tests/agent/test_loop_cron_timezone.pytests/command/test_builtin_dream.py
自检
- 为什么 Cron store 必须位于 workspace?
- 解析
jobs.json失败时为什么不能返回空任务列表并继续保存? - 用户 Cron 为什么需要 origin channel 和 Session?
- Heartbeat 与普通 reminder 有什么权限差异?
- Cron deferred queue 与用户 pending injection 有什么不同?
下一步
阅读 Subagent 生命周期。