Two schema changes the fixes in the following commits build on (BUGS.md B-09, B-04): ix_rounds_single_active is a unique index over the constant expression (1), restricted to the active statuses, so the table holds any number of closed rounds and only ever one live one. Rounds never overlapping was previously enforced only by a read-then-insert in open_new_round_if_needed, which two concurrent callers can both pass — and a second stuck "open" row blocks every future round forever, since get_active_round matches on status. The migration doesn't create that index blind: an instance that already has two active rounds (the very bug) would fail mid-migration with an opaque IntegrityError, so it first closes the stale duplicates and keeps the newest — which is what get_active_round was already doing silently. Verified against a DB seeded with an 'open' plus a 'closing' round. pending_transactions.failure_reason is for the reconciler added next: when it gives up on a transaction, an operator needs to see whether it was dropped or rejected. The PendingTransaction docstring now also documents the full status lifecycle (building -> pending -> confirmed | failed), since "building" is new and load-bearing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Generic single-database configuration with an async dbapi.