Commit graph

7 commits

Author SHA1 Message Date
786857f568 feat(models): settle_transaction op, alerts_pubkey, operator_notified_at (m017)
ADR-005 §6 groundwork. settle_transaction is a machine-wide op like
resume_cash_out; it carries the machine txid it closes and the operator's
provenance note. super_config.alerts_pubkey overrides where owed-cash
alerts go; dca_settlements.operator_notified_at stops a report resend
from re-alerting.
2026-10-10 22:26:45 +02:00
79413dc06d test: dispense outcome capture — handler transitions, payment gate, resume op, hold mirror
Some checks failed
ci.yml / test: dispense outcome capture — handler transitions, payment gate, resume op, hold mirror (pull_request) Failing after 0s
Twenty tests in the project's style (asyncio.run, monkeypatched crud, no
DB): confirmed → pending + distribution; nothing out → cash_owed; some
out → partial_pending with nothing spawned; already-captured recorded
not moved; identical resend acked without a row; report-before-payment
stored unlinked and adopted when the payment lands; remediation moves
the owed settlement; a remediation that did not confirm leaves it;
unpaired sender / malformed body refused. The payment gate: cash_out →
awaiting_dispense with no distribution, cash_in unchanged. The
resume_cash_out op's position rule and wire shape, the worklist model's
new buckets, and the state-document hold mirror set-and-clear. The
existing nulls-never-reach-the-wire test learns the machine-wide op.
2026-10-10 21:51:52 +02:00
c76a1bb125 feat(cassettes): persist the machine's counts-uncertain marker
The machine has been publishing counts_uncertain_since since v1 of the
state document and spirekeeper has been parsing it into a field nobody
read. That defeats the point of the marker: it exists so a human opens
the bay and recounts.

A dispenser can throw, or time out, after notes have physically moved.
The machine cannot know how many left, so rather than decrement a number
it would be guessing at, it stamps the moment (bitspire ADR-004,
decision 3). m014 gives that stamp a home on dca_machines, and the
consumer mirrors it on every state event.

Stored on the machine rather than the bay because the uncertainty is
about the dispense as a whole; a multi-bay dispense that fails midway
gives no reliable way to attribute it to one position.

Written through even when the machine reports None. The machine clearing
the marker is as important as setting it — the operator recounted, the
bay is trustworthy again — and a banner that never goes away is a banner
nobody reads.
2026-09-23 12:47:30 +02:00
3d8368bcc4 feat(cassettes): consume the machine's operation acknowledgements
The machine echoes the operation ids it has applied in its state
document, and this records them. That echo is the only acknowledgement
this transport can carry: an addressable event gives its publisher no
failure signal at all, since the relay returns OK for an event it then
discards. Without it the dashboard could only ever show an operation as
sent, never as delivered.

Deliberately not gated on whether the state event advanced the counts.
The machine echoes its applied ids on every publish, heartbeats included,
so an event carrying nothing new about the counts can still be the first
one to tell us an operation landed.

The consumer goes in before the producer on purpose. The machine does not
send applied_ops yet, and every new field on the state payload defaults to
a value meaning "this machine does not report that yet" rather than to
one that would be wrong — an absent list reads as nothing acknowledged,
which is exactly right for a machine that has applied nothing.

Also picks up seq and counts_uncertain_since, which the machine already
publishes and this side was dropping on the floor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 12:38:12 +02:00
90bd43d6da feat(cassettes): publish operations to the ATM
The v2 operator to ATM wire. Same kind-30078 document and the same d-tag
the counts wire used, because the machine subscribes by that tag and the
document is addressable, so v2 replaces v1 in place.

Sends a WINDOW of recent operations, oldest-first, not just the newest
change. Each publish replaces the last, so a machine that was offline for
one of them would otherwise never see that operation again; carrying the
recent history means the channel heals itself without anyone noticing it
broke. Re-delivery costs nothing because every op carries an id the
machine dedups on.

Tests pin the contract rather than the implementation: the d-tag, that
the payload declares v2 and carries no positions key, that window order
survives the publisher untouched, that an empty window still ships a
well-formed document so a machine can tell "no operations" from "operator
still on v1", and that an npub entered in the UI is normalised to hex —
get that last one wrong and the machine's subscription filter silently
never matches.

Additive. The endpoints still publish counts until the next commit, so
the tree is not left half-switched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 12:35:42 +02:00
b2157db219 feat(cassettes): record and read operator operations
Append-only, and deliberately nothing here writes cassette_configs. That
table now holds only what the machine has reported; letting an operation
write it would put back the second writer this whole design exists to
remove.

get_cassette_ops_window returns oldest-first because order is meaning: a
recount followed by a refill is not the same as the reverse. It takes the
most recent N and reverses, so the window slides without the machine ever
seeing them out of sequence.

The window is what makes the channel self-healing, so it has to cover a
plausible outage rather than just the newest change — a machine that
missed one event still sees the operation in the next.

_should_ack_op is extracted pure, the same way the state-event gate is,
because three of its rules are easy to get wrong and none need a database
to test: an unknown id closes out nothing, one machine must never be able
to ack another machine's operation, and the FIRST acknowledgement is the
one worth keeping. That last one matters because the machine echoes a
window, so every id comes back many times over; overwriting would keep
sliding the timestamp forward and lose when the operation actually landed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 12:33:46 +02:00
d8190375a6 feat(cassettes): schema and models for operator operations
First piece of the v2 wire (bitspire ADR-004). The operator stops
publishing counts and starts publishing what it DID; the machine, which
holds the notes, keeps the running total. A value with one writer cannot
be clobbered, which is the whole point: the absolute-count wire let a
form loaded before a dispense discard that dispense when published, and
nothing in an addressable event can tell the loser it lost.

m013 adds cassette_ops, append-only. The id is minted here and is the
idempotency key the machine dedups on, because a delta applied twice is
wrong and addressable events are re-delivered on reconnect. acked_at is
set when the machine reports that id back, which is the only
acknowledgement this transport can carry.

The models enforce that an op carries exactly the one field its type
means, so an instance is publishable by construction — the same contract
FeeConfigPayload has — and nulls never reach the wire for the machine to
disambiguate. recount is the only absolute, deliberately: it is what an
operator opening a bay and counting actually does, and it stays
auditable as its own act rather than looking like a stale form.

Vocabulary mirrors lamassu-server's cash_unit_operation_type.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 09:50:47 +02:00