Files
reader/docs/openclaw/archive/p1-status-reconciliation-plan-2026-04-14.md
T

368 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Reader 日报链路 P1 状态收敛问题:规划与修复清单(2026-04-14)
## 背景
在 2026-04-14 的 reader 日报正式运行中,出现了以下现象:
- `openclaw-delivery-payload.json`、`digest-brief.json`、`run-report.json` 已真实落盘
- 但 `get_freshrss_pipeline_job_status` / `get_run_status` 仍可能显示:
- `running`
- `failed`
- 或 `current_stage=generate_summaries`
- `resume_run` 在这种状态下可能直接超时
这说明当前 reader 的**状态层(job/run-state)**与**产物层(artifacts/report)**之间没有稳定收敛。
---
## 本次确认的核心结论
### 1. job status 与 run status 是两套独立状态系统
- **job 层状态**:`src/summary_mcp/runtime/freshrss_pipeline_jobs.py`
- `start_freshrss_pipeline_job()`
- `run_freshrss_pipeline_job()`
- `get_freshrss_pipeline_job_status()`
- 状态文件位于:`outputs/freshrss/pipeline_jobs/<job_id>/run-state.json`
- 只有 4 个粗粒度 stage:
- `prepare_job`
- `load_input`
- `run_pipeline`
- `write_result`
- **run 层状态**:`src/summary_mcp/workflows/freshrss_pipeline.py`
- `run_freshrss_pipeline()`
- 由 `src/summary_mcp/runtime/query_service.py:get_run_status()` 查询
- 状态文件位于:`outputs/freshrss/rerun/<run_dir>/run-state.json`
- 包含 6 个细粒度 stage:
- `fetch_feed`
- `extract_articles`
- `generate_summaries`
- `apply_filters`
- `build_delivery_payload`
- `write_run_report`
**问题:** 两套状态没有统一收敛规则,用户可以同时看到两套不同口径的“当前进度”。
---
### 2. 查询层目前优先信 run-state,不会用 artifacts / run-report 纠偏
代码位置:`src/summary_mcp/runtime/query_service.py`
关键行为:
- `_resolve_run_record()` 只要发现 `run-state.json` 存在,就优先使用 `RunStore.load(...)`
- 即使 `run-report.json`、`delivery_payload`、`digest_brief` 已存在,也不会自动纠偏状态
**结果:**
- 一旦 `run-state.json` 因中断、超时、外层 SIGTERM 或写回未完成而停留在旧值
- `get_run_status()` 就会持续返回过期状态
- 造成“产物已完成,但状态仍显示 running/failed/卡在 summary”的错觉
---
### 3. `generate_summaries` 假卡住,本质上更像 stale state,不像真实业务卡住
代码位置:`src/summary_mcp/workflows/freshrss_pipeline.py`
从执行顺序看:
1. `start_stage(generate_summaries)`
2. summary 循环
3. `finish_stage(generate_summaries)`
4. `start_stage(apply_filters)`
5. `finish_stage(apply_filters)`
6. `start_stage(build_delivery_payload)`
7. 写 payload / digest brief
8. `finish_stage(build_delivery_payload)`
9. `start_stage(write_run_report)`
10. 写 run-report
11. `finish_stage(write_run_report)`
12. `finish_run(...)`
**判断:**
如果 payload / digest brief / run-report 都已经存在,那么“仍显示卡在 `generate_summaries`”更可能是:
- `run-state.json` 没来得及写回最终状态
- 或查询时读到了旧状态
而不是 summary 阶段真实没有跑过去。
---
### 4. `resume_run` 不是轻量恢复,而是同步继续跑工作流
代码位置:`src/summary_mcp/runtime/resume_service.py`
关键行为:
- `resume_run()` 会根据 `resume_from_stage` 直接继续执行:
- `_run_summary_stage(...)`
- `_run_filter_stage(...)`
- `_run_delivery_stage(...)`
- `_run_report_stage(...)`
这意味着它不是“修状态”的工具,而是“同步继续跑剩余工作流”的工具。
**问题:**
- 如果 stale state 把 `resume_from_stage` 定在 `generate_summaries`
- 那么 `resume_run` 会从一个过早阶段重新跑
- 在 MCP 包装层下非常容易超时
---
## 问题分类
### A. 真实 bug
1. **查询层过度信任 stale `run-state.json`**
- 文件:`src/summary_mcp/runtime/query_service.py`
- 影响:产物已完成但状态仍错误
2. **`resume_run` 过度依赖 stale `current_stage` / recovery 信息**
- 文件:`src/summary_mcp/runtime/resume_service.py`
- 影响:从过早阶段重跑,放大 timeout 风险
### B. 状态设计缺陷
3. **job 层与 run 层两套状态源没有统一收敛规则**
- 文件:`src/summary_mcp/runtime/freshrss_pipeline_jobs.py`
- 文件:`src/summary_mcp/runtime/query_service.py`
- 影响:用户看到两个互相打架的状态解释
4. **状态系统完全依赖显式写回,不会按产物反推修正**
- 文件:`src/summary_mcp/runtime/run_store.py`
- 影响:一旦中断,状态比产物更容易脏
### C. 调用层误判
5. **把 `resume_run` 当成轻量恢复接口使用**
- 实际上它更接近“同步恢复执行器”
- 影响:在长链路场景下超时是高概率事件
---
## 修复目标
## 当前落地状态(回填)
- [x] Phase 1 已落地:`get_run_status()` 会基于 `run-report.json` 与关键产物做终态收敛,并暴露 `status_source` / `state_conflict`
- [x] Phase 2 已落地第一阶段:`resume_run()` 会拒绝对已有终态 `run-report.json` 的 run 继续恢复
- [x] Phase 2 已继续增强:恢复起点现在会优先根据 artifacts 重算,而不是直接盲信 `run-state.recovery.resume_from_stage`
- [x] 新增 `inspect_resume_plan(run_id)` 作为恢复前置判定接口,避免调用方用 `resume_run` 探路
- [x] Phase 2 已补齐生产恢复 artifacts:正式 run 会稳定写出 `summary/summary-batch.json` 与 `candidates/candidate-batch.json`,`resume_run` / `inspect_resume_plan` 会优先使用它们,而不是依赖 debug per-item 文件
- [x] Phase 3 已落地:job 状态与结果读取会基于 linked run 做收敛,避免 outer job stale state 卡住编排
### 一级目标(必须达成)
1. 当 `run-report.json` / `delivery_payload` / `digest_brief` 已存在时,`get_run_status()` 不应继续盲目展示明显过期的 stage 状态;对调用方暴露的 `status` 必须直接收敛为可用终态,而不是只附加 hint
2. 当状态层与产物层冲突时,查询结果必须显式标注“状态冲突 / stale state”
3. `resume_run()` 在恢复前应优先基于现有 artifacts 判断真实可恢复起点,避免从过早阶段重跑
### 二级目标(建议达成)
4. job 层状态结果中增加对 linked run 的补充解释,避免“job running 但 run 产物已齐”这种情况毫无说明
5. 为后续编排层提供明确可消费的“状态可信度/冲突提示”字段
---
## 最小修复方案
### Phase 1|先修 run 查询层(优先级最高)
#### 目标
让 `get_run_status()` 至少能正确识别:
- run-state 是旧的
- 但关键产物已经齐了
#### 建议改动点
文件:`src/summary_mcp/runtime/query_service.py`
#### 建议动作
- [x] 在 `_resolve_run_record()` 或 `_build_status_response()` 中增加“关键产物存在性检查”
- `run-report.json`
- `candidates/openclaw-delivery-payload.json`
- `candidates/digest-brief.json`
- [x] 如果 `run-state.current_stage` 仍停留在早期阶段,但关键产物已齐:
- 不要继续原样输出为可信最终态
- 应直接把对外 `status` / `current_stage` / `recovery` 收敛成终态语义
- 同时新增解释字段,例如:
- `state_conflict: true`
- `state_conflict_reason: "run_state indicates generate_summaries but run-report.json already proves the workflow reached a terminal state"`
- `status_source: "run_report_reconciliation"`
- [x] 保留 `state_source=run_state`,但增加 `status_source` / `state_quality` / `state_conflict` 之类解释字段
#### 预期收益
- OpenClaw 继续按 `status` 分支时也不会卡住
- 第一时间减少“明明产物齐了却还像没跑完”的误判
- 不需要立刻动 workflow 主链路
---
### Phase 2|修 `resume_run` 的恢复起点判断
#### 目标
避免 stale state 让恢复逻辑从 `generate_summaries` 这类过早阶段重跑。
#### 建议改动点
文件:`src/summary_mcp/runtime/resume_service.py`
#### 建议动作
- [x] 在 `_resolve_resume_from_stage()` 之前/之后加入真实 artifacts 检查
- [x] 如果以下文件已存在:
- `openclaw-delivery-payload.json`
- `digest-brief.json`
- `run-report.json`
则不要再从 `generate_summaries` 或 `apply_filters` 起跑
- [x] 为 `resume_run()` 增加“恢复起点是基于 artifacts 重算还是基于 state 推断”的返回说明
- [x] 必要时增加更保守逻辑:
- `run-report.json` 已存在时,默认拒绝继续 resume,并提示“产物已完成,请先检查状态一致性”
- 补充:默认生产模式下,主链路会稳定写出 `summary-batch` / `candidate-batch`,恢复逻辑优先消费这两个 batch artifacts;若它们缺失或不稳定,才回退到更早的安全 stage 或直接拒绝恢复
- 补充:调用方可先走 `inspect_resume_plan`,只有 `recommended_action=resume` 时再调用 `resume_run`
#### 预期收益
- 降低无意义重跑和 timeout 风险
- 让 `resume_run` 更接近真正的恢复工具,而不是误重跑工具
---
### Phase 3|补 job/run 双状态解释层
#### 目标
让 `get_freshrss_pipeline_job_status()` 和 `get_run_status()` 的关系对调用方更可理解。
#### 建议改动点
文件:`src/summary_mcp/runtime/freshrss_pipeline_jobs.py`
#### 建议动作
- [x] 在 `get_freshrss_pipeline_job_status()` 中,读取 linked run 的关键产物存在性(轻量即可)
- [x] 若 job 仍显示 `run_pipeline`,但 linked run 已有 report/payload/digest 产物:
- 不仅增加解释字段,还应直接把 job 对外 `status` 收敛为终态,避免外层永远轮询
- 例如:
- `status_source: "linked_run_reconciliation"`
- `status_note: "linked run artifacts are complete; the job can be treated as completed"`
- [x] 若 `result.json` 缺失,但 linked run 已有 `run-report.json` 与 delivery 产物:
- `get_freshrss_pipeline_job_result()` 应能基于 linked run 产物合成最小结果,至少稳定返回 `run_id`
- [x] 明确文档:job status 是外层异步任务态,不等于内部 workflow 细粒度状态
#### 预期收益
- 减少“job running / run finished”口径冲突带来的误解
- 避免 OpenClaw 因 outer job stale state 卡死在轮询和 result 读取前
---
### Phase 4|把 `resume_run` 改成异步恢复 job
#### 目标
解决当前剩余的核心问题:`resume_run` 虽然恢复判定已经安全,但执行模型仍是同步 MCP 调用,长链路恢复时依然可能超时,导致 OpenClaw 编排层“看起来像又卡住了”。
#### 建议改动点
文件:
- `src/summary_mcp/runtime/resume_jobs.py`(新)
- `scripts/run_resume_job.py`(新)
- `src/summary_mcp/server.py`
- `src/summary_mcp/runtime/__init__.py`
- `src/summary_mcp/runtime/resume_service.py`
#### 建议动作
- [x] 新增最小异步恢复接口:
- `start_resume_job(run_id)`
- `get_resume_job_status(job_id)`
- `get_resume_job_result(job_id)`
- [x] job 目录固定落到:
- `outputs/freshrss/resume_jobs/<job_id>/`
- [x] 最少产物约定:
- `run-state.json`
- `input.json`
- `result.json`(成功时)
- `job-report.json`
- [x] `start_resume_job` 内部先调用 `inspect_resume_plan`
- 只有 `recommended_action=resume` 才允许真正启动
- `read_terminal_result` / `start_new_run` 要直接在 job 输入校验阶段返回,不进入执行器
- [x] 后台执行时复用现有 `_resume_freshrss_run(...)`
- 不重写恢复业务逻辑
- 只把同步入口拆成异步 job 外壳
- [x] `resume_run(run_id)` 保留,但降级为 debug / fallback
- 文档中明确:OpenClaw 编排默认应走 resume async job,而不是同步 `resume_run`
- [x] job result 里至少稳定返回:
- `run_id`
- `resume_from_stage`
- `status`
- `result_source`
- `delivery_output` / `report_output`(若存在)
#### 预期收益
- 彻底切掉恢复阶段的 MCP 同步超时风险
- 让 OpenClaw 对“启动恢复 / 轮询恢复 / 读取恢复结果”的控制面与主 pipeline async job 保持一致
- 把“恢复判定”与“恢复执行”分层,减少误调用和卡住错觉
---
## 不建议现在就做的事
- [ ] **不要先做自动 fallback 修状态**
- 例如:看到 artifacts 齐了就直接把 run-state 强行改成 success
- 原因:这会掩盖真正的状态写回问题
- [ ] **不要先大改 workflow 主链路**
- 当前更像查询层与恢复层的状态解释缺陷
- 先修读取与恢复判断,收益更大、风险更低
---
## 建议执行顺序
1. **先改 `query_service.py`**
- 让 `get_run_status()` 能暴露 stale state / artifact conflict
2. **再改 `resume_service.py`**
- 避免从错误阶段重跑
3. **最后看 `freshrss_pipeline_jobs.py`**
- 给 job status 加 linked run 补充说明
4. **收尾改 `resume async job`**
- 让恢复执行也走正式异步控制面,避免同步恢复再把编排卡住
---
## 验收标准
### 验收 1:状态冲突识别
构造一个场景:
- `run-state.json` 留在 `generate_summaries`
- 但 payload / digest brief / run-report 已存在
期望:
- `get_run_status()` 不再只回“卡在 generate_summaries”
- 会显式返回冲突提示字段
### 验收 2:恢复起点修正
构造一个场景:
- `run-state` 指向 `generate_summaries`
- 但 `delivery_payload` / `run-report` 已存在
期望:
- `resume_run()` 不应再从 summary 阶段重跑
- 至少应拒绝恢复并提示“产物已完成,优先检查状态一致性”
### 验收 3:job/run 双层说明
构造一个场景:
- job status 仍在 `run_pipeline`
- linked run 已有关键产物
期望:
- `get_freshrss_pipeline_job_status()` 能返回补充说明,不再只有生硬 running
### 验收 4:恢复执行不再阻塞编排
构造一个场景:
- run 可恢复
- 恢复点为 `generate_summaries` 或 `apply_filters`
- 恢复执行耗时超过单次 MCP 同步窗口
期望:
- OpenClaw 调用的是 `start_resume_job(...)`,而不是同步 `resume_run(...)`
- `get_resume_job_status(job_id)` 可稳定轮询到终态
- `get_resume_job_result(job_id)` 至少稳定返回 `run_id`、`resume_from_stage` 与最终产物引用
- 即使恢复失败,也能在 job-report / result 中看清失败点,而不是只表现为调用超时
---
## 备注
截至 2026-04-14,本文件中的 Phase 1 / 2 / 3 / 4 已完成主要落地;当前 `resume` 链路已经从“状态收敛 + 安全恢复点判定”进一步补齐到“正式异步恢复执行”。