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.jsonl
  • runs/
  • lock 文件。

2. Cron 数据模型

主要类型:

  • CronSchedule
  • CronPayload
  • CronRunRecord
  • CronJobState
  • CronJob
  • CronStore

位置:

  • 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。

三种任务对照

类型谁创建主要输入结果
用户 reminderCron Tool/用户session-bound message回原 Session/Channel
HeartbeatGateway 系统配置HEARTBEAT.md最近活跃目标
DreamGateway 系统配置或 /dreamMemory history + 长期文件更新长期 Memory

运行前提

以下功能要求 nanobot gateway 持续运行:

  • 用户 Cron 到点执行;
  • Heartbeat;
  • 周期 Dream;
  • Channel 长连接;
  • WebUI/WebSocket。

只运行 nanobot agent -m 不会让后台调度器持续存在。

观察任务

  1. 启动隔离 Gateway;
  2. 创建一个短延迟 reminder;
  3. 查看 <workspace>/cron/jobs.json;
  4. 查看 run record;
  5. 在原 Session 进行长请求,观察 Cron 是否被延后;
  6. 检查 system job 是否受保护;
  7. 停止 Gateway,确认后台任务不会自行运行。

源码与测试

  • nanobot/cron/types.py
  • nanobot/cron/service.py
  • nanobot/cron/bound_runner.py
  • nanobot/cron/session_turns.py
  • nanobot/agent/cron_turns.py
  • nanobot/cli/commands.py::_run_gateway
  • tests/cron/
  • tests/agent/test_loop_cron_timezone.py
  • tests/command/test_builtin_dream.py

自检

  1. 为什么 Cron store 必须位于 workspace?
  2. 解析 jobs.json 失败时为什么不能返回空任务列表并继续保存?
  3. 用户 Cron 为什么需要 origin channel 和 Session?
  4. Heartbeat 与普通 reminder 有什么权限差异?
  5. Cron deferred queue 与用户 pending injection 有什么不同?

下一步

阅读 Subagent 生命周期。