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
- 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.
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
- 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.
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
- Work item not found: ${id}
- 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/c7f9537fdf6372f0.
Report an issue: GitHub.