# `task_execution.py` 恢复逻辑假设 Planner 一定已产出 sdd/prd/benchmark artifact——问题记录 **日期**: 2026-08-08 **性质**: 问题记录(真实复现,根因已定位,未修复)。发现于 `docs/done/work-graph-self-planning-and-loop-back-requirements.md`(Planner 自规划)上线后的第一次真实端到端验证。 **状态**: ✅ 已修复(commit `ea19c7d`,2026-08-08,随 Phase 1 一并落地)。修复方式与 §4"可能的修复方向"一致:sdd/prd/benchmark 的存在性探测下沉到新增的 `_review_planner_output_with_retries()` 内部(有就用,没有就重新规划),不再在 resume 分支顶部无条件硬读;同一次实现还修复了 fresh-run 路径(`run_task_lifecycle()`)里同类但更早触发的 `KeyError`。详细设计见 `docs/done/org-graph-unification-phase1-review-gate-design.md` §6-7,`python3 -m unittest discover -s src` 全绿(2026-08-12 复核)。 ## 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()`——不是通用修复,只是这一次真实卡住的任务的人工解法。