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
- Query the current status first (SELECT status FROM execution_intent WHERE id = $1) and only call the recoverable API for rows in 'prepared'.
- Treat the error as benign in idempotent recovery loops: catch it, log the current status, and skip — the intent already advanced.
- If the intent is stuck in an unexpected status, use the transition audit table (execution_transaction_transition) to determine the actual progress before acting.
- 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
- Filter recovery candidates to status = 'prepared' before calling the API.
- Make recovery loops idempotent: skip, don't fail, on already-advanced intents.
- Serialize concurrent recovery workers to avoid double-transition races.
- Consult execution_transaction_transition to confirm intent progress before acting.
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
- Failed to lock execution verification state: {e}
- Canonical nonce ledger changed during verification bootstrap
- Execution payload {} changed during migration
- Execution payload storage is not rewrapping
- Execution payload storage is not rolling back
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/9d88a466ecf4c97e.
Report an issue: GitHub.