fix(gateway): settlement_failed session status + safe retry/cancel for stuck commits - #245
Merged
Conversation
…r stuck commits A REAL-settlement commit that fails to mint a usable on-chain escrow (#242's 502 fail-loud path) previously left the session status=committed — stuck: /commit retry 409s, no cancel/retry path. Add a first-class recoverable settlement_failed state and a guarded recovery: - spec: add settlement_failed to SessionStatus (free TEXT col, no migration). - session-liveness: 409 on settlement_failed too, so /commit,/quote,/review and the A2A path still refuse it — preserving #242's double-escrow block. - negotiation /commit: on real-settlement failure transition committed->settlement_failed, stamping a machine-readable class (provably-preflight vs escrow-may-exist). - POST /retry-settlement: never double-mints — resume from a recorded escrow if one exists; else re-mint only when the prior failure was provably pre-flight; else fail closed. DELETE cancels a settlement_failed session cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, not absolute Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
#242 (merged) made
POST /api/negotiate/session/:id/commitfail loud with502 { error: "settlement_failed" }when REAL-settlement wiring fails to mint an on-chain escrow — closing the false-200 hole. But it deliberately left the sessionstatus="committed"so the shared liveness gate would 409-block a/commitretry (its only guard against a secondcreateJobFromSessiondeploying a duplicate on-chain escrow).The side effect: a failed-commit session is stuck — a retry 409s, and there is no cancel/retry path. This PR closes that gap (a fast-follow, not a prod blocker; the baseline loop works without it).
What this does
Adds a first-class, recoverable
settlement_failedSessionStatusand a guarded recovery path that never risks a duplicate on-chain escrow.packages/spec/src/types/negotiation.ts— addsettlement_failedtoSessionStatus. The DBstatuscolumn is freeTEXT(no CHECK/enum) → no migration. The A2A status switch has adefault, so the union addition is backward-compatible.packages/gateway/src/routes/session-liveness.ts—assertSessionLive()now returns 409 onsettlement_failedtoo. This preserves fix(gateway): /commit fails loud (502) when real settlement wiring fails instead of a false 200 (SWALLOW) #242's double-escrow block:/commit,/quote,/review, and the A2A commit path all still refuse asettlement_failedsession, so a naive/commitretry can never blind-mint. Recovery is out-of-band only.packages/gateway/src/routes/negotiation.tscommitted → settlement_failed(instead of silently leaving itcommitted), stamping a machine-readable failure class into the transition:settlement_failed:preflight_no_escrow— gateway key unset, socreateJobFromSessionthrew at its first line before any on-chain call → provably no escrow → safe to re-mint.settlement_failed:onchain_maybe_minted— any other failure → an escrow may have minted before the throw → fail closed.502body is unchanged (error/message/sessionId/jobId).POST /api/negotiate/session/:id/retry-settlement— money-path order of operations, fail-closed at every ambiguity:escrowAddress, orescrows.findByCwm(cwmId), real0x…notmock-…) → RESUME from it, transition tocommitted, never re-mint.settlement_unrecoverable_ambiguous(a duplicate escrow outranks a stuck session).settlement_failedand never double-mints.DELETEalready cancels any non-committedsession, so it cleanly closes asettlement_failedone (clarifying comment added: cancel closes the negotiation record only; no claim to refund/rescind an escrow).Safety rationale (money-path)
The on-chain escrow is minted inside
withSignerLockbefore any DB write, so a throw in that window can leave an on-chain escrow with no DB trace. The retry therefore (a) resumes from any recorded escrow rather than minting, and (b) only blind-mints when the prior failure was provably pre-flight (gateway key unset ⇒ threw before the first chain call). Everything else fails closed.Tests (green on Spark)
New
packages/gateway/src/__tests__/settlement-failed-recovery.test.ts(9 tests) in a non-CI-excluded file (per #322,paid-job-flow.test.ts/settlement.test.tsare excluded):settlement_failed, 502 body unchanged;/commitonsettlement_failed→ 409;cwmId/ on the session row → no second escrow;DELETEcancels asettlement_failedsession cleanly.Escrow-count assertions measure the delta (the seed pre-populates escrows) so "no double-mint" is verified precisely.
Also updated
negotiation-commit-fail-loud.test.ts(#242's suite): the real-failure session now assertssettlement_failed, and a naive/commitretry is still 409-blocked (double-escrow guard preserved).Out of scope (untouched): SEAM-1/SEAM-2 files, the #233 gate, and the CI-suite exclusions. This branch changes exactly 5 files: the two test files +
negotiation.ts+session-liveness.ts+spec/negotiation.ts.🤖 Generated with Claude Code