# 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//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-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//` - [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` 链路已经从“状态收敛 + 安全恢复点判定”进一步补齐到“正式异步恢复执行”。