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
- Pass a terminal status value (one for which `is_active()` is false) as the `status` argument.
- Check the transition caller: if the lane should simply stop, use the `stop` entry point which derives the correct terminal status.
- Inspect the lane record's current status first with the registry read APIs and only request a terminal transition when appropriate.
- 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
- Only call mark_terminal_if_active_* with enum variants known to be terminal.
- Wrap terminal transitions in a helper that takes the record and derives the terminal status itself.
- Recheck status mapping after any LaneStatus enum change.
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
- Agent Mail can be marked read only after delivery
- Agent Mail can be canceled only while queued
- Cannot continue agent
- Cannot follow up agent
- Cannot resume agent : status is ( )
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_seqView on GitHub (pinned to 73e0f67d83)