Heartbeat Monitor 是一个面向多会话集群(fleet)的监控技能,核心目标是检测并替换卡死的会话,同时避免误杀正在工作的会话。它指出两类需要避免的失败:一是放任卡死会话不管,导致其队列任务流失;二是替换掉正在工作的会话,造成在途工作重复、分支出现第二个写入者,并让集群对自身可靠性产生错误认知。后者更常见也更昂贵,本技能正是为防止它而写。
核心原则:消息缺失不等于工作缺失。在判定一个会话“静默”之前,必须先读取该会话的产物(artifact)。一个不回复的会话不一定没有工作,它可能正处于长命令执行中、被不可中断的前台调用阻塞、按协议处于待命状态等待唤醒,或者工作正常但路由器丢掉了消息。因此,报告必须指明读取了哪个产物并引用其内容,例如“heartbeats/lane-x.md 最后一行时间为 11:16Z,状态 working,任务 #123;PR #456 的 head 在 11:41Z 有移动”,而不是简单说“Lane X 静默”。
监控的边界:本监控只能观察静默,无法发现一个正常说话但所谈之事已不再成立的会话。资料提到,曾有三个会话在等待已经完成的工作,分别浪费约 2 小时、68 分钟和 566 分钟,没有任何监控(包括本技能)检测到,因为心跳一直正确且最新。唯一能捕获这类问题的是会话自己测量其阻塞原因。因此,监控的职责比看起来更早结束,仅依赖它的集群会损失看不见的时间。
使用方式:在每次替换决策前以及需要状态报告时,运行 census 命令:node "${CLAUDE_PLUGIN_ROOT}/scripts/fleet.mjs" census。该命令执行步骤 1、2 和分类,并校验每一行。它输出已读取的 lane 数(read=n of n)、每个 lane 的年龄和类别、每个文件的畸形行数,以及每个候选 lane 的 fleet blockers 命令(步骤 3 中最高产出的产物读取)。退出码 1 表示有候选需要读取,而不是有 lane 需要替换。
Census 按顺序执行:1)读取心跳文件最后一行,包括 UTC 时间、状态、任务,覆盖注册表中所有 lane,并说明读取的集合(n of n lanes);2)计算年龄,即 now 减去最后心跳时间;3)对超过阈值的 lane 读取产物,按顺序在第一个显示移动的产物处停止:远程分支 head(自上次心跳后是否移动)、打开的 PR(是否有新开、更新或新评论)、工作树(是否有新提交或未提交工作)、报告文件(如果已写);4)分类后行动。禁止循环轮询 ListAgents,因为它消耗轮次且无法证明会话是否在工作,应改为读取文件并伪造状态。
三类静默:STANDBY——最后状态为 standby,没有问题,发送工作即可唤醒;BLOCKED-BY-DESIGN——最后状态为 waiting,备注说明有长调用在途,应等待并在下个周期重读,替换会重复调用;HUNG——超过 staleMinutes,状态不是 standby,且产物无移动,先 ping 一次再替换。超过阈值但产物有移动的 lane 不是卡死,而是心跳纪律问题,应发消息而非替换。
心跳行有 schema 和校验门:运行 ${CLAUDE_PLUGIN_ROOT}/scripts/validate-heartbeat.mjs 针对本次运行的心跳目录。--self-test 证明该门能在六个命名输入上变红、在六个真实输入上变绿,且两个变异各自恰好破坏依赖它的用例。资料指出,仅靠约定不够:曾有三个会话对同一格式产生三种畸形——字面 \n 转义而非换行、没有竖线的项目符号导致状态解析为空、从同一运行中其他 markdown 表格复制来的前导竖线使每个字段偏移一位,导致解析器把时间戳读成状态。为阻止监控把死会话计为存活而写的墓碑反而让监控宣布它存活。因此需要校验门。
校验门不覆盖的情况:没有时间戳也没有竖线的项目符号与文件中合法散文无法区分,行级规则无法在不拒绝所有交接笔记的情况下捕获它。它由上一级捕获:该周期没有有效心跳即为过期心跳。可解析不等于有效,这正是校验门存在的理由。监控自身的解析器只问字段 2 是否存在且非空,按此标准 22 个文件全部解析干净;而校验门问的是时间戳格式、状态词汇、转义、前导竖线,发现了 46 处畸形但均可解析。两者答案都真,回答的是不同问题,其差距正是校验器要弥合的:解析成看似合理状态的行才是失败模式,而不是解析失败的行。
阈值来自 fleet.config.json:heartbeat.intervalMinutes(默认 15)表示活跃会话必须追加心跳的频率;heartbeat.staleMinutes(默认 25)表示超过此值且未通过步骤 3 的活跃会话成为替换候选。两者按集群配置。典型任务长于间隔的集群应提高间隔,而不是教会话忽略它——所有人都违反的阈值是无法变红的门。
替换流程:先 ping 一次,发送恰好一条消息“heartbeat now”,记录发送时间。如果心跳文件在一个周期内没有变化,且产物检查仍无移动,则该 lane 卡死。两次独立静默,其中一次是直接地址,构成证据标准。替换时需书面裁定卡死并附证据:最后心跳行原文、其年龄、运行的产物检查及各自返回、未应答 ping 的时间。在确认前任不再写入之前,绝不触碰其工作树或分支。后继者在该工作树中继续,一个分支一个写入者,不创建第二个。后继者继承心跳文件,前任最后状态即交接,读取并恢复,不要重启。后继者的简报应从前任状态而非原始简报撰写:已交付什么、在途什么、哪个分支和工作树、哪个 PR 等待审查、最后裁定是什么。为后继者追加注册表行,旧行保留。后继者命名要区分(<lane>-2、<lane>-3),重用名称会使注册表歧义。通过路由器宣布替换,以免有人向停止应答的地址发送工作。
两个使替换记录不可靠的事实及修复:被重新利用的会话不是新 lane,当会话完成角色并承担另一角色时,注册表有同一 session id 的新行,按行计数的 census 会重复计数,应按 session id 匹配并显式记录重新利用。记录中的时间戳可能不一致,例如某次运行中替换的注册表行早于其前任卡死的裁定,不要在报告中抹平,应引用记录原样并说明不一致;整理过的记录比矛盾的记录更糟,因为矛盾是唯一表明记录有误的信号。
共享身份下:如果所有会话以同一账户认证,所有权无法从 git 或 forge 状态推导,census 无法回答“这是谁的分支”。自报 census 在冲突处有洞:当两个 lane 声称同一项且一个以零提交回退时,失败者无物可指。应基于仓库自身对分支和 PR 的无上限枚举构建 census,并与分配时写入的分配记录交叉引用,绝不使用确认日志,因为后者记录的是说过什么而非存在什么。替换裁定模板见 references/replacement-policy.md,证据规则见 evidence-rules 技能。