Implements docs/todo/work-graph-time-travel-debugging-design.md, backend
and console together (user confirmed 2026-08-06).
Key finding that shrank the v1 scope: ux_work_graph_run_active is a
PARTIAL unique index (only covers non-terminal statuses), not a blanket
one -- forking from a parent run that's already reached a terminal status
never collides with it at all. v1 restricts forking to exactly that case,
so no index/constraint change is needed, only two new nullable columns
(parent_run_id, fork_of_node_exec_id).
Resolved the requirements doc's FR-1 ambiguity ("override input_payload,
re-trigger execution") as OUTPUT SUBSTITUTION instead: the fork point's
own output_payload is replaced with the human-supplied content directly,
it is never re-executed via a new claude -p call -- only the strict
downstream subgraph actually re-runs. This sidesteps reconciling the
different build_prompt/input_payload conventions each caller uses
(Planner's artifact_type lookup, E's generic task_prompt field, F's jury)
-- work_graph_fork.py only needs the persisted graph_manifest and each
node's output_payload.
Implementation needs ZERO changes to the execution engine: pre-seeding
every non-downstream node as a 'done' work_node_execution row before
calling the unmodified run_graph() on the unmodified graph_manifest is
enough -- run_work_node()'s existing REQ-06 breakpoint-recovery
short-circuit (already-'done' nodes return their persisted output without
calling run_fn) does all the "reuse without re-executing" work for free.
_reconstruct_build_prompt supports two graph shapes generically from
persisted state alone (Planner's canonical doc chain via
e2e_pipeline._planner_prompt_builder, and E's generic task_prompt field);
anything else raises ForkGraphShapeUnsupportedError rather than producing
broken prompts.
New: src/work_graph_fork.py (create_fork/execute_fork/fork_work_graph_run/
start_fork_in_background, the last splitting fast DB-only setup from slow
real execution so an HTTP caller gets run_id back immediately, same
pattern task_execution.start_task_execution_in_background already uses).
work_graph_run gets parent_run_id/fork_of_node_exec_id (SCHEMA_SQL +
migrate_add_work_graph_fork_columns for existing databases).
work_graph_run_query.py/task_query.py surface both fields. New endpoint
POST /api/work-nodes/:node_exec_id/fork. Console: a "从这里分叉" form on
each 'done' node in v_run_detail, a fork badge linking back to the parent
run, and a 🔀 marker on v_graph_console's run list -- no diff/merge UI
(non-goal #3), read-only branch visibility only.
Verified end-to-end for real: started the actual backend, sent a real
HTTP fork request, confirmed the new run persisted with correct
parent_run_id/fork_of_node_exec_id lineage.
Tests cover requirements doc acceptance standards 1-3 (PRD swapped ->
SDD/Benchmark genuinely re-execute, BRD untouched; original run completely
unaffected; the same node forked twice produces independent branches) plus
schema migration idempotency, query-layer field surfacing, the HTTP route,
and 3 frontend smoke-test assertions. Full src suite (608 tests) + all 3
frontend smoke tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>