{"record":{"id":"ba844b356daf6ffb","repo":"Hmbown/CodeWhale","slug":"v9","errorCode":"V9","errorMessage":"node {id} is terminal ({:?}) and cannot move to {:?}. Re-sending its current state is fine; changing it needs Supersede.","messagePattern":"node (.+?) is terminal \\((.+?)\\) and cannot move to (.+?)\\. Re-sending its current state is fine; changing it needs Supersede\\.","errorType":"validation","errorClass":"ValidationReport","httpStatus":null,"severity":"error","filePath":"crates/tui/src/work_graph/reducer.rs","lineNumber":258,"sourceCode":"/// a node out of a terminal state.\nfn patch_node(\n    next: &mut WorkGraphSnapshot,\n    id: &WorkNodeId,\n    patch: &WorkNodePatch,\n    now: i64,\n) -> Result<(), ValidationReport> {\n    let node = next\n        .node_mut(id)\n        .ok_or_else(|| structural(format!(\"node {id} not found\")))?;\n    // Only an actual transition OUT of a terminal state is forbidden.\n    // Re-asserting the state a node already holds is a no-op, and rejecting it\n    // broke the only tool that writes here: `work_update` replaces the whole\n    // todo list on every call, so once an item is cancelled every later call\n    // re-sends it as cancelled and the entire update was refused. The model was\n    // then told to \"use Supersede\", which `work_update` does not expose — a\n    // dead end whose only escape was silently dropping the item from the list.\n    if node.state.is_terminal() && patch.state.is_some_and(|s| s != node.state) {\n        return Err(ValidationReport::single(\n            ValidationCode::V9,\n            format!(\n                \"node {id} is terminal ({:?}) and cannot move to {:?}. \\\n                 Re-sending its current state is fine; changing it needs Supersede.\",\n                node.state,\n                patch.state.expect(\"checked above\")\n            ),\n        ));\n    }\n    if let Some(title) = &patch.title {\n        node.title = title.clone();\n    }\n    if let Some(state) = patch.state {\n        node.state = state;\n    }\n    if let Some(acceptance) = &patch.acceptance {\n        node.acceptance = acceptance.clone();\n    }","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/work_graph/reducer.rs#L240-L276","documentation":"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.","triggerScenarios":"Thrown at crates/tui/src/work_graph/reducer.rs:258 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Treat re-sending the node's current terminal state as a no-op rather than an error","To change a finished/cancelled node, send a Supersede patch instead of a direct state transition","If this fires unexpectedly, inspect the patch stream for a stale writer replaying old state as a change"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}