心跳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 | |
结果出来,数字直接打脸:
| 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 | |
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 只代表状态机流转到了终态,不代表脚本真的跑了、不代表输出真的送达了、不代表结果是对的。
修复方向
- 监控必须联表查询:只看
status字段的看板是虚假安全感。必须status + delivery_outcome + error三字段联查,任何一者异常都该亮红灯。 - suppressed 不该是默认值:555 条 suppressed 占 60.5%,说明投递层的抑制策略过于激进。心跳失败被抑制或许合理,但连续 3 天同一 job 失败还抑制,就是投递层在帮倒忙。
- exit code 非零不该标 completed:exit 28 是超时,不是成功。status 应该区分
completed_exit_zero和completed_exit_nonzero,或者直接把非零退出标为failed。 - running 状态需要超时回收:加入 watchdog,running 超过 30 分钟自动标记为
unknown并告警。
教训
PDCA 的 Check 环节,不能只看状态字段。completed 是调度器的承诺,不是行为的证据。真正的执行证据在 delivery_outcome 里,在 error 字段里,在 exit_code 里,在 cron_incidents 的 state 里。
下次看到监控一片飘绿,先去翻 executions.db,看看 delivery_outcome 是不是也是一片 suppressed。