Hmbown/CodeWhale · error

thread not found

Error message

thread {} not found

What it means

upsert_thread_goal saves a goal attached to a thread, but first verifies the referenced thread row exists. If the SELECT returns no row, it bails with 'thread {id} not found' rather than creating an orphaned goal. This is a foreign-key-style guard implemented in application code.

Solutions

  1. Check the thread id is valid: SELECT id FROM threads WHERE id = ? before the call.
  2. Create the thread first (or restore it) before saving the goal.
  3. If the thread was deleted intentionally, drop or re-parent the goal instead of saving it.
  4. Confirm you're connected to the same database the thread was written to.
  5. For races, wrap the exists-check and insert in one transaction and handle the constraint failure.

Example fix

// before: saving with a possibly stale id
state.upsert_thread_goal(&goal)?;
// after: verify or create the thread first
if !state.thread_exists(&goal.thread_id)? {
    state.create_thread(&goal.thread_id)?;
}
state.upsert_thread_goal(&goal)?;
Defensive patterns

Strategy: validation

Validate before calling

fn thread_exists(conn: &Connection, id: &str) -> rusqlite::Result<bool> {
    conn.query_row("SELECT 1 FROM threads WHERE id = ?1", params![id], |_| Ok(true))
        .optional().map(|o| o.unwrap_or(false))
}
// call: ensure thread_exists(conn, &goal.thread_id)? before upserting

Type guard

fn goal_has_live_thread(conn: &Connection, goal: &Goal) -> bool {
    thread_exists(conn, &goal.thread_id).unwrap_or(false)
}

Try / catch

match state.upsert_thread_goal(&goal) {
    Err(e) if e.to_string().contains("not found") => {
        state.create_thread(&goal.thread_id)?;
        state.upsert_thread_goal(&goal)?;
    }
    r => r?,
}

Prevention

When it happens

Trigger: Calling upsert_thread_goal(conn, goal) with a goal.thread_id that has no corresponding row in the threads table (typo'd id, thread deleted before the goal write, id from a different database).

Common situations: UI kept a stale thread id after the thread was deleted; goal persisted from a different session/workspace db; race where another process removed the thread between listing and saving; test fixtures referencing ids never inserted.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/09965c834ab136b7. Report an issue: GitHub.

Appendix: source

Thrown at crates/state/src/lib.rs:851

            goal.last_gap_pass,
            u32::try_from(goal.continuation_count.max(0)).unwrap_or(u32::MAX),
        )
        .map_err(anyhow::Error::msg)?;
        let pause_reason = goal
            .pause_reason
            .map(|reason| serde_json::to_string(&reason))
            .transpose()?;
        let conn = self.conn()?;
        let exists: Option<i64> = conn
            .query_row(
                "SELECT 1 FROM threads WHERE id = ?1",
                params![goal.thread_id],
                |row| row.get(0),
            )
            .optional()
            .context("failed to verify thread before saving goal")?;
        if exists.is_none() {
            anyhow::bail!("thread {} not found", goal.thread_id);
        }

        conn.execute(
            r#"
            INSERT INTO thread_goals (
                thread_id, goal_id, objective, status, token_budget, tokens_used,
                time_used_seconds, continuation_count, created_at, updated_at,
                last_gap_fingerprint, repeated_gap_count, last_gap_pass, pause_reason
            ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)
            ON CONFLICT(thread_id) DO UPDATE SET
                goal_id=excluded.goal_id,
                objective=excluded.objective,
                status=excluded.status,
                token_budget=excluded.token_budget,
                tokens_used=excluded.tokens_used,
                time_used_seconds=excluded.time_used_seconds,
                continuation_count=excluded.continuation_count,
                created_at=excluded.created_at,

View on GitHub (pinned to 73e0f67d83)