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>
This commit is contained in:
Padreug 2026-09-23 12:38:12 +02:00
commit 3d8368bcc4
3 changed files with 124 additions and 13 deletions

View file

@ -338,6 +338,7 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
apply_reported_state,
get_machine_by_atm_pubkey_hex,
list_all_active_machines,
mark_cassette_ops_acked,
)
machines = await list_all_active_machines()
@ -373,6 +374,7 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
event_message,
get_machine_by_atm_pubkey_hex,
apply_reported_state,
mark_cassette_ops_acked,
)
except Exception as exc:
logger.warning(
@ -383,10 +385,36 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
return filter_key
async def _record_op_acknowledgements(
machine_id: str, payload, mark_cassette_ops_acked
) -> None:
"""Mark the operations a machine reports as applied.
Deliberately not gated on whether the state event advanced the counts. The
machine echoes its applied-op ids on EVERY state publish, so an event
carrying nothing new about the counts can still be the first one to tell us
an operation landed; gating on that would lose the acknowledgement.
This echo is the only acknowledgement the transport can carry. An
addressable event gives its publisher no failure signal at all — the relay
returns OK for an event it then discards — so without it the dashboard
could only ever show an operation as sent, never as delivered.
"""
if not payload.applied_ops:
return
newly_acked = await mark_cassette_ops_acked(machine_id, payload.applied_ops)
if newly_acked:
logger.info(
f"spirekeeper: machine {machine_id} acknowledged "
f"{newly_acked} cassette operation(s)"
)
async def _handle_cassette_state_event(
event_message,
get_machine_by_atm_pubkey_hex,
apply_reported_state,
mark_cassette_ops_acked,
) -> None:
"""Verify signature, resolve the operator's signer, decrypt via the
signer abstraction (bunker round-trip for RemoteBunkerSigner; direct
@ -487,12 +515,16 @@ async def _handle_cassette_state_event(
)
if applied:
logger.info(
f"spirekeeper: applied bootstrap state event {event_id[:12]}... "
f"spirekeeper: applied reported state event {event_id[:12]}... "
f"to machine {machine.id} ({len(payload.positions)} cassettes)"
)
else:
# Replay: event_id already on file. Normal on relay reconnect.
# Replay or an older event. Normal on relay reconnect.
logger.debug(
f"spirekeeper: cassette state event {event_id[:12]}... "
f"already applied to machine {machine.id} (replay no-op)"
f"not newer than stored state for machine {machine.id} (no-op)"
)
# Acknowledgement runs regardless of whether the counts were newer — see
# _record_op_acknowledgements for why.
await _record_op_acknowledgements(machine.id, payload, mark_cassette_ops_acked)