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

  1. 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.
  2. 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.
  3. Run the insert + read-back inside an explicit transaction so no other write can land between them.
  4. 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

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


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