Execution recovery
Agents and adapters that submit KeeperHub direct-execution writes
(POST /api/execute/transfer, POST /api/execute/contract-call) must recover
safely when the network flakes, a status read races ahead of persistence, or
an on-chain receipt is not successful.
This guide is the published summary of the normative contract in
execution-recovery-v1/contract.md.
It does not describe POST /api/workflows/<id>/webhook. That is a
different surface.
Safe first-write sequence
- Simulate when available and continue only if the call would not revert.
- Broadcast once with a stable
Idempotency-Keythat names the work, not the attempt. - Save
executionId. - Poll
GET /api/execute/{executionId}/status. - Do not infer on-chain success from
status=completedalone. Treatreceipts[].receiptStatusas the receipt evidence: onlysuccessis a successful receipt.revertedandsafe_inner_failureare conclusive failures.not_foundandtimeoutmean the receipt was unreadable; the server settles those rows asunconfirmed, notfailed.
KEEP-966 on the server re-verifies every claimed hash before writing
completed. A reverted receipt is stored verified: false and the row
settles as failed. A client that still sees completed plus a non-success
receipt must fail closed — that combination is a defensive invariant, not an
observed production envelope.
Direct-execution status vocabulary
pending | running | unconfirmed | completed | failed
(app/api/execute/_lib/types.ts. There is no queued status on this endpoint.)
Poll while a row is pending or running. Stop on unconfirmed, completed
or failed. unconfirmed means the transaction was broadcast but no receipt
could be read; the server keeps reconciling that row, so treat it as “stop
waiting and report”, not as a failure, and read the settled status later
against the same execution ID.
Workflow run status uses a different vocabulary (success / error /
cancelled) — do not mix it with direct-execution statuses.
CLI behaviour
kh ex transfer/kh ex ccattachIdempotency-Keyon every write. Override with--idempotency-keyto pin a key across process restarts.- HTTP 5xx retries reuse that key.
- HTTP 409
idempotency_in_progress: retry the same key until--timeout. Do not mint a new key. - HTTP 409
idempotency_conflict: fail. Do not rotate the key (that would broadcast a second transaction). --waitpolls the same execution ID and tolerates a bounded initial HTTP 404 until--timeout(default 5m). Persistent 404 (wrong id, other org) is a timeout error.--watchdoes not treat 404 as pending. A mistyped or foreign-org id exits with an error instead of looping.--waitand--watchstop onunconfirmedand report it.--waitexits zero there, printing the status and transaction hash, because a non-zero exit invites a re-run that would broadcast a second transaction.- Wait paths fail when
status=failed, when a receipt isrevertedorsafe_inner_failure, and whenstatus=completedcarries any non-success receipt. kh ex status --require-verifiedadditionally demands chain proof: it exits non-zero unless the execution completed carrying at least one receipt and every receipt isverified: truewithreceiptStatus: success.unconfirmedfails that gate, and so does a completion with an emptyreceiptsarray, which is treated as success without the flag.
Fixtures
Golden responses live under testdata/execution_recovery_v1/ (version: 1)
and are loaded by go test ./internal/execrecovery/.... Each fixture is
labeled observed, defensive, or classifier.