{"record":{"id":"e007d58c1d7ebd78","repo":"Hmbown/CodeWhale","slug":"terminal-lane-transition-requires-a-terminal-statu","errorCode":null,"errorMessage":"terminal lane transition requires a terminal status","messagePattern":"terminal lane transition requires a terminal status","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/lane/src/registry.rs","lineNumber":378,"sourceCode":"    /// A pre-lock check is a TOCTOU: another process can transition the record\n    /// between the caller's read and this write, and the caller would then act\n    /// on a generation it never observed. Checking here means a stale fence\n    /// refuses without running `before_transition`, so no backend teardown\n    /// happens for a run the caller did not actually target.\n    ///\n    /// [`mark_terminal_if_active_with`]: Self::mark_terminal_if_active_with\n    pub fn mark_terminal_if_active_fenced<F>(\n        &self,\n        record: &mut LaneRecord,\n        status: LaneStatus,\n        expected_lifecycle_seq: Option<u64>,\n        before_transition: F,\n    ) -> Result<TerminalTransition>\n    where\n        F: FnOnce(&LaneRecord) -> Result<()>,\n    {\n        if status.is_active() {\n            bail!(\"terminal lane transition requires a terminal status\");\n        }\n\n        let lock_path = self.root.join(format!(\"{}.lock\", record.id));\n        let lock_file = OpenOptions::new()\n            .create(true)\n            .truncate(false)\n            .read(true)\n            .write(true)\n            .open(&lock_path)\n            .with_context(|| format!(\"open lane lock {}\", lock_path.display()))?;\n        let mut lock = fd_lock::RwLock::new(lock_file);\n        let _guard = lock\n            .write()\n            .with_context(|| format!(\"lock lane record {}\", record.id))?;\n\n        let mut current = self.load(&record.id)?;\n        // Fence first: a mismatched generation must not run backend teardown.\n        if let Some(expected) = expected_lifecycle_seq","sourceCodeStart":360,"sourceCodeEnd":396,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/lane/src/registry.rs#L360-L396","documentation":"Thrown by LaneRegistry::mark_terminal_if_active_fenced when the status argument is an active status. The API exists to move a lane from an active state (pending/running) to a terminal one; LaneStatus::is_active() covers Pending and Running, so passing either bails immediately, before any locking happens. Valid terminal statuses are stopped, failed, and completed.","triggerScenarios":"Calling mark_terminal_if_active_fenced(&mut record, LaneStatus::Running, ...) — typically a wrapper that maps an incoming status string/enum straight into the call, or code that confuses 'set status' semantics with 'finish the lane' semantics.","commonSituations":"Generic status-update adapters feeding arbitrary LaneStatus values into the terminal-transition API; restart flows that try to re-mark a lane as running through the terminal path instead of the start path.","solutions":["Pass a terminal status: LaneStatus::Completed, LaneStatus::Failed, or LaneStatus::Stopped","To stop a running lane, use LaneStatus::Stopped; to record success/failure use Completed/Failed","If the input status is dynamic, gate the call with !status.is_active() and route active statuses to the appropriate non-terminal update path"],"exampleFix":"// before\nregistry.mark_terminal_if_active_fenced(&mut record, LaneStatus::Running, None, |_| Ok(()))?;\n\n// after\nregistry.mark_terminal_if_active_fenced(&mut record, LaneStatus::Stopped, None, |_| Ok(()))?;","handlingStrategy":"type-guard","validationCode":"if !matches!(status, LaneStatus::Pending | LaneStatus::Running) {\n    let transition = registry.mark_terminal_if_active_fenced(&mut record, status, expected_seq, before)?;\n} else {\n    // active statuses go through their own update path\n}","typeGuard":"fn is_terminal_status(status: LaneStatus) -> bool {\n    matches!(status, LaneStatus::Stopped | LaneStatus::Failed | LaneStatus::Completed)\n}","tryCatchPattern":"match registry.mark_terminal_if_active_fenced(&mut record, status, seq, before) {\n    Ok(t) => t,\n    Err(err) if err.to_string().contains(\"requires a terminal status\") => {\n        anyhow::bail!(\"bug: routed active status {status:?} into terminal transition\");\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Encode the active/terminal split in your own types so only terminal statuses can reach this API","Cover the mapping from external status strings to LaneStatus with tests asserting terminal-only inputs"],"tags":["rust","lane","state-machine","lifecycle","validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}