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
- Check the thread id is valid: SELECT id FROM threads WHERE id = ? before the call.
- Create the thread first (or restore it) before saving the goal.
- If the thread was deleted intentionally, drop or re-parent the goal instead of saving it.
- Confirm you're connected to the same database the thread was written to.
- 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
- Create the thread row before any goal referencing it
- Purge goals when their thread is deleted (cascade)
- Never cache thread ids across database files
- Wrap exists-check + insert in a transaction to avoid races
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
- Another pet recorder is using this output.
- failed to enable WAL for
- channel does not exist
- channel does not exist
- external agy credential file
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)