最近被问得最多的一个问题是:OpenClaw 里那个HEARTBEAT.md到底是个什么文件?为什么每次 agent 一卡住、一崩溃、一锁死,所有人第一个想到的就是去看它?更有意思的是,网上都在传 OpenClaw 有个“自愈机制”,但又没人能说清楚它到底怎么工作。
我自己的答案是:没有HEARTBEAT.md,就没有真正的自愈。这个文件表面上看只是一个普通的 Markdown 记录,实际上它是 agent 的“生命体征登记表”,是整个 OpenClaw 能在掉线、重启、崩溃之后恢复过来的地基。
这篇文章我不打算讲概念,直接结合部署和日常使用场景,把这文件拆开聊透,顺便把自愈机制的实现链路、配置方法和踩坑经验一起端出来。无论你是刚在 Windows 上跑通一键部署,还是已经在 Ubuntu 上折腾过 session file locked 的老手,应该都能从这里拿到点新东西。
1. 先搞清楚 OpenClaw 的运行框架
1.1 OpenClaw 到底是什么,为什么需要状态文件
OpenClaw 是一个偏向个人助理定位的开源 agent 项目,你可以把它理解成一个能接多个聊天渠道、能调工具、能自己记忆上下文的“数字分身”。它在 Windows、Linux 甚至 NAS 上都能跑,社区里经常有“openclaw 本地一键部署”“飞牛安装 openclaw”这类讨论,热度一直在涨。
但很多人忽略了一个关键点:OpenClaw 不是一次只跑一个任务的脚本,它是一个长期运行的常驻进程。长期运行意味着它会被打断,比如电脑重启、网络断连、某个外部 API 超时、甚至系统 OOM 把进程杀掉。一旦中断,agent 内部的内存状态全部丢失。
如果这是一个只处理单次请求的无状态服务,丢了也就丢了。但 OpenClaw 不同,它要连续管理多个 channel,要记录每个会话上下文,还要跨时间维护 agent 的身份一致性。要支撑这些东西,必须把一部分状态固化成文件。HEARTBEAT.md就是这套持久化状态里最核心的一个入口。
1.2 一次会话完整生命周期里,文件扮演什么角色
我从实际运行的角度梳理一下一次完整的 agent 会话生命周期:
- 启动阶段:OpenClaw 进程启动后,先检查
HEARTBEAT.md是否存在。如果存在且内容有效,说明上次是异常退出,进入恢复流程。如果是全新部署,则创建一份初始心跳文件。 - 运行阶段:每次处理消息、调用工具、切换 channel 之后,agent 都会更新心跳状态。它记录的不只是时间戳,还包括当前会话 ID、待处理任务、上下文引用等。
- 中断阶段:进程意外退出,内存被清空。但
HEARTBEAT.md里的最后状态还在。 - 恢复阶段:重启后,agent 读取心跳文件,判断上次执行到哪一步,决定是重试、继续还是放弃。
也就是说,HEARTBEAT.md不只是给你看的日志,它是 agent 恢复记忆的唯一线索。这也是为什么很多人说,看懂了它的内容,就懂了 OpenClaw 一半。
2. HEARTBEAT.md 文件深度拆解
2.1 HEARTBEAT.md 的使命:不只是“心跳”
字面上看,“heartbeat”就是心跳,用来告诉系统“我还活着”。但 OpenClaw 里这个文件承担的东西比心跳多得多。它更像一个“状态锚点”,既要表示当前是否正常,又要记录在异常时如何找回应有的现场。
我见过不少新手打开这个文件,看到里面只有几行简单的字段,就以为它只是个标记。实际运行中,它的使命有三条:
- 存活检测:最后一次更新的时间戳,用来判断 agent 是否失联。
- 断点续传:保存当前活动会话、挂起任务,让重启后的 agent 能对上号。
- 一致性校验:通过记录启动次数、进程 PID、任务 hash 等,防止重复消费或脏数据恢复。
举个生活化的例子:这就像你出门前在玄关贴了张便利贴,写清楚“炉子上在炖汤,20分钟后关火”。人走了,灶台还在;后来你回来看到便利贴,才知道厨房里发生了什么。HEARTBEAT.md就是这张便利贴,只不过它是给 OpenClaw 自己看的。
2.2 文件格式、写入时机和字段含义
不少初次接触的朋友以为这是人工写的文档,其实它完全由程序生成和更新。以我本地部署的默认版本为例,结构大致长这样:
# OpenClaw Heartbeat last_update: 2025-02-14T10:23:45.678Z pid: 37264 active_session: a8f3c1 pending_tasks: 2 recovery_count: 3 last_channel: microsoft-teams context_ref: ./memory/session_a8f3c1.md这些字段不是摆设:
last_update是核心,所有超时判断都基于它。pid记录进程号,用来识别上次运行的实体。active_session指向当前会话 ID,和 memory 目录下的会话文件对应。pending_tasks表示还有没做完的挂起任务,恢复后优先处理。recovery_count是已经触发过几次自愈恢复,这个字段能帮你判断系统是否陷入循环重启。context_ref直接给恢复流程指路,省去重新扫描上下文的成本。
写入时机也有讲究。Intensive 操作之前会写一次,操作完成之后会再写一次,比如调用工具前、发送消息后、切换 channel 时。这样设计的目的就是尽量减少丢失的“窗口期”。就算在两次写入中间崩了,最多丢失一小段操作进度,而不会丢掉整个会话身份。
2.3 为什么用 Markdown 而不是数据库
这是个很自然的问题。按照传统工程思维,这种状态数据应该放 SQLite 或者 Redis。OpenClaw 偏偏选了 Markdown,而且文件名就叫.md,很多人不理解。
从实际使用看,这有几个现实理由:
- 可读性强。你可以直接用编辑器打开,能看到完整状态;排查问题时不依赖额外客户端工具。
- 方便日志审计。Markdown 本身就是给人看的,出现异常时可以快速浏览变更历史。
- 避免过度工程。agent 的状态文件通常不会非常大,用数据库反而增加维护负担,也违背了项目“轻量化”的调性。
- 便于迁移和同步。可以直接丢进 Obsidian 等知识库工具里,和记忆文件放在一起管理。
虽然 Markdown 在并发控制上有天然劣势,但 OpenClaw 通过“单进程独占写入 + 文件锁机制”来规避这个问题。后面会聊到一个非常常见的报错session file locked,本质上就是这个锁机制在起作用。
3. 自愈机制是怎么实现的
3.1 自愈机制的三个层级
很多人以为自愈就是把进程拉起来重启,这太低估它了。OpenClaw 的自愈机制至少分成三个层级:
- 进程级自愈:检测 agent 进程消失、无响应,自动重新拉起。
- 任务级自愈:进程还在,但某个任务卡死,通过超时判断和心跳比对,重置任务状态。
- 上下文级自愈:上次会话的上下文文件损坏或缺失,基于
HEARTBEAT.md的引用重新构建可用上下文。
这三个层级是递进的。底层拉起了进程,不代表上层状态就正确。很多部署方案的坑都在这里:进程确实重启了,但 agent 完全忘了自己在干嘛,结果造成连环错误。
所以 OpenClaw 把恢复逻辑跟HEARTBEAT.md深度绑定。进程重启后,不是简单“新开一个 agent”,而是让新进程继承心跳文件里的记忆线索。这就是它和我以前用过的很多“伪自愈”方案最本质的区别。
3.2 从 HEARTBEAT.md 触发自愈的完整链路
我直接按实际执行顺序写一遍自愈链路,方便你对照自己部署后的表现:
- 任何外部监督器(systemd、Windows 计划任务、Docker restart、或者 OpenClaw 内置 supervisor)发现进程退出了。
- 监督器重新拉起进程,并传入
--recover之类的启动参数。 - 新进程启动后,第一时间读取
HEARTBEAT.md。 - 解析
last_update与当前时间差。如果超过阈值,则视为“上次心跳已过期”。 - 收集
active_session、context_ref、pending_tasks等字段。 - 按优先级恢复:先恢复会话身份,再恢复上下文文件,最后重新挂起任务。
- 全部恢复成功,更新
HEARTBEAT.md中的pid、last_update、recovery_count,然后继续对外服务。
整个链路里,第 4 步的“心跳过期判定”是核心中的核心。你经常在日志里看到的session file locked或 timeout 报错,往往就出现在第 3 步和第 4 步之间的文件锁冲突上。
3.3 自愈机制的边界:哪些能自愈,哪些不能
自愈机制不是万能的。我用下来发现它有几个明确的边界:
- 能自愈:进程崩溃、网络闪断导致的连接断开、外部 API 超时、本地临时文件锁残留。
- 不能自愈:配置错误(比如 channel 的 token 失效)、严重的数据文件损坏(Markdown 结构都坏了)、长时间死锁导致系统资源耗尽。
这种边界意识很重要。很多人一遇到问题就以为是自愈机制没生效,实际上从recovery_count字段就能判断出来。如果数值持续增长,说明进程反复重启且每次都失败,这种时候应该去看配置,而不是盲目调大 timeout。
4. 实操:部署 OpenClaw 时让自愈机制真正生效
4.1 安装 Linux/Windows 时怎么处理 HEARTBEAT.md
因为HEARTBEAT.md是自愈的前提,部署阶段就要给它安排合适位置。我在 Ubuntu 上部署时通常把工作目录放在/opt/openclaw/,心跳文件就在根目录下。Windows 部署则建议放在专门的数据目录,比如C:\Users\你的用户名\openclaw_data\,避免权限问题导致心跳文件写不进去。
安装过程中最容易犯的错是“只启动不检查”。我建议你装完先手动跑一次,确认能生成HEARTBEAT.md再配置成自动服务。如果文件根本没生成,说明安装目录或权限有问题,后面一切自愈都无从谈起。
Linux 下如果使用 systemd,一定不要把用户设置成nobody,否则进程可能没有权限更新工作目录里的文件。我见过不少permission denied导致自愈失败的案例,最后都是因为 systemd 配置里少了User=openclaw或者WorkingDirectory=没写对。
4.2 接入 channel(Teams、千问、OBSIDIAN)时的 watchdog 配置
自愈机制能不能处理好 channel,就看你有没有把 channel 与心跳文件正确关联。拿最常讨论的openclaw 如何接入 microsoft teams来说,Teams 的消息会绑定到某个会话 ID。当 agent 崩溃后,如果自愈恢复时找不到原会话 ID,Teams 那边的新消息就会被当成新会话,之前的上下文接不上。
我在配置 Teams 时,会额外开一个 watchdog 脚本,每 30 秒检查一次last_update字段。如果超过 60 秒没有更新,就执行一次重启。为什么是 60 秒?这个值不是拍脑袋定的,它至少要大于 agent 调用外部工具的最大耗时,否则会出现“真在做任务但心跳来不及更新”的误杀。
接入通义千问或者 Obsidian 的方法类似,重点都在心跳文件里的last_channel和context_ref。接入多个 channel 时,尽量只使用一个主工作目录,避免每个 channel 各生成一份心跳,否则自愈时容易选错上下文。
4.3 解决“session file locked”类问题的经验
agent failed before reply: session file locked (timeout 60000ms)这段报错,几乎每个部署 OpenClaw 的人都会遇到。它本质上是两个进程抢同一个状态文件造成的锁冲突。
在你用 systemd 或 Windows 服务方式运行时,最常见的原因是上一次进程没有被完全杀死,端口和文件锁还残留。Linux 下可以用ps -ef | grep openclaw查一下,看到残存的进程先kill,再启动。Windows 下则要在任务管理器里确认服务已经退出,不要直接双击重复运行。
还有一种隐蔽情况:多个 channel 同时唤醒 agent,触发了并发写。默认的 60000ms 超时就是为了防止无限等待。我的处理方法是把 heartbeat 的写入逻辑通过文件锁串行化,同时降低 heartbeat 的更新频率。如果一个会话要执行多步操作,并不是每步都写磁盘,而是等关键节点再统一更新,这样可以明显减少锁竞争。
5. 常见问题与排查技巧实录
5.1 自愈失效的几类典型场景
我在测试和实际使用中,遇到过不下十次自愈失效,总结下来最容易出问题的是这几类:
第一,进程重启了但HEARTBEAT.md没更新。这通常是因为旧进程在退出前把心跳文件写坏了,比如写入了一半就崩溃。恢复时如果只是简单读取,就会拿到残缺数据。我的经验是养成“先重命名再写入”的习惯,也就是写一个临时文件,成功后再替换原文件,避免中断留脏数据。
第二,recovery_count一直在涨,但每次恢复后还是无法正常工作。这种时候别纠结自愈逻辑了,直接去看配置文件和外部服务状态。我遇到过因为网络代理环境变化导致外部 API 请求失败,自愈怎么拉都拉不回来,最终是调整了网络设置才解决。
第三,多个 OpenClaw 实例共用了同一份心跳文件。这样会导致一个实例重启,另一个实例也认为自己应该接管,互相踩踏。排查方法很简单,检查各实例的pid字段,如果你发现心跳文件里的进程号和实际进程号对不上,说明被多实例共用了。
5.2 排查 HEARTBEAT.md 问题的速查表
我把日常排查要点整理成一张表,便于你遇到问题直接对照:
| 现象 | 排查方向 | 常见处理 |
|---|---|---|
last_update很久未更新 | 进程存活状态、工作目录权限 | 检查进程是否被杀、确认目录可写 |
报错session file locked | 是否有残留旧进程、多实例并发 | 清理旧进程,串行化心跳写入 |
| 恢复后丢失会话上下文 | context_ref指向的文件是否完整 | 检查 memory 目录下会话文件 |
recovery_count异常增长 | 配置错误或外部依赖故障 | 检查 channel 配置与网络状态 |
| 心跳文件内容乱码或半截 | 上次写入被中断 | 改为临时文件原子替换 |
| 自愈后重复回复消息 | 任务完成状态没同步 | 确认会话文件里的任务状态 |
这张表是我自己的经验索引,不一定覆盖所有情况,但基本够你处理 90% 的日常问题。
6. 让自愈机制更健壮的几个额外建议
6.1 定期备份 HEARTBEAT.md 和同级记忆文件
虽然自愈机制能恢复大部分状态,但前提是文件本身还在。磁盘损坏、误删、写坏,都可能让自愈无处下手。我现在的做法是每天凌晨把整个数据目录打包一次,保留最近 7 天。成本很低,收益却很大,至少能让你在严重事故后回到“昨天的状态”。
你可以把备份命令加到 crontab 里,也可以挂到onCalendar任务里,具体无所谓,重要的是别备份正在被修改的中间态。我会先复制到另一个目录再打包,确保得到的是完整一致的数据。
6.2 在多个 channel 间共享心跳状态时注意隔离
如果你和我一样同时接 Teams、千问、Obsidian,尽早给自己定一条规则:一个 agent 实例只服务一个主 channel 加最多一个辅助 channel。channel 数量越多,自愈时锁竞争越明显,恢复后的上下文碰撞概率也越高。
我不推荐在一个实例里塞五六个 channel。OpenClaw 虽然支持多 channel,但自愈机制的设计思路更偏向“尽量少的共享状态”。多开几个实例、每个实例独立工作目录,反而更稳。最关键的是,每个实例必须有自己独立的HEARTBEAT.md,千万别共用。
6.3 用监控脚本做一个“人工兜底”
我部署 OpenClaw 时,除了系统自带的服务重启策略,还会额外写一个 15 秒轮询的心跳检查脚本。逻辑很简单:如果last_update超过 90 秒没变,就强制重启进程。
这个脚本的作用不是替代自愈,而是给自愈兜底。万一某个错误让 agent 进入假死状态,占着进程但不写心跳,内置机制可能很难判断是真在忙还是真死了。有外部兜底的话,至少能强制打断假死。脚本不用太复杂,Shell 或 PowerShell 十几行就能写完。
注意:心跳超时的判断要留足余量,尤其是调用千问等模型 API 时,长思考模式可能超过 60 秒。建议先用正常负载跑一天,统计实际的最大无心跳间隔,再据此设置报警阈值。
我自己踩过几次坑之后,现在对HEARTBEAT.md和自愈机制的态度是:它们确实能解决很多问题,但前提是你得真正理解它的边界,并且给它配好运行环境。部署 OpenClaw 不像是装个普通软件,更像是在养一只有习惯的生物。你需要知道它什么时候需要休息、什么时候该被叫醒、什么时候手动介入比自动恢复更靠谱。
如果你也折腾过这些,或者在session file locked上花过更长时间,欢迎按自己的经验调整这套配置。技术方案没有唯一正确答案,找到适合你场景的那套,就是最好的。