nautechsystems/nautilus_trader · error

Execution intent {intent_id} is {current_status}, not recove

Error message

Execution intent {intent_id} is {current_status}, not recoverable before signing

What it means

After locking the execution_intent row, the code runs anyhow::ensure!(current_status == "prepared", ...) before transitioning it to 'recoverable'. Recovery is only legal from the 'prepared' state (before signing); any other status — signed, broadcast, confirmed, replaced, already recoverable — trips this error. It prevents resurrecting or mutating intents whose execution already advanced.

Source

Thrown at crates/adapters/blockchain/src/cache/database.rs:6522

    /// Releases an intent when no broadcast attempt can have occurred.
    ///
    /// # Errors
    ///
    /// Returns an error if the intent advanced to broadcast or persistence fails.
    pub async fn mark_execution_intent_recoverable(&self, intent_id: i64) -> anyhow::Result<()> {
        let mut transaction = self.pool.begin().await.map_err(|e| {
            anyhow::anyhow!("Failed to start recoverable execution transition: {e}")
        })?;
        let current_status = sqlx::query_scalar::<_, String>(
            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
        )
        .bind(intent_id)
        .fetch_optional(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to lock recoverable execution intent: {e}"))?
        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
        anyhow::ensure!(
            current_status == "prepared",
            "Execution intent {intent_id} is {current_status}, not recoverable before signing"
        );
        let result = sqlx::query(
            "
            UPDATE execution_intent
            SET status = 'recoverable', active = FALSE, updated_at = NOW()
            WHERE id = $1 AND status = 'prepared'
            ",
        )
        .bind(intent_id)
        .execute(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to mark execution intent recoverable: {e}"))?;
        anyhow::ensure!(
            result.rows_affected() == 1,
            "Execution intent {intent_id} is not recoverable from preparation"
        );

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Query the current status first (SELECT status FROM execution_intent WHERE id = $1) and only call the recoverable API for rows in 'prepared'.
  2. Treat the error as benign in idempotent recovery loops: catch it, log the current status, and skip — the intent already advanced.
  3. If the intent is stuck in an unexpected status, use the transition audit table (execution_transaction_transition) to determine the actual progress before acting.
  4. Ensure concurrent workers serialize recovery work (e.g. distributed lock or claim column) so only one caller attempts the transition.

Example fix

// before
db.mark_execution_intent_recoverable(intent_id).await?; // panics-safe but errors on signed intents
// after
let status: String = sqlx::query_scalar("SELECT status FROM execution_intent WHERE id = $1")
    .bind(intent_id)
    .fetch_one(&db.pool)
    .await?;
if status == "prepared" {
    db.mark_execution_intent_recoverable(intent_id).await?;
} else {
    tracing::info!(%intent_id, %status, "intent already advanced; skipping recovery");
}
Defensive patterns

Strategy: validation

Validate before calling

async fn can_recover(pool: &sqlx::PgPool, intent_id: i64) -> anyhow::Result<bool> {
    let status: Option<String> = sqlx::query_scalar("SELECT status FROM execution_intent WHERE id = $1")
        .bind(intent_id)
        .fetch_optional(pool)
        .await?;
    Ok(status.as_deref() == Some("prepared"))
}

Try / catch

// Treat non-'prepared' statuses as expected in idempotent recovery
if let Err(e) = db.mark_execution_intent_recoverable(id).await {
    let msg = e.to_string();
    if msg.contains("not recoverable before signing") {
        tracing::info!(%id, "intent already advanced; skipping");
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: Calling mark_execution_intent_recoverable(intent_id) when the row's status column is anything other than 'prepared', e.g. the intent was already signed or broadcast, or was already marked recoverable.

Common situations: Two recovery workers race and one wins (second sees 'recoverable'); a retry fires after the intent already progressed to broadcast; an operator re-runs a recovery script against completed intents; a caller misidentifies which intents are still pending signing.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/9d88a466ecf4c97e. Report an issue: GitHub.