Files
HomeAgent/cmd/homed
JianFeeeee 562ba6bd98 fix(guard): 启动宽限期 —— 停机重启后不再误杀加载中的 worker
## 故障(2026-10-12 部署实测,6 连杀 + 服务降级)

停机 34s 后重启:heartbeat 文件 mtime 停在**上一代 worker**(已远超
30s 阈值)。新 worker 要先做完全部重型初始化(chineseclip 512 +
两个静态词向量 + 知识重建,冷启动 5–8s)才写第一次心跳
(startHeartbeat 在 main.go:190,位于所有初始化之后)。
而 guard 的 5s ticker 先到、拿旧 mtime 判 stale → 把还在加载的
worker SIGKILL → 循环重启,guard 进入 failback Safe-Mode:

    19:00:28 worker up pid=931403 boot=normal
    19:00:33 heartbeat stale for pid=931403 ... SIGKILL   ← 加载中
    19:00:33 worker up pid=931557 boot=normal
    19:00:38 heartbeat stale ... SIGKILL
    ...(6 连杀后)
    19:00:43 3 failures >= max 3, entering failback (Safe-Mode)

worker 自身日志完全健康(graph/multimodal/knowledge 依次就绪),
纯死于"还没来得及写第一次心跳"。

## 根因

heartbeatStale 对「文件不存在」宽限,但对「文件存在、mtime 是
上一代的」不宽限——停机超过 heartbeatTimeout 的每一次重启都会踩。
旧内核未暴露只因它从未停机这么久(上一次是系统重启,mtime 新鲜)。
既有缺陷,block-union 重构未触碰该逻辑(main 分支同病)。

## 修法

spawn 后给一个 heartbeatTimeout 的**启动宽限期**:期间即使心跳
文件 mtime 极旧也不判杀。宽限期满后恢复原判定(卡死 worker 靠
宽限躲不过去)。决策抽成纯函数 shouldKillStaleHeartbeat。

## 判据(TestStartupGraceAllowsInit,变异测试通过)

  (a) 宽限期内 + mtime 极旧(上一代遗留)→ 不许杀
      (末沿 -50ms 也钉住:ticker 恰在 deadline 前触发是常态)
  (b) 宽限期满 + mtime 极旧(真卡死)→ 必须杀
  (c) 宽限期满 + mtime 新鲜(健康)→ 不许杀

变异(移除宽限判定、直接 heartbeatStale)→ (a) 两条断言当场 FAIL;
恢复后全绿。TestHeartbeatStale(三分支既有判据)不受影响。

cmd/homed 包全量测试通过;全仓生产编译 GREEN。
2026-10-12 19:16:57 +08:00
..