# 工单：观察一次任务中断与恢复

将下面的任务交给 agent 时，工作目录应是你自己的 CF Fieldbook checkout。输入全部来自仓库合成例子，不需要填写账号、token、主机或真实业务数据。先读[恢复用例](../use-cases/recoverable-jobs.md)与[模型说明](../examples/job-state/README.md)，再运行同一个模型；这份工单不另写一套实现。

## 目标与允许的动作

解释任务登记、租约内阻止重复领取、租约到期恢复、新旧执行代次及 `uncertain` 的差别。允许读取相关公共源码，执行下面的离线演示与既有测试，并报告观察。不要修改源码来迎合预期值。

本工单不包含登录、部署、请求外部服务、创建云资源或接入真实任务。所需环境为 Python 3.11+；依赖使用 Python 标准库。若环境不满足，报告实际版本与错误，不自动安装工具。

## 合成输入

演示中的 `local-transform` 是可重做的本地工作；`external-effect` 的外部流水也是内存中的合成记录。时钟由模型调用显式传入，不需要实际等待 30 秒。演示使用临时 SQLite，不传入自己的数据库文件。

## 精确步骤

在仓库根目录执行：

```bash
python3 examples/job-state/demo.py
python3 -m unittest discover -s tests -p 'test_job_recovery.py' -v
```

第一条命令读取现有 `demo.py`／`model.py` 并打印实际返回值；第二条检查中断恢复、数据库重开、迟到提交拒绝及未知外部结果的分叉。它们不访问 Cloudflare。

## 可观察结果

| 检查点 | 应观察到什么 | 它证明到哪里 |
|---|---|---|
| 登记后 | `('pending', 0)` | 登记不是完成 |
| A 在 `t=0` 领取、`t=10` 再领取 | generation `1`；随后 `None` | 租约内不重新领取同一工作 |
| B 在 `t=30` 恢复领取 | generation `2` | 到期允许新一轮本地执行 |
| A 与 B 在 `t=31` 提交 | 旧代次 `False`，新代次 `True` | 旧结果不能覆盖当前代次 |
| 完成后重复领取 | `None` | 已完成任务不被重放 |
| 响应丢失后重启、`t=500` 领取 | `None`；状态仍为 `('uncertain', 1)` | 未知结果被保留，不自动当作失败重试 |
| 核对合成外部流水 | 动作次数 `1` | 只证明这份合成记录；模型没有人工裁决接口 |

## 失败证据与停止条件

命令非零退出、实际值与预期不符或测试失败时，保留失败检查点、退出码与必要错误文本，再停止后续依赖该结果的步骤。不要把真实日志或私人路径粘进公共 issue。若任务要求网络、凭据、持久数据库或不可逆外部动作，说明新增范围并停止；此工单没有授权这些动作。

特别检查模型边界：worker 在发送外部请求后、写入 `uncertain` 前崩溃时，本模型仍可能重新领取。不要因此宣称付款、发布、删除或通知可以安全自动重试；外部幂等或结果核对需要单独设计与验证。

## 清理与回报

演示退出时自动清除临时 SQLite；测试也使用临时目录。没有云资源、账户状态或常驻服务需要清理，不运行额外删除命令。

回报实际运行的命令、退出结果、关键输出、失败项、临时资源是否正常结束，以及尚未验证的云端交付与外部副作用。若只阅读了源码或网页模拟，就明确写未运行本地检查。
