Commit Graph

2 Commits

Author SHA1 Message Date
Joseph Doherty 8a9cb40a72 feat(localdb): alarm_sf_events replicated table
Adds the alarm store-and-forward buffer to the consolidated LocalDb file and
registers it for replication, so a node that dies with undelivered alarm
history no longer takes it to the grave.

The table keeps the legacy queue's shape rather than the status column the
plan sketched. An acknowledged row is deleted, not marked delivered: that
needs no second sweeper to keep the table bounded, leaves the capacity
semantics untouched, and the replication engine carries the delete as a
tombstone so the peer drops its copy anyway -- which is what the status
column was for. last_error is retained because it is the only operator-facing
record of why a row was dead-lettered.

The primary key is app-minted TEXT. The legacy AUTOINCREMENT RowId cannot
replicate under last-writer-wins: two nodes would independently allocate
rowid 7 to different alarms and silently overwrite each other. The drain
index therefore orders by enqueued_at_utc rather than insertion order, with
id as a tiebreak so the ordering is total.

Tables are created unconditionally, independent of AlarmHistorian:Enabled.
An empty registered table costs three triggers; creating it lazily would mean
a node that enables the historian later writes rows before its capture
triggers exist, which is exactly the silent-loss shape OnReady's ordering
comment warns about.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-21 03:58:50 -04:00
Joseph Doherty 124de57e6f docs(localdb): phase-2 recon findings
Answers Task 0 with file:line citations against master 2e46d054.

The STOP condition does not fire: PayloadJson is TEXT, no BLOB column, so
alarm_sf_events can be registered for replication as-is.

Two findings reshape the plan's assumptions:

- The drain worker is not a hosted service or an actor. It is a
  self-rescheduling Timer owned by the sink and started from the DI
  factory, so the Primary gate has to live inside DrainOnceAsync as an
  injected delegate -- Core.AlarmHistorian cannot reference
  PrimaryGatePolicy, which lives in Runtime.

- A Primary gate already exists on the enqueue side
  (HistorianAdapterActor.ShouldHistorize), using a different policy than
  the one the plan specifies for the drain. That gate is why today's
  ungated drain is safe: the Secondary never enqueues, so its queue is
  empty. Replicating the table breaks that invariant, which makes the
  drain gate mandatory in the same commit rather than a refinement.

Records deviations D-1 (deterministic content-hash ids so a boot-window
double-enqueue converges instead of duplicating), D-2 (gate denial must be
observable or a snapshot identity mismatch silently stops alarm history),
D-3 (Tasks 2+3 land as one commit) and D-4 (the rig will dead-letter in
~5 minutes at MaxAttempts 10 with no gateway).

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-21 03:56:06 -04:00