> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.prividium.com/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

The stream never skips. An instruction that cannot finish keeps its sequence, and every later instruction waits behind
it. There is no API route for recovery yet: an operator with access to the platform database resolves it with the
procedures below, and the service picks the change up on its next poll (`EXECUTION_POLL_INTERVAL_MS`).

## When an operator is needed

Stages that name an unavailable dependency retry on every poll and heal on their own once it is back:
`AUTHORIZATION_UNAVAILABLE` (`error` reads `POLICY_HTTP_<status>`, `POLICY_DEPENDENCY_UNAVAILABLE`, or
`POLICY_RESPONSE_INVALID` when the policy API answers in a shape the service does not read), `RPC_UNAVAILABLE`,
`SIGNING_UNCERTAIN`, `WALLET_NOT_CONFIGURED` (the contract maps the account's wallet to no address yet), and `UNCERTAIN`
while the broadcast transaction can still be mined.

A decision or approval that ages past the policy admit window before it is used is not a failure either: the run asks
for a new one, which may mean a new approval, and the record reads `POLICY_AUTHORIZATION_EXPIRED` until it arrives. The
same holds for a broadcast transaction the chain has not mined when its decision ages out, for example after an RPC or
sequencer outage: the run gets a fresh decision for the same call and resends the same signed transaction. A ruleset
publish is followed too: the run continues under the decision policy records for the new ruleset, and a pending
approval raised under the old one is invalidated and replaced by the new ruleset's.

These need an operator:

| Stage                     | What happened                                                                                                                           | Then                                                   |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `CONTRACT_REVERT`         | The simulated call reverted: missing role, insufficient balance, limit, paused contract                                                 | Fix the on-chain condition, then retry                 |
| `ONCHAIN_REVERT`          | The broadcast transaction reverted                                                                                                      | Same                                                   |
| `RECEIPT_MISMATCH`        | The transaction was mined, but its receipt is not the authorized operation: another sender, contract or event                           | Reconcile on chain first, then close                   |
| `NONCE_CONSUMED`          | Another transaction used the signer's nonce and this one has no receipt, so it can never land                                           | Find what used the nonce, then retry or close          |
| `AUTHORIZATION_DENIED`    | Contract, signer, wallet or arguments changed after authorization, or the signer lost a role                                            | Restore the deployment and retry, or close             |
| `POLICY_DENIED`           | Policy denied the re-check after signing (a denial before signing closes on its own, also after a retry)                                | Retry once the cause is resolved, or close             |
| `INVALID_INSTRUCTION`     | The accepted instruction no longer passes validation or cannot be encoded for the contract                                              | Close; resubmit the event under a new `source.eventId` |
| `EXECUTION_STATE_INVALID` | The stored intent, decision or signing attempt no longer parses, for example after an upgrade                                           | Retry; it rebuilds all three                           |
| `SIGNING_REJECTED`        | Custody refused or abandoned the signing request (`REJECTED`, `CANCELLED`, `BLOCKED`, `FAILED`, `TIMEOUT`)                              | Resolve it on the custody side, then retry             |
| `SIGNING_INVALID`         | The attempt failed an integrity check: custody signed with a key other than the signer address, or the signer changed since preparation | Fix the signer configuration, then retry               |

## What may change

The accepted instruction is immutable. The trigger `asset_instructions_immutable` rejects any change to `source_system`,
`source_event_id`, `sequence`, `kind`, `account`, `operation_id`, `accepted_payload` and `actor_context`, so a wrong
amount or account is never fixed in place: close the instruction and resubmit the corrected event under a new
`source.eventId` at the next free sequence. `status`, `stage`, `failure_reason`, `execution_intent`, `policy_checkpoint`
and `transaction_hash` may change.

On `asset_instruction_attempts`, the trigger `asset_instruction_attempts_immutable` fixes the attempt's identity and
`prepared_transaction`, and fixes `signed_transaction`, `transaction_hash` and `custody_reference` once written. Only
`state` may change.

Never change a `COMPLETED` instruction or a `CONFIRMED` attempt. Every row change is captured by the platform's
`capture_db_mutation_audit` trigger in `db_mutation_audit_logs`; set `app.job_id` in the same transaction so the change
is attributed.

Every procedure below first takes the stream's advisory lock, the one the executor holds for each pass. The transaction
waits for a pass in progress to finish — a signing request can hold it for up to `FIREBLOCKS_SIGNING_TIMEOUT_MS` — and
the executor skips its passes until the transaction commits, so it cannot sign or broadcast over an operator's change.

## Hold execution

A hold stops execution while accepting goes on: sources keep submitting and sequences are still checked, but the
executor starts no instruction — no binding, policy request, signing or broadcast. A transaction already broadcast is
still confirmed once it is mined; it is neither resent nor re-authorized. `GET /api/asset-instructions/overview` shows
the hold as `hold { since, reason }`, and the service logs a warning when it first sees it.

Make the hold step one of any recovery that pauses or upgrades the contract: hold, pause, do the work, unpause, release.
No instruction reaches the paused contract meanwhile, so none fails with `CONTRACT_REVERT`. If the contract was paused
without a hold and the instruction at the cursor has already failed, hold, unpause, [retry](#retry) it, then release.

```sql
BEGIN;
SELECT pg_advisory_xact_lock(hashtextextended('asset-instructions:execution', 0));
SELECT set_config('app.job_id', 'asset-instructions:operator:<ticket>', true);
INSERT INTO asset_instruction_execution_hold (reason) VALUES ('<ticket>: <why>');
COMMIT;
```

Once this commits, no pass is signing or broadcasting. The row takes `held_by` from `app.job_id` and refuses a hold
without it; while one hold is in place a second `INSERT` fails. Release it, and execution resumes on the next poll:

```sql
BEGIN;
SELECT pg_advisory_xact_lock(hashtextextended('asset-instructions:execution', 0));
SELECT set_config('app.job_id', 'asset-instructions:operator:<ticket>', true);
DELETE FROM asset_instruction_execution_hold;
COMMIT;
```

## Settle the signing attempt first

```sql
SELECT state, transaction_hash, prepared_transaction->>'nonce' AS nonce, prepared_transaction->>'signerAddress' AS signer
  FROM asset_instruction_attempts
 WHERE instruction_id = (SELECT id FROM asset_instructions WHERE operation_id = '<operationId>')
 ORDER BY attempt_number DESC LIMIT 1;
```

If the latest attempt is `SUBMITTED` or `UNCERTAIN`, its transaction may still be mined. Check
`eth_getTransactionReceipt(transaction_hash)`: a receipt means the operation happened, so leave the row alone and the
service confirms it on its next poll. Only when there is no receipt and `eth_getTransactionCount(signer)` is above the
attempt's nonce is the transaction dead. A dead or never-broadcast attempt (`PREPARED`, `SIGNING`, `SIGNED`) is marked
`REVERTED`, the one state from which the executor opens a new attempt; both procedures below include that step.

## Retry

The instruction starts over with a new authorization (a new approval where the policy requires one), a new signing
attempt and a new transaction. The previous intent, decision and attempt remain in `db_mutation_audit_logs` and in the
reverted attempt row. If the simulation then reports the operation as already recorded, an earlier attempt landed after
all: the run finds its receipt among the instruction's attempts and completes from it. Only an operation recorded by a
transaction this instruction never sent stays `CONTRACT_REVERT`.

```sql
BEGIN;
SELECT pg_advisory_xact_lock(hashtextextended('asset-instructions:execution', 0));
SELECT set_config('app.job_id', 'asset-instructions:operator:<ticket>', true);
UPDATE asset_instruction_attempts SET state = 'REVERTED'
 WHERE instruction_id = (SELECT id FROM asset_instructions WHERE operation_id = '<operationId>')
   AND state NOT IN ('CONFIRMED', 'REVERTED');
UPDATE asset_instructions
   SET status = 'RECEIVED', stage = 'AWAITING_POLICY', failure_reason = NULL,
       execution_intent = NULL, policy_checkpoint = NULL, transaction_hash = NULL
 WHERE operation_id = '<operationId>' AND status <> 'COMPLETED';
COMMIT;
```

## Close

Closing releases the sequence without executing the instruction. `OPERATOR_CLOSED` counts as closed, so the cursor
moves on at the next poll. The source event keeps its record, and resubmitting the same `source.eventId` returns it; a
corrected event is submitted under a new `source.eventId` at the next free sequence.

Only the instruction at the cursor may be closed: the cursor is the highest closed sequence plus one, so closing a later
one would skip every instruction in between. The last `UPDATE` matches only the cursor row and must report `UPDATE 1`;
on `UPDATE 0` the instruction is not at the cursor, so `ROLLBACK` instead of committing.

```sql
BEGIN;
SELECT pg_advisory_xact_lock(hashtextextended('asset-instructions:execution', 0));
SELECT set_config('app.job_id', 'asset-instructions:operator:<ticket>', true);
UPDATE asset_instruction_attempts SET state = 'REVERTED'
 WHERE instruction_id = (SELECT id FROM asset_instructions WHERE operation_id = '<operationId>')
   AND state NOT IN ('CONFIRMED', 'REVERTED');
UPDATE asset_instructions
   SET status = 'FAILED', stage = 'OPERATOR_CLOSED', failure_reason = '<ticket>: <why>'
 WHERE operation_id = '<operationId>' AND status <> 'COMPLETED'
   AND sequence = (SELECT COALESCE(MAX(sequence), 0) + 1 FROM asset_instructions
                    WHERE status = 'COMPLETED' OR stage IN ('POLICY_REJECTED', 'OPERATOR_CLOSED'));
-- Expect UPDATE 1; otherwise ROLLBACK.
COMMIT;
```
