Hmbown/CodeWhale · error · ValidationReport

V9

V9

Error message

node {id} is terminal ({:?}) and cannot move to {:?}. Re-sending its current state is fine; changing it needs Supersede.

What it means

patch_node is a validation guard in the work-graph reducer: it rejects a WorkNodePatch that moves a node out of a terminal state (e.g. cancelled/done), while re-asserting the current state stays a no-op. The message names the node id, its terminal state, and the attempted target, and tells the caller to use Supersede for genuine state changes — because work_update rewrites the whole todo list, re-sending cancelled items is expected and must not error.

Source

Thrown at crates/tui/src/work_graph/reducer.rs:258

/// a node out of a terminal state.
fn patch_node(
    next: &mut WorkGraphSnapshot,
    id: &WorkNodeId,
    patch: &WorkNodePatch,
    now: i64,
) -> Result<(), ValidationReport> {
    let node = next
        .node_mut(id)
        .ok_or_else(|| structural(format!("node {id} not found")))?;
    // Only an actual transition OUT of a terminal state is forbidden.
    // Re-asserting the state a node already holds is a no-op, and rejecting it
    // broke the only tool that writes here: `work_update` replaces the whole
    // todo list on every call, so once an item is cancelled every later call
    // re-sends it as cancelled and the entire update was refused. The model was
    // then told to "use Supersede", which `work_update` does not expose — a
    // dead end whose only escape was silently dropping the item from the list.
    if node.state.is_terminal() && patch.state.is_some_and(|s| s != node.state) {
        return Err(ValidationReport::single(
            ValidationCode::V9,
            format!(
                "node {id} is terminal ({:?}) and cannot move to {:?}. \
                 Re-sending its current state is fine; changing it needs Supersede.",
                node.state,
                patch.state.expect("checked above")
            ),
        ));
    }
    if let Some(title) = &patch.title {
        node.title = title.clone();
    }
    if let Some(state) = patch.state {
        node.state = state;
    }
    if let Some(acceptance) = &patch.acceptance {
        node.acceptance = acceptance.clone();
    }

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Treat re-sending the node's current terminal state as a no-op rather than an error
  2. To change a finished/cancelled node, send a Supersede patch instead of a direct state transition
  3. If this fires unexpectedly, inspect the patch stream for a stale writer replaying old state as a change
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at crates/tui/src/work_graph/reducer.rs:258 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/ba844b356daf6ffb. Report an issue: GitHub.