fix(cassettes): order ATM state events, and reconcile the bay set #44

Merged
padreug merged 2 commits from fix/cassette-state-reconcile into main 2026-09-22 20:30:45 +00:00

2 commits

Author SHA1 Message Date
106da5b46b fix(cassettes): drop bays the machine no longer reports
Some checks failed
ci.yml / fix(cassettes): drop bays the machine no longer reports (pull_request) Failing after 0s
apply_reported_state upserted the positions in the payload and left every
other row untouched. When a machine's bay count shrank, the stale row
stayed: the dashboard showed a mix of old and new bays, and publish
validation then rejected every operator payload for a position-set
mismatch. The error text told the operator to fix it with atm-tui, which
could not propagate either, so the only way out was DELETE FROM by hand.

The payload is the machine's full bay set and the machine owns that layout,
so positions absent from it are now deleted. The delete runs before the
upserts: since this data layer commits per statement, a crash between the
two leaves rows missing rather than stale, and the next heartbeat
re-inserts them — the safe direction of the two.

Refs #43

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 22:23:40 +02:00
27449e1d11 fix(cassettes): order state events by created_at, not by one remembered id
The gate on the ATM-state consumer compared the incoming event id against
the id stored on a single arbitrary row (SELECT ... LIMIT 1, no ORDER BY).
That is a one-event memory, not a watermark: a re-delivered A, B, A applied
three times. Worse, created_at was parsed, written to state_at and then
never compared, so an event arriving late overwrote newer state — nothing
in the path ever looked at the clock.

Events are now applied only when strictly newer than the OLDEST state stamp
on file. Strict '>' subsumes replay dedup, since a replay carries the same
stamp. Oldest rather than newest is deliberate: every execute in this data
layer commits on its own, so a multi-row apply cannot be made atomic here,
and gating on the oldest means a crash mid-apply is re-applied on the next
event instead of being mistaken for a complete one. The ATM republishes on
a heartbeat, so it converges.

Stamps are compared as unix floats because SQLite returns integers,
Postgres returns timestamps and the incoming value is tz-aware; comparing
raw would either raise or quietly mislead. An unparseable incoming stamp
fails closed.

Also renames apply_bootstrap_state to apply_reported_state and corrects the
module comments. There has never been a once-per-machine guard, so calling
it a one-shot bootstrap consumer described something the code did not do.

Refs #43

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