Files
lmxopcua/docs/plans/2026-07-20-localdb-adoption-phase2.md.tasks.json
T
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

80 lines
2.6 KiB
JSON

{
"planPath": "docs/plans/2026-07-20-localdb-adoption-phase2.md",
"tasks": [
{
"id": 0,
"subject": "Task 0: Recon \u2014 sink schema, seam, drain lifecycle, role-view bridge (STOP conditions)",
"status": "completed",
"note": "STOP condition does NOT fire (no BLOB; PayloadJson TEXT). Recon doc: docs/plans/2026-07-20-localdb-phase2-recon.md. Key finding: the drain worker is an internal Timer inside the sink (not a hosted service/actor), so the gate is a Func<bool> ctor param; and HistorianAdapterActor already primary-gates ENQUEUE with a different policy (ShouldHistorize) - which is why today's ungated drain is safe and why replication breaks it. Deviations D-1..D-4 recorded."
},
{
"id": 1,
"subject": "Task 1: alarm_sf_events schema + registration (+ exact-set pin update)",
"status": "completed",
"blockedBy": [
0
],
"note": "alarm_sf_events created in AlarmSfSchema (Core.AlarmHistorian) + registered third in LocalDbSetup.OnReady. Exact-set pin updated to 3 tables. DEVIATION D-5: kept the legacy delete-on-ack + dead_lettered flag + last_error rather than the plan's status column (no sweeper for 'delivered' rows; last_error is the only record of why a row died). Drain ORDER BY moves to (enqueued_at_utc, id)."
},
{
"id": 2,
"subject": "Task 2: Rewire sink onto ILocalDb + delete bespoke file management (cutover 1/2)",
"status": "pending",
"blockedBy": [
1
]
},
{
"id": 3,
"subject": "Task 3: Primary-gated drain via PrimaryGatePolicy (cutover 2/2 \u2014 may co-commit with Task 2)",
"status": "pending",
"blockedBy": [
2
]
},
{
"id": 4,
"subject": "Task 4: One-time alarm-historian.db legacy migrator",
"status": "pending",
"blockedBy": [
3
]
},
{
"id": 5,
"subject": "Task 5: Convergence + failover scenarios in the pair harness (+ positive control)",
"status": "pending",
"blockedBy": [
3
]
},
{
"id": 6,
"subject": "Task 6: Rig config + docs",
"status": "pending",
"blockedBy": [
3
]
},
{
"id": 7,
"subject": "Task 7: DoD sweep (offline) \u2014 STOP and report after this",
"status": "pending",
"blockedBy": [
4,
5,
6
]
},
{
"id": 8,
"subject": "Task 8: Live gate on the docker-dev rig (needs explicit user go-ahead)",
"status": "pending",
"blockedBy": [
7
]
}
],
"lastUpdated": "2026-07-21T00:00:00Z"
}