fix(cassettes): publish state on every change, and make the stamps monotonic
Three linked failures in one mechanism, so one commit. The state publish was gated on a one-shot 'have we said hello' flag. It fired once on first boot and then only after a dispense or an applied operator config, so any change to the layout itself — a reseed, an atm-tui edit, direct SQL — was never announced. The operator kept validating against a bay set the machine no longer had, and a publish from the dashboard could overwrite a fresh seed (#94). State is now published on every start. A publish is one fire-and-forget event with no retry. If the relay was unreachable at the moment of a dispense, that update was gone until the next customer bought cash. A five-minute heartbeat makes the channel self-healing and is also the only way an out-of-band edit to the table ever reaches the operator. Addressable events are ordered by created_at at second granularity with ties broken by lowest event id, and a relay acknowledges an event it then discards. Two publishes inside one second therefore left the winner decided by a hash, permanently, and a clock stepping backwards would have made every report from this machine vanish silently. Each publish now takes a stamp strictly above the last, recorded in the meta row that used to hold the gate — same key, no migration, honest name. Closes #94 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
db68e6e244
commit
54c59fadcc
6 changed files with 98 additions and 77 deletions
|
|
@ -406,10 +406,20 @@ export function getLastKnownConfigCreatedAt(): number {
|
|||
}
|
||||
|
||||
/**
|
||||
* Read the one-shot bootstrap-publish gate. Returns null if the ATM has
|
||||
* not yet published its `bitspire-cassettes-state:<machine_id>` hello-event.
|
||||
* The `created_at` of the last `bitspire-cassettes-state` event this machine
|
||||
* published, or null if it has never published one.
|
||||
*
|
||||
* This used to be a one-shot gate ("have we said hello yet"), which meant a
|
||||
* layout change after first boot was never announced (#94). It is now a
|
||||
* high-water mark: every publish records its stamp, and the next one is forced
|
||||
* strictly above it. Addressable events are ordered by `created_at` at second
|
||||
* granularity, and a relay silently keeps the higher one, so a clock that steps
|
||||
* backwards would otherwise make this machine's reports vanish with an `OK`.
|
||||
*
|
||||
* Stored under the original `bootstrapPublishedAt` meta key so no migration is
|
||||
* needed; the name is historical, the meaning is not.
|
||||
*/
|
||||
export function getBootstrapPublishedAt(): number | null {
|
||||
export function getLastStatePublishedAt(): number | null {
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('bootstrapPublishedAt') as
|
||||
| { value: string }
|
||||
|
|
@ -419,12 +429,8 @@ export function getBootstrapPublishedAt(): number | null {
|
|||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the bootstrap hello-event as published. Idempotent — only takes
|
||||
* effect the first time it's set. Subsequent calls overwrite the
|
||||
* timestamp (harmless; the gate just needs to be non-null).
|
||||
*/
|
||||
export function markBootstrapPublished(unixTimestamp: number): void {
|
||||
/** Record the `created_at` just published, as the next publish's floor. */
|
||||
export function markStatePublished(unixTimestamp: number): void {
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
db.prepare('UPDATE meta SET value = ? WHERE key = ?').run(
|
||||
String(unixTimestamp),
|
||||
|
|
@ -533,11 +539,12 @@ export function clearBunkerBinding(): void {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reset the bootstrap-publish gate so the ATM re-publishes its
|
||||
* `bitspire-cassettes-state` hello-event. Called on a re-pair (new seed) so
|
||||
* the new operator receives the spire's current state (aiolabs/bitspire#56).
|
||||
* Forget the publish high-water mark. Called on a re-pair (new seed): the
|
||||
* next publish is then free to use the wall clock, which is what a fresh
|
||||
* operator relationship wants. The state itself is republished on startup
|
||||
* regardless, so the new operator always receives current counts.
|
||||
*/
|
||||
export function resetBootstrapGate(): void {
|
||||
export function resetStatePublishWatermark(): void {
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
db.prepare('UPDATE meta SET value = ? WHERE key = ?').run('', 'bootstrapPublishedAt')
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue