affaan-m/ECC · error · anyhow::Error

Session not found: {session_id}

Error message

Session not found: {session_id}

What it means

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.

Source

Thrown at ecc2/src/session/store.rs:1305

        pid: Option<u32>,
    ) -> Result<()> {
        let updated = self.conn.execute(
            "UPDATE sessions
             SET state = ?1,
                 pid = ?2,
                 updated_at = ?3,
                 last_heartbeat_at = ?3
             WHERE id = ?4",
            rusqlite::params![
                state.to_string(),
                pid.map(i64::from),
                chrono::Utc::now().to_rfc3339(),
                session_id,
            ],
        )?;

        if updated == 0 {
            anyhow::bail!("Session not found: {session_id}");
        }

        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}"))?;

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Call get_session(session_id) first (which does prefix matching) and use the resolved full id, or verify the row exists before update.
  2. 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.
  3. Confirm the DB path passed to the writer matches the one used to create the session.
  4. Use the full session id, not a prefix, for any update_* or delete_* call.

Example fix

// before: raw id may be stale or truncated
store.update_state_and_pid("abc", &SessionState::Running, Some(pid))?;

// after: resolve and guard
let session = store.get_session("abc")?.ok_or_else(|| anyhow::anyhow!("No session matches id abc"))?;
store.update_state_and_pid(&session.id, &SessionState::Running, Some(pid))?;
Defensive patterns

Strategy: validation

Validate before calling

// Resolve the canonical (exact) id with prefix matching before any update.
fn resolve_session_id(store: &Store, id_or_prefix: &str) -> anyhow::Result<String> {
    let session = store.get_session(id_or_prefix)?
        .ok_or_else(|| anyhow::anyhow!("no session matches {id_or_prefix}"))?;
    Ok(session.id)
}

let full_id = resolve_session_id(&store, &maybe_prefix)?;
store.update_state_and_pid(&full_id, &SessionState::Running, Some(pid))?;

Type guard

// Newtype that guarantees a resolved, exact-match session id.
#[derive(Debug, Clone)]
pub struct ResolvedSessionId(String);

impl ResolvedSessionId {
    pub fn resolve(store: &Store, id_or_prefix: &str) -> anyhow::Result<Self> {
        let s = store.get_session(id_or_prefix)?
            .ok_or_else(|| anyhow::anyhow!("no session matches {id_or_prefix}"))?;
        Ok(Self(s.id))
    }
    pub fn as_str(&self) -> &str { &self.0 }
}

// update_state_and_pid takes &ResolvedSessionId so callers cannot pass a raw prefix.

Try / catch

// update_state_and_pid is called from the runtime; treat NotFound as fatal
// to the runtime and clean up the child.
match store.update_state_and_pid(&id, &state, pid) {
    Ok(()) => Ok(()),
    Err(e) if e.to_string().contains("Session not found") => {
        let _ = child.kill().await;
        tracing::error!("session {id} vanished before state/pid update");
        Err(e)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/c7f9537fdf6372f0. Report an issue: GitHub.