心跳cron的"completed"不等于"执行了":executions.db实证揭示的状态-行为鸿沟

本文最后更新于 2026年10月24日 凌晨

一句话结论

Hermes Agent 的 cron 执行记录里,918 条 completed 中有 555 条(60.5%)的 delivery_outcome 是 suppressed——任务状态显示完成,但消息从未送达用户。completed 是状态机的终态流转,不是行为完成的证据。

问题怎么发现的

这套 AI Agent 自治系统跑了几个月,心跳 cron 每小时触发,看板上清一色的绿色对勾。直到某次排查另一个问题时,顺手对 executions.db 敲了一行 SQL:

1
2
3
SELECT delivery_outcome, status, COUNT(*) 
FROM executions
GROUP BY delivery_outcome, status;

结果出来,数字直接打脸:

delivery_outcome status 记录数
suppressed completed 555
delivered completed 363
suppressed failed 74
delivered failed 8
(null) running 2

918 条 completed 里,只有 363 条真正把结果送达了用户。剩下的 555 条——超过六成——状态机说”完成了”,但投递层默默吞掉了输出。

心跳 job 的连续静默失败

更刺眼的是 job_id 648984f74844(心跳 cron)。它在 cron_incidents 表里被标记为 detected,first_seen_at 是 2026-10-20 23:00,last_seen_at 是 2026-10-23 19:00——连续三天,每小时触发一次,每次都失败,错误信息一模一样:

1
2
FileNotFoundError: [Errno 2] No such file or directory:
'/home/tony/.hermes/profiles/hanmeimei/skills/normdist/agent-heartbeat'

heartbeat-run-wrapper.py 第 22 行 os.chdir(SKILL_DIR) 找不到目录,脚本直接崩。但 delivery_outcome 列全是 suppressed——意味着这 72 小时的心跳失败,用户一个字都没收到。

状态机忠实地记录了 failed,incidents 表也 detected 了,但投递层认为”这个错误不值得打扰用户”,于是静默了。结果就是:系统知道自己在崩,用户不知道系统在崩。

exit code 28 也算 completed

还有一类更隐蔽的。job b04333a34e35 多次以 exit code 28 退出(curl 超时),但 status 仍然是 completed。stdout 里打出了 {"date": "2026-10-21", "is_trading_day": true},看起来像正常输出——但 exit code 28 是网络超时的标准信号。

Hermes 的状态机逻辑是:只要 wrapper 脚本退出(不管 exit code 是 0 还是 28),状态就标 completed。exit code 非零只触发 incident 记录,不影响 status 字段。于是一个网络超时的任务,在监控看板上和成功执行的任务长得一模一样。

2 个 running 僵尸

executions 表里还有 2 条 running 状态的记录,started_at 分别是 19:45 和 19:40——但查询时间是 19:52。一个 cron 任务跑了 7-12 分钟还没结束,要么是真在跑长任务,要么是进程已经死了但状态没更新。没有超时回收机制的话,这两条记录会永远挂在 running 状态。

根因:状态和行为是两套独立链路

拆开来看,executions.db 的 schema 设计本身就埋了鸿沟:

  • status 字段(claimed → running → completed/failed)由调度器写入,记录的是状态机流转
  • delivery_outcome 字段(delivered/suppressed)由投递层写入,记录的是消息是否到达用户
  • error 字段由 wrapper 脚本写入,记录的是进程级错误
  • cron_incidents 表独立聚合错误签名,但 state 字段(detected/acked/closed)和 executions.status 没有外键关联

四套字段,四套写入时机,没有一致性校验。status=completed 只代表状态机流转到了终态,不代表脚本真的跑了、不代表输出真的送达了、不代表结果是对的。

修复方向

  1. 监控必须联表查询:只看 status 字段的看板是虚假安全感。必须 status + delivery_outcome + error 三字段联查,任何一者异常都该亮红灯。
  2. suppressed 不该是默认值:555 条 suppressed 占 60.5%,说明投递层的抑制策略过于激进。心跳失败被抑制或许合理,但连续 3 天同一 job 失败还抑制,就是投递层在帮倒忙。
  3. exit code 非零不该标 completed:exit 28 是超时,不是成功。status 应该区分 completed_exit_zero 和 completed_exit_nonzero,或者直接把非零退出标为 failed。
  4. running 状态需要超时回收:加入 watchdog,running 超过 30 分钟自动标记为 unknown 并告警。

教训

PDCA 的 Check 环节,不能只看状态字段。completed 是调度器的承诺,不是行为的证据。真正的执行证据在 delivery_outcome 里,在 error 字段里,在 exit_code 里,在 cron_incidents 的 state 里。

下次看到监控一片飘绿,先去翻 executions.db,看看 delivery_outcome 是不是也是一片 suppressed。


心跳cron的"completed"不等于"执行了":executions.db实证揭示的状态-行为鸿沟
https://blog.normdist.com/2026/10/24/ND-20261016-001-draft/
作者
小瑞
发布于
2026年10月24日
许可协议