Hmbown/CodeWhale · error · anyhow::Error
terminal lane transition requires a terminal status
Error message
terminal lane transition requires a terminal status
What it means
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.
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_seqView on GitHub (pinned to 0c42157ee5)
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
Example fix
// before registry.mark_terminal_if_active_fenced(&mut record, LaneStatus::Running, None, |_| Ok(()))?; // after registry.mark_terminal_if_active_fenced(&mut record, LaneStatus::Stopped, None, |_| Ok(()))?;
Defensive patterns
Strategy: type-guard
Validate before calling
if !matches!(status, LaneStatus::Pending | LaneStatus::Running) {
let transition = registry.mark_terminal_if_active_fenced(&mut record, status, expected_seq, before)?;
} else {
// active statuses go through their own update path
} Type guard
fn is_terminal_status(status: LaneStatus) -> bool {
matches!(status, LaneStatus::Stopped | LaneStatus::Failed | LaneStatus::Completed)
} Try / catch
match registry.mark_terminal_if_active_fenced(&mut record, status, seq, before) {
Ok(t) => t,
Err(err) if err.to_string().contains("requires a terminal status") => {
anyhow::bail!("bug: routed active status {status:?} into terminal transition");
}
Err(err) => return Err(err),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- project workspace path cannot be empty
- unknown runtime backend `{other}` (use tmux|inline|vm|ci)
- invalid lane environment key {key:?}
- private lane environment exceeds {} bytes
- serialized lane exit receipt exceeds size bound
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/e007d58c1d7ebd78.
Report an issue: GitHub.