affaan-m/ECC · error · anyhow::Error

Remote dispatch request {id} was not found after insert

Error message

Remote dispatch request {id} was not found after insert

What it means

Raised by the remote-dispatch-request insertion helper in ecc2/src/session/store.rs:1670 when get_remote_dispatch_request(id) returns None right after INSERT INTO ... and last_insert_rowid. Same class as the scheduled-task read-back failure: the row is not visible to the inserting connection, typically due to triggers, a shared connection, or the table being a view.

Source

Thrown at ecc2/src/session/store.rs:1670

                target_session_id,
                task,
                target_url,
                task_priority_db_value(priority),
                agent_type,
                profile_name,
                working_dir.display().to_string(),
                project,
                task_group,
                if use_worktree { 1_i64 } else { 0_i64 },
                source,
                requester,
                now.to_rfc3339(),
                now.to_rfc3339(),
            ],
        )?;
        let id = self.conn.last_insert_rowid();
        self.get_remote_dispatch_request(id)?.ok_or_else(|| {
            anyhow::anyhow!("Remote dispatch request {id} was not found after insert")
        })
    }

    pub fn list_remote_dispatch_requests(
        &self,
        include_processed: bool,
        limit: usize,
    ) -> Result<Vec<RemoteDispatchRequest>> {
        let sql = if include_processed {
            "SELECT id, request_kind, target_session_id, task, target_url, priority, agent_type, profile_name, working_dir,
                    project, task_group, use_worktree, source, requester, status,
                    result_session_id, result_action, error, created_at, updated_at, dispatched_at
             FROM remote_dispatch_requests
             ORDER BY CASE status WHEN 'pending' THEN 0 WHEN 'failed' THEN 1 ELSE 2 END ASC,
                      priority DESC, created_at ASC, id ASC
             LIMIT ?1"
        } else {
            "SELECT id, request_kind, target_session_id, task, target_url, priority, agent_type, profile_name, working_dir,

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Check SELECT sql FROM sqlite_master WHERE type='trigger' AND tbl_name LIKE '%dispatch%' for intercepting triggers.
  2. Wrap insert + read-back in a transaction and read back within the same tx so no other write can interfere.
  3. Guarantee the connection is used by one logical writer at a time (the DbWriter actor pattern); last_insert_rowid is only meaningful before any other write on that connection.
  4. SELECT by rowid in get_remote_dispatch_request to rule out id-derivation bugs.

Example fix

// before
self.conn.execute("INSERT INTO ... dispatch ...", params![...])?;
let id = self.conn.last_insert_rowid();
self.get_remote_dispatch_request(id)?.ok_or_else(|| anyhow!("not found after insert"))

// after: atomic insert + read-back in one tx
let tx = self.conn.transaction()?;
tx.execute("INSERT INTO ... dispatch ...", params![...])?;
let id = tx.last_insert_rowid();
let req = get_remote_dispatch_request_in_tx(&tx, id)?.ok_or_else(|| anyhow!("not found after insert"))?;
tx.commit()?;
Ok(req)
Defensive patterns

Strategy: validation

Validate before calling

// Same transactional defense as errorIndex 710, applied to dispatch requests.
pub fn create_remote_dispatch_request_atomic(&self, /* fields */) -> anyhow::Result<RemoteDispatchRequest> {
    let tx = self.conn.transaction()?;
    tx.execute(
        "INSERT INTO remote_dispatch_requests (...) VALUES (...)",
        rusqlite::params![...],
    )?;
    let id = tx.last_insert_rowid();
    let req = get_remote_dispatch_request_in_tx(&tx, id)?
        .ok_or_else(|| anyhow::anyhow!("dispatch request {id} not found after insert"))?;
    tx.commit()?;
    Ok(req)
}

fn assert_no_dispatch_triggers(conn: &rusqlite::Connection) -> anyhow::Result<()> {
    let n: i64 = conn.query_row(
        "SELECT COUNT(*) FROM sqlite_master WHERE type='trigger' AND tbl_name LIKE '%dispatch%'",
        [], |r| r.get(0),
    )?;
    if n > 0 { anyhow::bail!("{n} triggers on dispatch tables 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

match store.create_remote_dispatch_request(/* ... */) {
    Ok(req) => Ok(req),
    Err(e) if e.to_string().contains("not found after insert") => {
        tracing::error!("dispatch insert/read-back mismatch; check triggers and shared connections");
        Err(e)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: A trigger or INSTEAD OF rule on the dispatch-requests table swallows the insert; last_insert_rowid returns an id from a different table because a concurrent insert on the same shared connection landed between the INSERT and the read of last_insert_rowid; the table was converted to a view by a migration.

Common situations: Cross-thread use of a single rusqlite Connection without the actor mutex; a custom migration added an AFTER INSERT trigger that moves rows; schema drift after a partial upgrade.

Related errors


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