affaan-m/ECC · error · anyhow::Error
Scheduled task {id} was not found after insert
Error message
Scheduled task {id} was not found after insert What it means
Raised by the scheduled-task insertion helper in ecc2/src/session/store.rs:1537 when, immediately after INSERT INTO scheduled_tasks and reading last_insert_rowid, get_scheduled_task(id) returns None. last_insert_rowid is connection-local in rusqlite, so a None read-back indicates the row is not visible to the same connection that just inserted it, or a trigger/INSTEAD OF rule rewrote the insert.
Source
Thrown at ecc2/src/session/store.rs:1537
updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
rusqlite::params![
cron_expr,
task,
agent_type,
profile_name,
working_dir.display().to_string(),
project,
task_group,
if use_worktree { 1_i64 } else { 0_i64 },
next_run_at.to_rfc3339(),
now.to_rfc3339(),
now.to_rfc3339(),
],
)?;
let id = self.conn.last_insert_rowid();
self.get_scheduled_task(id)?
.ok_or_else(|| anyhow::anyhow!("Scheduled task {id} was not found after insert"))
}
pub fn list_scheduled_tasks(&self) -> Result<Vec<ScheduledTask>> {
let mut stmt = self.conn.prepare(
"SELECT id, cron_expr, task, agent_type, profile_name, working_dir, project, task_group,
use_worktree, last_run_at, next_run_at, created_at, updated_at
FROM scheduled_tasks
ORDER BY next_run_at ASC, id ASC",
)?;
let rows = stmt.query_map([], map_scheduled_task)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}
pub fn list_due_scheduled_tasks(
&self,
now: chrono::DateTime<chrono::Utc>,
limit: usize,View on GitHub (pinned to 01e15490f0)
Solutions
- Inspect the scheduled_tasks schema (SELECT sql FROM sqlite_master WHERE name='scheduled_tasks') for triggers or view definitions and remove any that intercept the insert.
- Ensure the rusqlite Connection used by store is not shared across threads without the mutex/actor serialization; last_insert_rowid must be read on the same connection that did the INSERT with no intervening write.
- Run the insert + read-back inside an explicit transaction so no other write can land between them.
- If the id derivation is the issue, switch get_scheduled_task to SELECT by rowid explicitly.
Example fix
// before: read-back may miss on a contended connection
self.conn.execute("INSERT INTO scheduled_tasks ...", params![...])?;
let id = self.conn.last_insert_rowid();
self.get_scheduled_task(id)?.ok_or_else(|| anyhow!("not found after insert"))
// after: hold a transaction so the read-back sees the row
let tx = self.conn.transaction()?;
tx.execute("INSERT INTO scheduled_tasks ...", params![...])?;
let id = tx.last_insert_rowid();
let task = get_scheduled_task_in_tx(&tx, id)?.ok_or_else(|| anyhow!("not found after insert"))?;
tx.commit()?;
Ok(task) Defensive patterns
Strategy: validation
Validate before calling
// Wrap insert + read-back in a transaction so no other write can land
// between them, and read back inside the same tx.
pub fn create_scheduled_task_atomic(&self, /* fields */) -> anyhow::Result<ScheduledTask> {
let tx = self.conn.transaction()?;
tx.execute(
"INSERT INTO scheduled_tasks (...) VALUES (...)",
rusqlite::params![...],
)?;
let id = tx.last_insert_rowid();
let task = get_scheduled_task_in_tx(&tx, id)?
.ok_or_else(|| anyhow::anyhow!("scheduled task {id} not found after insert"))?;
tx.commit()?;
Ok(task)
}
// Also assert no intercepting triggers exist before relying on this path.
fn assert_no_scheduled_tasks_triggers(conn: &rusqlite::Connection) -> anyhow::Result<()> {
let n: i64 = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='trigger' AND tbl_name='scheduled_tasks'",
[], |r| r.get(0),
)?;
if n > 0 { anyhow::bail!("{n} triggers on scheduled_tasks may intercept inserts"); }
Ok(())
} Type guard
// No type guard; the issue is connection/transaction usage. The defense is // the transaction wrapper and the trigger audit above.
Try / catch
// Surface read-back failure with the trigger/connection context so the
// operator knows where to look.
match store.create_scheduled_task(/* ... */) {
Ok(task) => Ok(task),
Err(e) if e.to_string().contains("not found after insert") => {
tracing::error!("scheduled task insert/read-back mismatch; check triggers and shared connections");
Err(e)
}
Err(e) => Err(e),
} Prevention
- Wrap insert + read-back in a single transaction; read back inside the tx.
- Ensure the rusqlite Connection is used by one logical writer at a time (DbWriter actor pattern); last_insert_rowid is connection-local and order-sensitive.
- Audit sqlite_master for triggers/views on scheduled_tasks after any schema migration.
- Do not share a Connection across threads without serialization.
When it happens
Trigger: A SQLite trigger on scheduled_tasks redirects the insert into another table; the connection is in WAL mode and the read-back uses a different snapshot (unusual for the same connection); last_insert_rowid returns an id that does not correspond to a scheduled_tasks row because another insert preempted it on a shared connection; a schema where scheduled_tasks is a VIEW with INSTEAD OF triggers.
Common situations: A third-party migration added triggers on scheduled_tasks; the rusqlite Connection is shared across threads without serialization and a concurrent insert changed last_insert_rowid; manual schema edits turned scheduled_tasks into a view.
Related errors
- Remote dispatch request {id} was not found after insert
- candidate alias integrity verification failed
- Session not found: {session_id}
- Missing value for --db
- Work item not found: ${id}
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/3a825384b37bc826.
Report an issue: GitHub.