bug(machine): manual_dispense never decrements DB cassette counts — inventory overstates after operator remediation #76

Closed
opened 2026-07-03 22:29:19 +00:00 by padreug · 0 comments
Owner

Found during the dev-branch architecture review, alongside the position-decrement fix (#75).

Problem

recordTransaction()'s cassette-decrement branch fires only for type === 'cash_out' (apps/machine/electron/state-store.ts). The operator command poller records manual dispenses as type: 'manual_dispense' (apps/machine/electron/main.ts:683), so a remediation dispense:

  • does decrement HAL's in-memory bay counts (hal-service.ts dispenseCash decrements bays[i].count),
  • does not decrement the persisted cassettes rows in state.db.

The two inventory views then disagree: hal:get-inventory (in-memory) is correct until restart; state:get-inventory (DB) overstates. On the next boot, hal:init seeds bays from the DB, so the overstated counts become the live truth — the machine believes it has bills that were already dispensed to a customer.

Typical failure path

  1. Cash-out dispense fails (dispensed: 0 recorded, no decrement — correct).
  2. Operator issues a manual dispense command to remediate; bills physically leave the machine.
  3. manual_dispense transaction is recorded, but DB cassette counts are untouched.
  4. After the nightly auto-pull restart, inventory gating and the cassette-state publish to spirekeeper run on inflated counts.

Suggested fix

Include manual_dispense in the position-keyed decrement branch (it receives result.cassettes with per-bay dispensed counts, same shape as cash-out). Add a regression test next to state-store-transactions.test.ts.

One design question to settle first: whether a remediation dispense against a ref_txid whose original cash-out did already decrement (partial-dispense cases) should decrement again — the answer is probably yes (decrement reflects physical bills leaving bays, and the original tx only decremented what it actually dispensed), but worth a deliberate look at the remediate flow before changing it.

Found during the dev-branch architecture review, alongside the position-decrement fix (#75). ## Problem `recordTransaction()`'s cassette-decrement branch fires only for `type === 'cash_out'` (`apps/machine/electron/state-store.ts`). The operator command poller records manual dispenses as `type: 'manual_dispense'` (`apps/machine/electron/main.ts:683`), so a remediation dispense: - **does** decrement HAL's in-memory bay counts (`hal-service.ts` `dispenseCash` decrements `bays[i].count`), - **does not** decrement the persisted `cassettes` rows in `state.db`. The two inventory views then disagree: `hal:get-inventory` (in-memory) is correct until restart; `state:get-inventory` (DB) overstates. On the next boot, `hal:init` seeds bays from the DB, so the overstated counts become the live truth — the machine believes it has bills that were already dispensed to a customer. ## Typical failure path 1. Cash-out dispense fails (`dispensed: 0` recorded, no decrement — correct). 2. Operator issues a manual dispense command to remediate; bills physically leave the machine. 3. `manual_dispense` transaction is recorded, but DB cassette counts are untouched. 4. After the nightly auto-pull restart, inventory gating and the cassette-state publish to spirekeeper run on inflated counts. ## Suggested fix Include `manual_dispense` in the position-keyed decrement branch (it receives `result.cassettes` with per-bay `dispensed` counts, same shape as cash-out). Add a regression test next to `state-store-transactions.test.ts`. One design question to settle first: whether a remediation dispense against a `ref_txid` whose original cash-out **did** already decrement (partial-dispense cases) should decrement again — the answer is probably yes (decrement reflects physical bills leaving bays, and the original tx only decremented what it actually dispensed), but worth a deliberate look at the `remediate` flow before changing it.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire#76
No description provided.