{"record":{"id":"c7f9537fdf6372f0","repo":"affaan-m/ECC","slug":"session-not-found-session-id-c7f953","errorCode":null,"errorMessage":"Session not found: {session_id}","messagePattern":"Session not found: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"ecc2/src/session/store.rs","lineNumber":1305,"sourceCode":"        pid: Option<u32>,\n    ) -> Result<()> {\n        let updated = self.conn.execute(\n            \"UPDATE sessions\n             SET state = ?1,\n                 pid = ?2,\n                 updated_at = ?3,\n                 last_heartbeat_at = ?3\n             WHERE id = ?4\",\n            rusqlite::params![\n                state.to_string(),\n                pid.map(i64::from),\n                chrono::Utc::now().to_rfc3339(),\n                session_id,\n            ],\n        )?;\n\n        if updated == 0 {\n            anyhow::bail!(\"Session not found: {session_id}\");\n        }\n\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","sourceCodeStart":1287,"sourceCodeEnd":1323,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/ecc2/src/session/store.rs#L1287-L1323","documentation":"Raised by update_state_and_pid in ecc2/src/session/store.rs:1305 when the UPDATE sessions ... WHERE id = ? statement affects zero rows. update_state_and_pid atomically writes both the state and pid, and a zero row count means no session row matches the supplied session_id. Unlike update_state, this variant does not pre-check existence or transition legality, so the only failure mode is a missing row.","triggerScenarios":"Calling update_state_and_pid with a session_id that was never created, was already deleted, or uses a prefix that get_session would have resolved but raw SQL will not (raw UPDATE does exact-match, not prefix-match like get_session).","commonSituations":"Runtime writes a state update for a session that was concurrently garbage-collected/deleted; a caller stored a truncated session id expecting prefix resolution; a typo or stale id from a cached handle; running updates against the wrong sqlite file.","solutions":["Call get_session(session_id) first (which does prefix matching) and use the resolved full id, or verify the row exists before update.","Ensure only one process owns the DB and that session deletion cannot race state updates; if it can, treat NotFound as a benign terminal state rather than an error.","Confirm the DB path passed to the writer matches the one used to create the session.","Use the full session id, not a prefix, for any update_* or delete_* call."],"exampleFix":"// before: raw id may be stale or truncated\nstore.update_state_and_pid(\"abc\", &SessionState::Running, Some(pid))?;\n\n// after: resolve and guard\nlet session = store.get_session(\"abc\")?.ok_or_else(|| anyhow::anyhow!(\"No session matches id abc\"))?;\nstore.update_state_and_pid(&session.id, &SessionState::Running, Some(pid))?;","handlingStrategy":"validation","validationCode":"// Resolve the canonical (exact) id with prefix matching before any update.\nfn resolve_session_id(store: &Store, id_or_prefix: &str) -> anyhow::Result<String> {\n    let session = store.get_session(id_or_prefix)?\n        .ok_or_else(|| anyhow::anyhow!(\"no session matches {id_or_prefix}\"))?;\n    Ok(session.id)\n}\n\nlet full_id = resolve_session_id(&store, &maybe_prefix)?;\nstore.update_state_and_pid(&full_id, &SessionState::Running, Some(pid))?;","typeGuard":"// Newtype that guarantees a resolved, exact-match session id.\n#[derive(Debug, Clone)]\npub struct ResolvedSessionId(String);\n\nimpl ResolvedSessionId {\n    pub fn resolve(store: &Store, id_or_prefix: &str) -> anyhow::Result<Self> {\n        let s = store.get_session(id_or_prefix)?\n            .ok_or_else(|| anyhow::anyhow!(\"no session matches {id_or_prefix}\"))?;\n        Ok(Self(s.id))\n    }\n    pub fn as_str(&self) -> &str { &self.0 }\n}\n\n// update_state_and_pid takes &ResolvedSessionId so callers cannot pass a raw prefix.","tryCatchPattern":"// update_state_and_pid is called from the runtime; treat NotFound as fatal\n// to the runtime and clean up the child.\nmatch store.update_state_and_pid(&id, &state, pid) {\n    Ok(()) => Ok(()),\n    Err(e) if e.to_string().contains(\"Session not found\") => {\n        let _ = child.kill().await;\n        tracing::error!(\"session {id} vanished before state/pid update\");\n        Err(e)\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Always resolve prefixes via get_session before calling any update_* or delete_* method; those methods exact-match the id.","Keep session lifecycle (create/update/delete) under a single owner so updates cannot race deletes.","Verify the DB path the writer holds is the same file the session was created in.","Avoid passing display ids or truncated ids to store mutators."],"tags":["database","sqlite","session","not-found","update"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}