Skip to content

fix(gateway): settlement_failed session status + safe retry/cancel for stuck commits - #245

Merged
LamaSu merged 2 commits into
masterfrom
fix/settlement-failed-status
Jul 11, 2026
Merged

fix(gateway): settlement_failed session status + safe retry/cancel for stuck commits#245
LamaSu merged 2 commits into
masterfrom
fix/settlement-failed-status

Conversation

@LamaSu

@LamaSu LamaSu commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Problem

#242 (merged) made POST /api/negotiate/session/:id/commit fail loud with 502 { error: "settlement_failed" } when REAL-settlement wiring fails to mint an on-chain escrow — closing the false-200 hole. But it deliberately left the session status="committed" so the shared liveness gate would 409-block a /commit retry (its only guard against a second createJobFromSession deploying 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_failed SessionStatus and a guarded recovery path that never risks a duplicate on-chain escrow.

  • packages/spec/src/types/negotiation.ts — add settlement_failed to SessionStatus. The DB status column is free TEXT (no CHECK/enum) → no migration. The A2A status switch has a default, so the union addition is backward-compatible.
  • packages/gateway/src/routes/session-liveness.tsassertSessionLive() now returns 409 on settlement_failed too. 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 a settlement_failed session, so a naive /commit retry can never blind-mint. Recovery is out-of-band only.
  • packages/gateway/src/routes/negotiation.ts
    • On a real-settlement commit failure, transition committed → settlement_failed (instead of silently leaving it committed), stamping a machine-readable failure class into the transition:
      • settlement_failed:preflight_no_escrow — gateway key unset, so createJobFromSession threw 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.
      • The 502 body is unchanged (error/message/sessionId/jobId).
    • New POST /api/negotiate/session/:id/retry-settlement — money-path order of operations, fail-closed at every ambiguity:
      1. Escrow already recorded (session escrowAddress, or escrows.findByCwm(cwmId), real 0x… not mock-…) → RESUME from it, transition to committed, never re-mint.
      2. No recorded escrow + REAL mode + not provably pre-flight → 409 settlement_unrecoverable_ambiguous (a duplicate escrow outranks a stuck session).
      3. Safe (pre-flight real failure, or mock mode) → re-run the wiring; a repeat failure re-stamps settlement_failed and never double-mints.
    • DELETE already cancels any non-committed session, so it cleanly closes a settlement_failed one (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 withSignerLock before 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.ts are excluded):

  • real commit failure → session settlement_failed, 502 body unchanged;
  • naive /commit on settlement_failed → 409;
  • retry resumes from an escrow recorded by cwmId / on the session row → no second escrow;
  • retry fails closed on ambiguous failure with no recorded escrow;
  • retry re-mints exactly once when provably pre-flight (config fixed);
  • a still-broken real retry re-fails safely, never mints;
  • DELETE cancels a settlement_failed session 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 asserts settlement_failed, and a naive /commit retry 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

settlement-failed-status and others added 2 commits July 11, 2026 12:57
…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>
@LamaSu
LamaSu merged commit 8ed80da into master Jul 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant