selo CLI never guesses. It records the attempt, refuses to pay again for that release, and lets you fetch the result with --resume, which reuses the same preflight and Idempotency-Key so Selo never settles a second time.
The run state file
Before it sends the paid request,selo test writes the attempt to .selo/runs/<release>.json, relative to the directory you ran pnpm selo in. It writes the file again with the outcome. The directory is created with mode 700 and the file with mode 600, and each write replaces the file atomically.
.selo/runs/<release>.json
Phases
While the phase is
paying or unresolved, every new selo test for that release stops before the preflight, exits 2 and prints how to recover:
Output
2: the run state … is unreadable; inspect it before paying again.
What leaves a run unresolved
When Selo holds an inbound settlement it could not confirm, it never runs the job and it tells the client not to pay again. A retry of the same request is answered from the held job, without settling again.
Resume a run
1
Look at the payment on chain
Open
https://allo.info/tx/<expectedInboundTxId>, or check the payer account on any Algorand explorer. If the transaction is there, Selo was paid.2
Rerun the same command with --resume
Use the same
--api, --url, --method, --network and payer wallet as the run you resume. Pass every other flag as before.Terminal
3
Read the result
When the job has a report, the output is the same as a normal run: the checks, both transaction ids, the gate and the exit code. The run state moves to
concluded.Why a resume cannot charge you twice
A resume does not run a new preflight. It readspreflightId and idempotencyKey from the run state and sends the paid request again with the same key. Selo’s 402 still requires a payment on that request, so the CLI signs one, but Selo uses the key only to find the job:
- Selo verifies the payment with the facilitator. Verifying does not move funds.
- Selo claims the job by the preflight id and the
Idempotency-Key. The original request already claimed it, so this is a duplicate. - For a duplicate, Selo answers from the existing job and never settles the new payment: the report if it is written,
409 JOB_IN_PROGRESSif it is still running,409 JOB_TERMINALif it ended without a report, or502 INBOUND_SETTLEMENT_UNRESOLVEDif its settlement is held.
--resume would take a new preflight, derive a new key and be a new paid test. That is why the CLI refuses it while an outcome is unconfirmed.
What a resume can answer
Refusals
Limits
Resume within 15 minutes
Resume within 15 minutes
Selo checks the preflight before anything else on the paid route, and a preflight is valid for 15 minutes. After that, the unpaid request of a resume is answered
409 PREFLIGHT_EXPIRED, the CLI cannot fetch the result, and the run stays unresolved. Use the expected transaction id to learn on chain whether Selo was paid.The state is local to one directory and one machine
The state is local to one directory and one machine
The refusal to pay again lives only in
.selo/runs/<release>.json in the directory you ran from. Another directory, another machine, or a CI runner does not see it. In GitHub Actions the runner’s workspace is not kept between runs, so a re-run of the job starts a new paid test; the action warns you not to re-run blindly. See Gate deploys in GitHub Actions.Starting over after an unresolved run
Starting over after an unresolved run
The CLI has no command to clear a run. While the file says
paying or unresolved, it refuses every new paid run for that release id. Once you have checked on chain what happened to the expected transaction, you can test under a new --release id, or move the run state file aside yourself. Either way the next run is a new paid test.Next
Held payments and reconciliation
What Selo does on its side with a payment it could not confirm.
Output files
Every field of the run state, the summary and the attestation.