Hmbown/CodeWhale · error

terminal lane transition requires a terminal status

Error message

terminal lane transition requires a terminal status

What it means

This error is thrown by LaneRegistry::mark_terminal_if_active_fenced when a lane status that is still active is passed to a terminal-transition helper. The library only permits moving a lane into a terminal state (e.g. stopped/failed) when the transition target status is terminal; the fenced helper uses this to guarantee atomic, lock-protected terminal transitions.

Solutions

  1. Pass a terminal status value (one for which `is_active()` is false) as the `status` argument.
  2. Check the transition caller: if the lane should simply stop, use the `stop` entry point which derives the correct terminal status.
  3. Inspect the lane record's current status first with the registry read APIs and only request a terminal transition when appropriate.
  4. Update status-mapping code after enum changes so terminal values are no longer confused with active ones.

Example fix

// before
registry.mark_terminal_if_active_fenced(record_id, LaneStatus::Running, |record| { /* ... */ })?;
// after
registry.mark_terminal_if_active_fenced(record_id, LaneStatus::Stopped, |record| { /* ... */ })?;
Defensive patterns

Strategy: validation

Validate before calling

if status.is_active() { panic!("terminal transition requires a terminal status, got {:?}", status); }

Type guard

fn is_terminal(status: &LaneStatus) -> bool { !status.is_active() }

Prevention

When it happens

Trigger: Calling mark_terminal_if_active_fenced (directly or via mark_terminal_if_active_with or stop) with a `status` argument for which `status.is_active()` returns true, e.g. passing Running/Active instead of a terminal status.

Common situations: Mapping an internal lane state enum to the terminal-status parameter in the wrong order; retry/stop logic that reuses an in-memory active status variable; refactorings that changed the status enum without updating stop paths.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/e007d58c1d7ebd78. Report an issue: GitHub.

Appendix: source

Thrown at crates/lane/src/registry.rs:378

    /// A pre-lock check is a TOCTOU: another process can transition the record
    /// between the caller's read and this write, and the caller would then act
    /// on a generation it never observed. Checking here means a stale fence
    /// refuses without running `before_transition`, so no backend teardown
    /// happens for a run the caller did not actually target.
    ///
    /// [`mark_terminal_if_active_with`]: Self::mark_terminal_if_active_with
    pub fn mark_terminal_if_active_fenced<F>(
        &self,
        record: &mut LaneRecord,
        status: LaneStatus,
        expected_lifecycle_seq: Option<u64>,
        before_transition: F,
    ) -> Result<TerminalTransition>
    where
        F: FnOnce(&LaneRecord) -> Result<()>,
    {
        if status.is_active() {
            bail!("terminal lane transition requires a terminal status");
        }

        let lock_path = self.root.join(format!("{}.lock", record.id));
        let lock_file = OpenOptions::new()
            .create(true)
            .truncate(false)
            .read(true)
            .write(true)
            .open(&lock_path)
            .with_context(|| format!("open lane lock {}", lock_path.display()))?;
        let mut lock = fd_lock::RwLock::new(lock_file);
        let _guard = lock
            .write()
            .with_context(|| format!("lock lane record {}", record.id))?;

        let mut current = self.load(&record.id)?;
        // Fence first: a mismatched generation must not run backend teardown.
        if let Some(expected) = expected_lifecycle_seq

View on GitHub (pinned to 73e0f67d83)