Skip to content

Record each order's settlement once and deliver it after a failed credit - #373

Merged
ctfbruce merged 7 commits into
mainfrom
fix/credit-settlement-ledger
Oct 5, 2026
Merged

ctfbruce merged 7 commits into
mainfrom
fix/credit-settlement-ledger

Conversation

@ctfbruce

@ctfbruce ctfbruce commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Behaviour

An order's settlement is now decided in one place, recorded once, durably, and delivered reliably from the run's terminal result:

  • Dispatcher schema 21 adds order_settlements: one immutable row per settled order (credit or refund, amount, currency, executor and run), written in the same database transaction as the order's conditional state change and the executor's earnings credit or the chain refund. A repeated or concurrent delivery of the same decision leaves a settled order as it is.
  • The decision stays what it was (exit code 0 credits the executor, any other exit refunds the order) and is taken in one function used by the terminal report, the cancellation path and a new bounded pass of the existing maintenance loop, which settles terminal runs with a recorded exit code whose inline settlement failed. A terminal run without a recorded exit code is left pending; nothing infers an outcome.
  • A TEST order of a failed run becomes refunded locally, moving nothing; TEST intents are still never refunded as a whole, so a refused batch keeps its intent spendable.
  • Every intent records the pricing rule it was priced under (transactions.pricing_rule, bw-s-ceil-ms-v1); rows written before the column read as the rule of the writing release. The API guide states that a USDC intent is a local record created without any chain call.

Proof

  • Base reproduction: with a trigger making the inline credit fail, a terminal run's order stayed Outstanding after the trigger was dropped and the dispatcher restarted; with this change the next maintenance pass credits it once.
  • Real-SQLite tests: credit once with one settlement row; failed TEST run refunded locally and a later success report changes nothing; a refused settlement row rolls back the state change and the earnings for both kinds; settlement rows cannot be updated or deleted; eight concurrent completions on one handle and two handles on one file record one row; a credit racing a refund has exactly one winner; the sweep delivers after an inline failure across a restart, once, honours its limit, skips non-terminal and exit-code-less runs and a closed dispatcher; stored intents carry the rule name after a reopen.
  • The whole internal/dispatcher tree and cmd/dispatcher pass under -race; generated bindings match the pinned generator.

Upgrade

Dispatcher schema 21 is required: stop the dispatcher, back up the database and run the explicit upgrade (-upgrade-database, or deploy/ansible/upgrade-database.yml) before starting this build.

Requires #364 (dispatcher schema 20) first; this branch is based on its head.

Limits

  • Chain-currency refunds still perform the chain transfer inside the local transaction, so the maintenance pass retries only wholly local settlements (credits of any currency and TEST refunds) and defers chain refunds without touching the backend; a chain refund whose inline attempt failed stays outstanding until the durable transfer record of the following change takes it over. The pass continues after the last run it attempted and starts over once the listing is exhausted, so a failing order never blocks later ones.
  • A cancellation whose inline refund failed is not retried by the pass, because a cancellation records no executor exit code and nothing infers one.
  • No live chain path is exercised; chain payments remain disabled in every shipped configuration.

GitHub issues #76, #77 and #78.

Candidate CI

Run 37288440585 on 83de859: 16 of 16 lanes and required succeeded (an earlier head passed run 37284687485).

TheodorAdrienIsaak Mattli added 5 commits October 2, 2026 15:17
Compose the account recovery and provider configuration change with the
CILogon production deployment that landed on main. The merged deployment keeps
main's variable names, its production enablement, its credential preflight that
counts both entries without reading them, and its single example file, and adds
the browser-approved CLI authentication settings, the issuer and URL checks,
the configuration-update refresh of the unit and credentials, and the extended
render fixture. The duplicate example file is dropped; the installed credential
file name is unchanged.

Go, SQL and documentation changes are those already on the branch; the render
fixture passes inside the pinned provisioner against a retained package.

GitHub issues #226, #223 and #227.
The payment decision of a terminal run was applied by an order-state write
without a previous-state check and nothing recorded which run settled
which order; when the write after the terminal report failed, the order
stayed outstanding for good. Dispatcher schema 21 adds order_settlements,
one immutable row per settled order (credit or refund, amount, currency,
executor and run), and the pricing_rule column of transactions. The
decision now lives in one place: the order moves from outstanding only
through a conditional write, the settlement row and the earnings credit
or chain refund commit with it, and a repeated or concurrent delivery
leaves a settled order as it is. The maintenance loop settles terminal
runs with a recorded exit code whose inline settlement failed, a bounded
number per pass. A failed TEST run is refunded locally, moving nothing;
TEST intents are still never refunded as a whole. Every intent records
the name of the rule that priced it, and the API guide states that a USDC
intent is a local record created without a chain call.

GitHub issues #76, #77 and #78.
The executor's rejected-allocation test still asserted that the failed
run's TEST order stays outstanding, the limitation the settlement change
removes: a failed TEST run now refunds its order locally and no credit is
invented. The assertion follows the new behaviour.

GitHub issue #78.
@ctfbruce
ctfbruce marked this pull request as ready for review October 5, 2026 08:46
@ctfbruce ctfbruce added the Review Implementation is ready for review and required validation. label Oct 5, 2026
@ctfbruce
ctfbruce marked this pull request as draft October 5, 2026 09:00
The maintenance pass retried every outstanding order of a terminal run,
including USDC refunds, which the terminal path sends inside the local
transaction: a transfer whose response was lost or whose commit failed
left the order outstanding and the next pass paid the buyer again. The
pass also always listed the oldest rows, so a page of orders that keep
failing kept every later order from being attempted. The pass now settles
only wholly local outcomes, credits of any currency and TEST refunds, and
defers a chain refund without touching the backend; chain refunds are
attempted once from the terminal path until a durable transfer record
takes them over. The listing continues after the last run the previous
pass attempted and starts over once it is exhausted, so a failing order
never blocks the ones after it.

GitHub issue #78.
@ctfbruce
ctfbruce marked this pull request as ready for review October 5, 2026 09:21
@ctfbruce
ctfbruce merged commit ed9c5eb into main Oct 5, 2026
16 checks passed
@vincent10400094
vincent10400094 deleted the fix/credit-settlement-ledger branch October 8, 2026 00:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review Implementation is ready for review and required validation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant