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

14 KiB
Raw Permalink Blame History

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. 状态设计缺陷

  1. job 层与 run 层两套状态源没有统一收敛规则

    • 文件:src/summary_mcp/runtime/freshrss_pipeline_jobs.py
    • 文件:src/summary_mcp/runtime/query_service.py
    • 影响:用户看到两个互相打架的状态解释
  2. 状态系统完全依赖显式写回,不会按产物反推修正

    • 文件:src/summary_mcp/runtime/run_store.py
    • 影响:一旦中断,状态比产物更容易脏

C. 调用层误判

  1. 把 resume_run 当成轻量恢复接口使用
    • 实际上它更接近“同步恢复执行器”
    • 影响:在长链路场景下超时是高概率事件

修复目标

当前落地状态(回填)

  • Phase 1 已落地:get_run_status() 会基于 run-report.json 与关键产物做终态收敛,并暴露 status_source / state_conflict
  • Phase 2 已落地第一阶段:resume_run() 会拒绝对已有终态 run-report.json 的 run 继续恢复
  • Phase 2 已继续增强:恢复起点现在会优先根据 artifacts 重算,而不是直接盲信 run-state.recovery.resume_from_stage
  • 新增 inspect_resume_plan(run_id) 作为恢复前置判定接口,避免调用方用 resume_run 探路
  • Phase 2 已补齐生产恢复 artifacts:正式 run 会稳定写出 summary/summary-batch.json 与 candidates/candidate-batch.json,resume_run / inspect_resume_plan 会优先使用它们,而不是依赖 debug per-item 文件
  • 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 判断真实可恢复起点,避免从过早阶段重跑

二级目标(建议达成)

  1. job 层状态结果中增加对 linked run 的补充解释,避免“job running 但 run 产物已齐”这种情况毫无说明
  2. 为后续编排层提供明确可消费的“状态可信度/冲突提示”字段

最小修复方案

Phase 1|先修 run 查询层(优先级最高)

目标

让 get_run_status() 至少能正确识别:

  • run-state 是旧的
  • 但关键产物已经齐了

建议改动点

文件:src/summary_mcp/runtime/query_service.py

建议动作

  • 在 _resolve_run_record() 或 _build_status_response() 中增加“关键产物存在性检查”
    • run-report.json
    • candidates/openclaw-delivery-payload.json
    • candidates/digest-brief.json
  • 如果 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"
  • 保留 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

建议动作

  • 在 _resolve_resume_from_stage() 之前/之后加入真实 artifacts 检查
  • 如果以下文件已存在:
    • openclaw-delivery-payload.json
    • digest-brief.json
    • run-report.json 则不要再从 generate_summaries 或 apply_filters 起跑
  • 为 resume_run() 增加“恢复起点是基于 artifacts 重算还是基于 state 推断”的返回说明
  • 必要时增加更保守逻辑:
    • 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

建议动作

  • 在 get_freshrss_pipeline_job_status() 中,读取 linked run 的关键产物存在性(轻量即可)
  • 若 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"
  • 若 result.json 缺失,但 linked run 已有 run-report.json 与 delivery 产物:
    • get_freshrss_pipeline_job_result() 应能基于 linked run 产物合成最小结果,至少稳定返回 run_id
  • 明确文档: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

建议动作

  • 新增最小异步恢复接口:
    • start_resume_job(run_id)
    • get_resume_job_status(job_id)
    • get_resume_job_result(job_id)
  • job 目录固定落到:
    • outputs/freshrss/resume_jobs/<job_id>/
  • 最少产物约定:
    • run-state.json
    • input.json
    • result.json(成功时)
    • job-report.json
  • start_resume_job 内部先调用 inspect_resume_plan
    • 只有 recommended_action=resume 才允许真正启动
    • read_terminal_result / start_new_run 要直接在 job 输入校验阶段返回,不进入执行器
  • 后台执行时复用现有 _resume_freshrss_run(...)
    • 不重写恢复业务逻辑
    • 只把同步入口拆成异步 job 外壳
  • resume_run(run_id) 保留,但降级为 debug / fallback
    • 文档中明确:OpenClaw 编排默认应走 resume async job,而不是同步 resume_run
  • 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 链路已经从“状态收敛 + 安全恢复点判定”进一步补齐到“正式异步恢复执行”。