nautechsystems/nautilus_trader · error

Active terminal intent conflicts with the canonical nonce le

Error message

Active terminal intent conflicts with the canonical nonce ledger

What it means

This invariant fires during migration when an intent is simultaneously marked `active` (still occupying its nonce in the signer's ledger) and carries a terminal status (`finalized` or `reverted`), while its nonce equals `next_canonical_nonce` — the nonce the ledger is about to hand out. A finalized/reverted transaction has been mined and its nonce consumed, so it cannot still be the active occupant of the next canonical nonce. The library throws to prevent the nonce ledger from double-tracking a consumed nonce.

Source

Thrown at crates/adapters/blockchain/src/execution/client.rs:5467

                });
                continue;
            }

            let nonce = intent.nonce.ok_or_else(|| {
                anyhow::anyhow!("Retained signed intent {} has no nonce", intent.id)
            })?;
            let current = current.ok_or_else(|| {
                anyhow::anyhow!("Retained signed intent {} has no current hash", intent.id)
            })?;
            let raw_transaction = authenticated.get(&current.id).ok_or_else(|| {
                anyhow::anyhow!(
                    "Retained signed intent {} has no authenticated current payload",
                    intent.id
                )
            })?;

            if intent.active && nonce == next_canonical_nonce {
                anyhow::ensure!(
                    !matches!(intent.status.as_str(), "finalized" | "reverted"),
                    "Active terminal intent conflicts with the canonical nonce ledger"
                );
                records.push(ExecutionVerificationMigrationRecord {
                    intent_id: intent.id,
                    nonce: Some(nonce),
                    transaction_hash: Some(current.transaction_hash.clone()),
                    terminal_status: None,
                    block_number: None,
                    block_hash: None,
                    receipt_success: None,
                    gas_used: None,
                    effective_gas_price: None,
                    recover_prepared: false,
                    decisions: vec![base_decision],
                });
                continue;
            }

View on GitHub (pinned to 18893faf8b)

Solutions

  1. For the reported intent id, reconcile its `active` flag with its status: a finalized/reverted intent must have active=false in the state store, then restart migration.
  2. Restore from the last consistent state snapshot taken before the interrupted terminal update.
  3. Verify the transaction's actual on-chain state via its receipt and set the intent record to match reality (terminal, inactive) before re-running.
  4. Make terminal status updates write `status` and `active` atomically in a single transaction to prevent recurrence.

Example fix

// before (two separate writes; crash between them)
store.set_status(intent_id, "finalized");
store.set_active(intent_id, false);  // never ran after crash

// after (atomic update)
store.update(intent_id, |i| { i.status = "finalized"; i.active = false; })?;
Defensive patterns

Strategy: validation

Validate before calling

fn validate_intent_consistency(intent: &Intent) -> Result<(), String> {
    let terminal = matches!(intent.status.as_str(), "finalized" | "reverted");
    if terminal && intent.active {
        return Err(format!("intent {} terminal but still active", intent.id));
    }
    Ok(())
}

Type guard

fn is_consistent(intent: &Intent) -> bool {
    !matches!(intent.status.as_str(), "finalized" | "reverted") || !intent.active
}

Prevention

When it happens

Trigger: Raised by `anyhow::ensure!(!matches!(intent.status.as_str(), "finalized" | "reverted"), ...)` when a retained intent's status persisted as finalized/reverted but its `active` flag was not cleared, and its nonce matches the next canonical nonce — e.g. a crash between marking the intent terminal and deactivating it, or inconsistent updates that wrote status without flipping active.

Common situations: Node crash mid-terminal-update; a state-store write that is not atomic across the status and active fields; manual state edits that set status without clearing active; replaying old WAL entries in the wrong order after a restore.

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/5b9e862d9f806d69. Report an issue: GitHub.