Skip to content

id(currency_poc/tb_ledger): status mappers panic on unmodelled-but-ordinary TigerBeetle statuses (zero/int-max IDs, closed accounts) the in-memory backend accepts #6108

Open

Description

@dezren39

Summary

pkgs/id/src/currency_poc/tb_ledger.rs — the LedgerBackend over the real TigerBeetle client — maps TigerBeetle result statuses to the PoC's typed errors through two functions with a panic catch-all:

  • map_account_status (tb_ledger.rs:186–199): other => panic!("tigerbeetle create_accounts returned unmodelled status {other:?}")
  • map_transfer_status (tb_ledger.rs:202–242): other => panic!("tigerbeetle create_transfers returned unmodelled status {other:?}")

The module docs (tb_ledger.rs:25–29) call this deliberate — "The scenarios exercise only the mapped subset" — but several unmodelled statuses are not exotic: they are the ordinary per-event validation results for input shapes the PoC types themselves accept, and for state a real cluster can be in. When one arrives, the panic unwinds out of create_accounts/create_transfers (tb_ledger.rs:277/322) and crashes the calling executor/gateway process instead of returning the per-leg Result the LedgerBackend trait promises.

Evidence (verified this run, laptop primary @ c7a0982, HEAD == origin/main)

  • The pinned client is tigerbeetle = "=0.17.9" (pkgs/id/Cargo.toml:116). In that crate, CreateTransferStatus has ~70 variants and CreateAccountStatus ~27; the mappers model roughly a third of the transfer set and 9 account statuses.
  • Unmodelled transfer statuses reachable from ordinary inputs/state: IdMustNotBeZero (5), IdMustNotBeIntMax (6), DebitAccountIdMustNotBeZero (8), CreditAccountIdMustNotBeZero (10), DebitAccountIdMustNotBeIntMax (9), CreditAccountIdMustNotBeIntMax (11), FlagsAreMutuallyExclusive (7), DebitAccountAlreadyClosed (65), CreditAccountAlreadyClosed (66). (Also unmodelled, if less reachable today: CodeMustNotBeZero (20) — codes pass through wire_code's +1 shift — OverflowsTimeout (53), and the pending-transfer and imported-event families.)
  • Unmodelled account statuses reachable the same way: IdMustNotBeZero (6), IdMustNotBeIntMax (7), CodeMustNotBeZero (14), FlagsAreMutuallyExclusive (8), DebitsPendingMustBeZero / DebitsPostedMustBeZero / CreditsPendingMustBeZero / CreditsPostedMustBeZero (9–12), and LinkedEventFailed (1) / LinkedEventChainOpen (2) for linked account batches — both of which ARE modelled on the transfer side.
  • Backend divergence (probe, below): the in-memory reference backend accepts these shapes — an account with id = 0 is created Ok, a transfer with id = 0 posts Ok, and a transfer debiting account 0 posts Ok once account 0 exists. So a scenario exercised only against InMemoryLedger (the default in super::tests) passes, while the same batch against a real cluster returns e.g. IdMustNotBeZero and panics the process.
  • Probe (laptop, ~/git/fleet/probe-tb-unmodelled, real id crate + pinned tigerbeetle 0.17.9 crate; the two mappers copied verbatim from tb_ledger.rs:186–243 since they are private):
    • P1 create_accounts incl id=0: [Ok(()), Ok(()), Ok(())] — in-memory backend creates account 0.
    • P1 create_transfers transfer id=0: [Ok(())] and P1 create_transfers debit_account_id=0 (account 0 exists): [Ok(())] — the same shapes post fine in memory.
    • P2 transfer IdMustNotBeZero / DebitAccountIdMustNotBeZero / CreditAccountIdMustNotBeZero / IdMustNotBeIntMax / CodeMustNotBeZero / DebitAccountAlreadyClosed / OverflowsTimeout: PANIC (unmodelled) — all seven.
    • P2 account IdMustNotBeZero / CodeMustNotBeZero / LinkedEventFailed / IdMustNotBeIntMax: PANIC (unmodelled) — all four.
    • Control: P2 control LedgerMustNotBeZero transfer: Err(LedgerMismatch) — modelled statuses map as designed.
  • No existing filing: gh searches for map_transfer_status, tb_ledger, IdMustNotBeZero, and "tigerbeetle panic unmodelled status" (all states) return no covering issue. Adjacent but distinct: id(conversion): build_conversion_batch accepts ledger 0 — builds a batch that can never post, with no typed pre-ledger rejection #6102 is about build_conversion_batch accepting ledger 0 at the pure layer; this filing is the TB binding's status mapping, which panics for any caller of the trait, conversion or not.

Why it matters

The LedgerBackend contract is per-leg Results: the executor and the recovery machinery treat a rejected leg as data to classify and persist, not as a process-fatal event. A single malformed or unlucky leg — a zero/int-max ID from a bad ID generator, a transfer against an account that has since been closed, a timeout overflow on a pending transfer — currently takes down the whole process on the real backend while the in-memory backend (what CI exercises) sails through. That is exactly the class of divergence a two-backend design exists to prevent, and it hides until the TB harness or a real deployment hits it.

Acceptance criteria

  • Every CreateAccountStatus / CreateTransferStatus variant of the pinned tigerbeetle crate that is reachable from inputs the PoC types accept (zero/int-max account and transfer IDs, closed accounts, mutually exclusive flags, non-zero initial balances, linked-batch failures) maps to a typed AccountError / TransferError — new variants added where no existing one fits — instead of reaching the panic arm.
  • A unit test exercises both mappers over every variant of the pinned crate's two status enums and asserts none of them panics (an exhaustiveness net that fails loudly on the next crate bump instead of in production).
  • Any status intentionally left panic-worthy (e.g. imported-event statuses the binding can never emit) is enumerated explicitly in the match with a comment saying why it is unreachable, rather than falling into a bare catch-all.
  • The in-memory backend's behaviour for the zero-ID shapes is reconciled or explicitly documented as divergent, so scenario tests mean the same thing on both backends.
  • cargo test -p id --lib currency_poc passes (with the currency-poc-tb feature where the harness requires it).

Scope notes

  • Filed-only from the creator lane; not self-solved — the new error-variant shapes are the currency executor author's call, and the fix touches the recovery classification surface.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    on Nov 5, 2026
  2. developing-today commented on Nov 5, 2026

    @developing-today
    Contributor
  3. added
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Nov 5, 2026
  4. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

    • Agent: new-issue-creator (new-issue-creator-code-b)
    • Session / ID: cron job new-issue-creator-code-b, run 2026-11-05 00:50 CST
    • Found via: filed this run
    • Task: Filed this issue — tb_ledger.rs's TigerBeetle status mappers (map_account_status:186, map_transfer_status:202) panic on unmodelled statuses, several of which are ordinary validation results (zero/int-max account & transfer IDs, closed accounts, linked-batch failures on accounts). Verified this run on the laptop @ c7a0982: probe ~/git/fleet/probe-tb-unmodelled (real id crate + pinned tigerbeetle 0.17.9, mappers copied verbatim) — in-memory backend accepts account id=0 / transfer id=0 / debit-account-0 (all Ok), while the verbatim mappers PANIC on all 7 transfer + 4 account unmodelled statuses tried; modelled control (LedgerMustNotBeZero) maps to Err(LedgerMismatch) as designed.
    • Expectation: effort s–m — map the reachable statuses to typed AccountError/TransferError variants and add an exhaustiveness test over every variant of the pinned crate's two status enums; done when no reachable status can reach the panic arm and currency_poc tests pass.
    • Links: packet hidden_files/packet-6108-triage.json (queue repo-health-code-pilot); body hidden_files/issue-id-tb-unmodelled-status-panic.md
  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  6. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. 144 remaining items

  8. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  9. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 6, 2026
  10. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  11. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 6, 2026
  12. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)kind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/parkedQueue task is parked (waiting on CI, time, or an event)source/creatorFiled by the new-issue-creator jobstatus/parkedDeliberately paused with a recorded resume conditiontask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions