Skip to content

id(currency_poc): executor submit() and recovery resubmission treat a short create_transfers result vector as success — a backend returning no results marks the conversion Posted with nothing in the ledger #4669

Open

Description

@dezren39

Problem

In pkgs/id/src/currency_poc, neither the executor's submission path nor the recovery scanner checks that LedgerBackend::create_transfers returned one result per leg. A backend that returns a short — or empty — result vector is treated as complete success, and the conversion is persisted as Posted with nothing (or only part) in the ledger.

  • executor.rs:264-282 — submit() iterates record.legs.iter().zip(results.iter()) (line 270). zip stops at the shorter side, so legs with no corresponding result are never examined; the record then advances to IntentState::Posted at line 282 unconditionally.
  • recovery.rs:111-124 — the Resubmit path computes failed by scanning results for a non-Ok/Exists entry. An empty vector has no failing entry, so the record is persisted Posted and reported ReconcileOutcome::Resubmitted.
  • ledger.rs:134 — the LedgerBackend::create_transfers trait contract documents the linked-chain rule a production binding must implement, but never states that the returned vector must carry exactly one result per submitted transfer. The in-repo InMemoryLedger happens to return batch.len() results; nothing enforces that for the future TigerBeetle client binding this seam exists for (id: PoC: conversion executor — linked transfer pairs, write-ahead intent, recovery scanner #2289).

This is the same vacuous-success shape the module already hardened against once: #4133 fixed reconcile marking an empty/duplicate leg vector Posted because matched == legs.len() held vacuously (0 == 0). The result-vector side was left unchecked — a short result vector makes "no leg failed" true vacuously in both callers.

Verified this run (laptop probe, verbatim sources @ 0428638)

Probe crate ~/git/fleet/probe-2026-11-02-laneA-shortresults/ on the laptop compiles the verbatim currency_poc sources (sha256-matched: executor.rs d6ad6d84…, recovery.rs 3d96f415…) against a SilentBackend whose create_transfers drops the batch, posts nothing, and returns Vec::new(), with accounts/balances delegated to a funded InMemoryLedger:

  • Control: real InMemoryLedger → execute() returns Posted, 3 legs findable by user_data_128 = 777.
  • Executor probe: execute() returned Ok with record state Posted while 0 legs were in the ledger.
  • Recovery probe: a genuine Recorded record (legs built by build_legs) reconciled under RecoveryPolicy::Resubmit against the silent backend → outcome Resubmitted, record state Posted, 0 legs in the ledger.

cargo test --offline on the probe: 2 passed (both probes assert the buggy outcome).

Why it matters

The intent record is the recovery source of truth. A conversion marked Posted that never posted is invisible to the recovery scanner afterwards (the record is resolved), so the user's source funds never move and no operator signal fires — the worst failure mode for a write-ahead design whose whole point (#2289, #2063) is that state on disk never claims more than the ledger proves. A real client binding that batches, pages, or errors per-request rather than per-transfer can plausibly return a short vector exactly when something else already went wrong.

Acceptance criteria

  • ConversionExecutor::submit fails (record stays Submitted, error names the conversion and the expected/actual result counts) when results.len() != record.legs.len().
  • reconcile's resubmission path treats a short result vector as a rejected resubmission (ResubmissionRejected, record left unresolved, scan continues) rather than Resubmitted/Posted.
  • The LedgerBackend::create_transfers trait doc states the one-result-per-transfer contract explicitly.
  • Regression tests in currency_poc/tests.rs cover a short-result backend (empty and partial, e.g. 1 of 3) for both submit and the recovery resubmission path; existing scenario tests still pass.

Related: #4133 (same vacuous-success class, leg-vector side), #2289 (the PoC), #2063 (design). Distinct from #4578 (FileIntentStore listing).

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
    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)
    effort/sSmall: expected under 2 hours of work
    on Nov 3, 2026
  2. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged — CONFIRMED on current main

  4. 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 3, 2026
  5. 60 remaining items

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

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. 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 4, 2026
  9. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 5, 2026
  11. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor
    • Session / ID: task 6652 (repo-fleet-monitor run 2026-11-05T15:39:47Z)
    • Found via: monitor reconciliation (Duty 1 claim/label divergence repair)
    • Task: Claim evaporated: task 6652 is queued and unclaimed in repo-health-code-pilot, but this issue still carried claimed labels. Labels reset to the truth (queue/queued + status/needs-triage). Checkpoint stage at evaporation: handed_off.
    • Expectation: The next lane claim picks task 6652 up from the queued state and posts a fresh pickup note.
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)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobstatus/needs-triageNot yet triaged; needs a first looktask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions