# Queue-Runner + Watchdog 重复审查Bug（2026-07-02 实证）

## 现象
Doro反复收到同一份合同的"审查完毕"通知。今天受影响的合同：
- 夏阳合同：被通知至少2次（可能3次）
- 家庭医生签约合同：被通知2次（第3次在final_review被手动杀掉）

## Bug链条（已验证）

```
1. Queue-runner 启动 → START 合同X → 创建 worker PID → "Waiting for worker..."
2. Runner 进程异常退出（OOM/信号/shell被杀），但 worker 子进程继续运行
3. Worker 独立完成整个 workflow（包括 final_review = 私信通知 Doro）
4. Runner 已死 → 没有执行 "mv $FILE done/" → 文件仍在 queue 目录
5. Watchdog（20min cron）检测到：runner不在 + done/ < manifest → 重启 runner
6. 新 runner 看到文件还在 queue → SKIP逻辑只检查 `[ -e "$FILE" ]` → 认为未处理
7. 新 runner 启动第二个 thread → 从头审查 → final_review 又通知 Doro
```

## 额外失败模式（watchdog恢复旧thread）

```
watchdog.log:
[2026-07-02 18:40:14] suspended 06FJ42KNSK1ZA767W8AZ6MXJ6C → exec 恢复
[2026-07-02 18:40:15] idle 06FJ3ZJXR5WHJNPDHF22YYMGCM → exec 续跑
```

Watchdog 恢复处于 idle/suspended 状态的旧 thread，这些 thread 的同名合同可能已被新 runner 完成。
旧 thread 被唤醒后接着跑完 final_review → 又一次通知。

## 今天的实际时间线

| 时间(BJT) | 事件 |
|-----------|------|
| 02:45 | 第一个runner启动，处理恭兴(cancelled) |
| 05:00 | Watchdog重启runner → 恭兴(成功) |
| 06:25 | 肃言完成 |
| 07:28 | 卫健委完成 |
| 08:42 | 夏阳 thread#1 启动 (06FJ3ZJXR) |
| ~11:00 | 夏阳#1 完成（含final_review通知）；但runner已死，文件没进done/ |
| 11:40 | Watchdog重启runner → 夏阳 thread#2 启动 (06FJ58AD) |
| 12:38 | 夏阳#2 完成（第二次通知）；家庭医生签约 thread 启动 (06FJ5NPHT) |
| 18:40 | Watchdog恢复旧idle thread 06FJ3ZJXR5WHJNPDHF22YYMGCM (夏阳#1) + 06FJ42KNSK (家庭医生#?) |
| 19:21 | 家庭医生签约交付到Nextcloud |
| ~21:00 | 06FJ42KNSK 完成 final_review（第2次家庭医生通知） |
| 21:16 | Watchdog再次重启runner → 家庭医生 thread#2 (06FJ5NPHT) 进入 final_review |
| 21:30 | 手动 kill -9 杀掉 06FJ5NPHT 的 final_review → 阻止第3次通知 |

## 止血SOP

当 Doro 报告收到重复通知时：

1. **找重复进程**：`ps aux | grep -E "(background-worker|uwf-hermes)" | grep -v grep`
2. **杀掉重复进程**：`kill -9 <worker-PID> <uwf-hermes-PID>`
3. **杀掉queue-runner**：`kill <queue-runner-PID>`
4. **清理queue**：把所有tracker中已completed的文件移入done/
   ```python
   import json, os, shutil
   tracker = json.load(open(os.path.expanduser('~/.hermes/data/contract-tracker.json')))
   completed = {c['original_filename'] for c in tracker['contracts'] if c['status'] == 'completed'}
   queue_dir = '/tmp/contract-queue'
   for f in os.listdir(queue_dir):
       if f.endswith(('.doc', '.docx', '.pdf')) and f in completed:
           shutil.move(f'{queue_dir}/{f}', f'{queue_dir}/done/{f}')
   ```
5. **验证**：`find /tmp/contract-queue/ -maxdepth 1 -name '*.doc*'` 应为空
6. **确认watchdog不会重启**：done/ 文件数 ≥ manifest.txt 行数

## 缺失防线（待修复）

| 位置 | 应加的检查 |
|------|-----------|
| queue-runner START 逻辑 | 启动workflow前查tracker：已completed直接mv到done/ |
| watchdog 恢复thread逻辑 | resume前查：同filename的tracker记录是否already completed |
| final_review | 发通知前查：是否24h内已有同合同的通知（防御性去重） |

## 关键文件路径

- Queue runner: `/home/maggie/.hermes/skills/devops/uwf/scripts/contract-queue-runner.sh`
- Watchdog: `~/.hermes/scripts/contract-queue-watchdog.sh`
- Queue dir: `/tmp/contract-queue/` (manifest.txt + done/)
- Queue log: `/tmp/contract-queue/queue.log`
- Watchdog log: `/tmp/contract-queue/watchdog.log`
- Tracker: `~/.hermes/data/contract-tracker.json`
- Cron jobs: `contract-queue-watchdog` (*/20, job_id:3174518affda), `auto-notify-watchdog` (5min, job_id:63bb31d4f050)
