affaan-m/ECC · error · anyhow::Error
Invalid session state transition: {} -> {}
Error message
Invalid session state transition: {} -> {} What it means
Raised by update_state in ecc2/src/session/store.rs:1325 when the current SessionState cannot legally transition to the requested one per can_transition_to (mod.rs:347). The allowed graph: Pending->{Running,Failed,Stopped}; Running->{Idle,Stale,Completed,Failed,Stopped}; Idle/Stale->{Running,Idle,Stale,Completed,Failed,Stopped}; only Completed->Stopped and Failed->Stopped are valid from terminal states. Anything else (e.g. Pending->Idle, Stopped->Running, Completed->Running) bails.
Source
Thrown at ecc2/src/session/store.rs:1325
self.refresh_session_board_meta()?;
Ok(())
}
pub fn update_state(&self, session_id: &str, state: &SessionState) -> Result<()> {
let current_state = self
.conn
.query_row(
"SELECT state FROM sessions WHERE id = ?1",
[session_id],
|row| row.get::<_, String>(0),
)
.optional()?
.map(|raw| SessionState::from_db_value(&raw))
.ok_or_else(|| anyhow::anyhow!("Session not found: {session_id}"))?;
if !current_state.can_transition_to(state) {
anyhow::bail!(
"Invalid session state transition: {} -> {}",
current_state,
state
);
}
let updated = self.conn.execute(
"UPDATE sessions
SET state = ?1,
updated_at = ?2,
last_heartbeat_at = ?2
WHERE id = ?3",
rusqlite::params![
state.to_string(),
chrono::Utc::now().to_rfc3339(),
session_id,
],
)?;View on GitHub (pinned to 01e15490f0)
Solutions
- Check the transition legality with current_state.can_transition_to(&desired) before calling update_state, and branch on illegal transitions (e.g. create a new session instead of resurrecting).
- For watchdog/reaper code, skip Stale updates on terminal (Completed/Failed/Stopped) states.
- Order state events by timestamp or use a single state owner to prevent out-of-order terminal transitions.
- If resurrection is genuinely required, insert a new session row rather than transitioning a terminal one.
Example fix
// before: blind transition that may be illegal
store.update_state(&id, &SessionState::Running)?;
// after: gate on the legal graph, branch on rejection
let current = store.get_session(&id)?.and_then(|s| s.state.ok_or(()));
let desired = SessionState::Running;
if !current.can_transition_to(&desired) {
// create a fresh session instead of resurrecting a terminal one
return Ok(store.create_session(...)?.id);
}
store.update_state(&id, &desired)?; Defensive patterns
Strategy: validation
Validate before calling
// Check can_transition_to before calling update_state and branch on refusal.
use crate::session::SessionState;
fn transition_or_recreate(store: &Store, id: &str, desired: SessionState) -> anyhow::Result<()> {
let session = store.get_session(id)?
.ok_or_else(|| anyhow::anyhow!("no session matches {id}"))?;
let current = session.state;
if !current.can_transition_to(&desired) {
if matches!(current, SessionState::Stopped | SessionState::Completed | SessionState::Failed)
&& desired == SessionState::Running
{
anyhow::bail!("cannot resurrect {id}; create a new session instead");
}
return Ok(()); // ignore no-op / illegal transitions
}
store.update_state(id, &desired)
} Type guard
// A SessionState guard is already provided by the enum's can_transition_to.
// Wrap it in a function returning Result to centralize policy.
pub fn assert_legal_transition(from: &SessionState, to: &SessionState) -> anyhow::Result<()> {
if from.can_transition_to(to) { Ok(()) } else {
anyhow::bail!("illegal transition {from} -> {to}")
}
} Try / catch
// Distinguish illegal-transition from not-found in the catch.
match store.update_state(&id, &next) {
Ok(()) => Ok(()),
Err(e) if e.to_string().contains("Invalid session state transition") => {
tracing::warn!("illegal transition for {id}; ignoring");
Ok(())
}
Err(e) if e.to_string().contains("Session not found") => {
tracing::warn!("session {id} gone during transition");
Ok(())
}
Err(e) => Err(e),
} Prevention
- Always call can_transition_to before update_state; branch on the result.
- Never try to resurrect Stopped/Completed/Failed sessions; create a new session row instead.
- Make watchdogs skip Stale updates on terminal states.
- Order state events by timestamp when processing from a queue.
When it happens
Trigger: Attempting to mark a Stopped session Running (resurrection); transitioning Pending directly to Idle or Completed without going through Running; calling update_state(Completed) on a session already in Stopped state; a watchdog marking a Completed session Stale.
Common situations: Restart logic that tries to revive a stopped/failed session in place instead of creating a new one; a heartbeat watchdog that sets Stale on a Completed row before the completion update lands; out-of-order event processing where Completed arrives after Stopped.
Related errors
- invalid plan-canvas session key
- Session not found: {from_id}
- No sessions found
- Session not found: {value}
- Session not found: {session_id}
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/9fc427c06cf58e99.
Report an issue: GitHub.