{"record":{"id":"9fc427c06cf58e99","repo":"affaan-m/ECC","slug":"invalid-session-state-transition","errorCode":null,"errorMessage":"Invalid session state transition: {} -> {}","messagePattern":"Invalid session state transition: (.+?) -> (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"ecc2/src/session/store.rs","lineNumber":1325,"sourceCode":"\n        self.refresh_session_board_meta()?;\n        Ok(())\n    }\n\n    pub fn update_state(&self, session_id: &str, state: &SessionState) -> Result<()> {\n        let current_state = self\n            .conn\n            .query_row(\n                \"SELECT state FROM sessions WHERE id = ?1\",\n                [session_id],\n                |row| row.get::<_, String>(0),\n            )\n            .optional()?\n            .map(|raw| SessionState::from_db_value(&raw))\n            .ok_or_else(|| anyhow::anyhow!(\"Session not found: {session_id}\"))?;\n\n        if !current_state.can_transition_to(state) {\n            anyhow::bail!(\n                \"Invalid session state transition: {} -> {}\",\n                current_state,\n                state\n            );\n        }\n\n        let updated = self.conn.execute(\n            \"UPDATE sessions\n             SET state = ?1,\n                 updated_at = ?2,\n                 last_heartbeat_at = ?2\n             WHERE id = ?3\",\n            rusqlite::params![\n                state.to_string(),\n                chrono::Utc::now().to_rfc3339(),\n                session_id,\n            ],\n        )?;","sourceCodeStart":1307,"sourceCodeEnd":1343,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/ecc2/src/session/store.rs#L1307-L1343","documentation":"SessionStore::update_state enforces a finite state machine on session lifecycle states. It loads the current state and calls SessionState::can_transition_to; if the requested new state is not an allowed transition from the current one, it bails with 'Invalid session state transition: {old} -> {new}'. This prevents illegal jumps like Running -> Pending or updates to a terminal state.","triggerScenarios":"Calling update_state(session_id, new_state) where the session's current DB state cannot legally move to new_state — e.g. activating an already-active session, queuing a session that is Stopped, or double-stopping via stop_session_recorded.","commonSituations":"Heartbeat enforcers racing a stop (session moves to Stopped mid-flight, then an enforcer tries Running->Running or Running->Completed); create_session_in_dir/queue_session_with_resolved_profile_and_runner_program retrying activation after a prior transition already succeeded; duplicate webhook or timer callbacks firing the same transition twice.","solutions":["Check the session's current state first and only call update_state when can_transition_to allows the move.","Make transitions idempotent in your caller: if current == target, skip the call instead of erroring.","Serialize session mutations (single writer / locking) so enforcer loops don't race stop/start flows.","Catch the error and re-read the current state to decide whether the transition was already handled."],"exampleFix":"// before\nstore.update_state(&session_id, SessionState::Running)?;\n\n// after\nlet s = store.get_session(&session_id)?.ok_or_else(|| anyhow::anyhow!(\"no session\"))?;\nif s.state != SessionState::Running {\n    store.update_state(&session_id, SessionState::Running)?;\n}","handlingStrategy":"validation","validationCode":"let current = store.get_session(&id)?.ok_or_else(|| anyhow::anyhow!(\"no session {id}\"))?.state;\nif current.can_transition_to(&target) {\n    store.update_state(&id, target)?;\n}","typeGuard":"fn can_apply(current: &SessionState, target: &SessionState) -> bool {\n    current.can_transition_to(target)\n}","tryCatchPattern":"match store.update_state(&id, target) {\n    Err(e) if e.to_string().contains(\"Invalid session state transition\") => {\n        let cur = store.get_session(&id)?; // reconcile with DB state\n    }\n    other => other?,\n}","preventionTips":["Make transition callers idempotent: skip when current == target.","Serialize state changes per session (single writer or row lock).","Model the allowed transition table explicitly and unit-test it."],"tags":["state-machine","session-store","lifecycle","invalid-transition"],"backgroundTag":"invalid-state-transition","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}