task-execution-resume-assumes-planner-artifacts-exist.md 6.09 KB

task_execution.py 恢复逻辑假设 Planner 一定已产出 sdd/prd/benchmark artifact——问题记录

日期: 2026-08-08 性质: 问题记录(真实复现,根因已定位,未修复)。发现于 docs/done/work-graph-self-planning-and-loop-back-requirements.md(Planner 自规划)上线后的第一次真实端到端验证。 状态: 待修复,未立项。2026-08-08 更新:已折入 docs/todo/org-graph-unification-and-universal-self-planning-requirements.md 的 Phase 1(FR-1)——插入"Planner 产出复核"新阶段本来就要重写这段 run_or_resume_task_lifecycle() 分支逻辑,这个 bug 跟那次改动是同一段代码路径,不再单独排期,随 Phase 1 一并修。详细设计已发布:docs/todo/org-graph-unification-phase1-review-gate-design.md §6-7——修复方式是把 sdd/prd/benchmark 的存在性探测下沉到新增的 _review_planner_output_with_retries() 内部(有就用,没有就重新规划),不再在 resume 分支顶部无条件硬读;同一次调研还发现 fresh-run 路径(run_task_lifecycle())有同类但更早触发的 KeyError,一并修复。

1. 现象(真实复现)

任务 2084590a(目标:修复 work_node_run.py 的 execution_log_file UNIQUE 约束 bug):

  1. 第一次真实调用:Planner 自规划出 sdd → benchmark → test_script 三节点图(没有 brd/prd——goal 是个小 bug 修复,Planner 判断不需要),sdd 节点的 build_prompt() 因为 _DOC_PROMPTS["sdd"] 硬编码的 {prd_content} 占位符找不到值而抛 KeyError('prd_content')(已修复,见 e2e_pipeline.py::_MissingContentDefault)。此时 sdd 节点本身从未成功过,artifact 表里这个 task 一行都没有。
  2. 通过 /route 的 manual_resume 恢复:task.status 被设置回 planning_brd_prd_sdd。
  3. 重新执行时走的是 task_execution.py::run_or_resume_task_lifecycle() 的"从 planning_brd_prd_sdd 恢复"分支,而不是从头重跑 plan_documents()——这个分支无条件调用 _artifact_content(conn, ..., "sdd")/"prd"/"benchmark",读不到就直接 TaskExecutionError。因为这个 task 的 Planner 图从未成功过一个节点,三个 artifact 全部不存在,报错为 "task ... has no sdd artifact to resume from",任务再次卡住。

2. 根因

org_node_lifecycle.trigger_org_node_full(conn, "planner", ...) 在 run_task_lifecycle() 里先于 plan_documents() 执行,并把 task.status 设成 _POST_STATE["planner"] = "planning_brd_prd_sdd"(org_node_lifecycle.py:32)——这个状态转移只代表"Planner 的 ready 确认调用成功了",不代表"Planner 的真实规划(plan_documents())已经产出了任何东西"。

task_execution.py::run_or_resume_task_lifecycle() 里 planning_brd_prd_sdd 分支(task_execution.py:126-129)的注释写的是"Planner done, Coder not yet triggered"——这个假设在 Work Graph 自规划(docs/done/work-graph-self-planning-and-loop-back-requirements.md)上线前基本成立:固定链的第一个节点是 brd,几乎不可能在第一个节点就失败到零产出。自规划之后这个假设不再可靠——自规划出的图可能把一个更容易失败的节点(比如需要更复杂 judgment 的 sdd)排在第一位,如果它在第一次尝试就彻底失败(耗尽 loop_back/retry 上限,或者像这次一样是一个跟自规划本身相关的新 bug),Planner 的 work_graph_run 会整个失败,不会有任何 artifact 落盘,但 task.status 已经先一步被置成了 planning_brd_prd_sdd。

test_dataset_content/test_script_content 已经用 _artifact_content_or_default()(读不到就给默认值,不硬失败)处理过同一类问题(2026-08-06,那次是"这两个 artifact 类型本来就允许不存在"),但 sdd/prd/benchmark 三个仍然用会硬失败的 _artifact_content()——这个区分在固定链时代是对的(sdd/prd/benchmark 曾经是保证存在的),自规划之后不再成立。

3. 跟已实现的两个相关修复的区别

  • 不是 work-graph-partial-artifact-persistence-requirements.md 那次修复能覆盖的场景——那次解决的是"图跑到一半、后面节点失败,前面成功节点的产出要保留",这次是"图的第一个节点就彻底失败,压根没有'前面成功节点'可保留",_write_completed_node_artifacts 在这种情况下正确地什么都不落盘(因为真的什么都没成功),不是它的 bug。
  • 不是这次 session 里刚修的 _MissingContentDefault(e2e_pipeline.py::_planner_prompt_builder)能覆盖的——那个修的是"build_prompt 拼提示词时崩溃",这次是崩溃之后恢复逻辑本身的假设过时了,是下游的另一层问题。

4. 可能的修复方向(未验证,仅供后续参考)

在 run_or_resume_task_lifecycle() 的 planning_brd_prd_sdd 分支里,先检查这个 task 是否真的有任何 doc 类 artifact(_latest_artifact_ids(conn, task_id, _DOC_ARTIFACT_TYPES) 是否为空):

  • 如果有(哪怕只有部分,比如这次自规划场景下可能只有 sdd 没有 prd/benchmark),按现有逻辑继续,但 prd/benchmark 也应该跟 test_dataset/test_script 一样改用 _artifact_content_or_default,不再假设它们一定存在(自规划场景下 prd/benchmark 本来就可能被 Planner 判断为不需要,不是"缺失的异常情况")。
  • 如果一个都没有(这次真实复现的情况——Planner 的图从第一个节点就彻底失败,什么都没留下),说明"从这个状态恢复"这个假设本身不成立,应该等价于从 queued 重新完整跑一次 run_task_lifecycle()(即重新调用 plan_documents()),而不是尝试读取不存在的 artifact。

5. 临时规避(已用于解决这次真实卡住的任务)

直接把 task.status 手动改回 queued(UPDATE task SET status='queued', blocked_reason=NULL WHERE task_id=...),让下一次执行走 run_task_lifecycle() 的"queued"分支、重新完整调用一次 plan_documents()——不是通用修复,只是这一次真实卡住的任务的人工解法。