Skip to content

Alarms and actions

Partial

An alarm detects a condition and holds it; an action does something about it. A simple action is a single step (a notify); a multi-step action is a flow. Both are stateful entities that hold their state directly (not event-sourced). An action’s credentials live in credentials; the Expr and Go-template machinery in expressions.

Metaphor: a morning alarm, not a pop-up. A pop-up alert appears and is gone (a notify action); an alarm goes off when its condition is met and stays high until interacted with. The alarm row holds its current state directly (status, opened_at, resolved_at in the target design; built today: severity, message, raised_at, cleared_at, acknowledged_at, acknowledged_by).

An alarm write is not sample-silent: raising or clearing one recomputes health in the same transaction, and the rollup records the verdict transition as a calculated-provenance property sample (provenance='calculated', source_rule='health-rollup'). Cycle safety therefore rests on provenance, not topology (ADR-0069, see Cycle safety).

An alarm’s raised state is a fact about its condition: the one-open invariant is per (component, dedup_key) (ADR-0075), and it is the condition, not a person, that decides whether the alarm is still up. An acknowledgement is a fact about a person: somebody has seen this. The two are therefore orthogonal, stored as two nullable columns rather than one status enum, and an alarm sits in any of these states:

| | unacknowledged | acknowledged | | --- | --- | --- | | raised | nobody has looked; the queue an operator works | somebody is on it, and it is still broken | | cleared | it came and went unattended | it was seen, then fixed |

POST /components/{name}/alarms/{id}:acknowledge records it, gated by alarm:acknowledge (held by operator and above). Three properties follow from acknowledgement being a fact about a person:

  • It does not recompute health. Acknowledging is not fixing. A recompute here would record a verdict transition at a moment when nothing about the fleet changed, and the health history is meant to be edges and only edges.
  • It carries its own permission and its own scope, resolved on the component tier from alarm:acknowledge itself rather than from component:update. Recording that somebody looked is not editing the component, and a responder role may hold one without the other (identity and access).
  • It is idempotent. The first sighting is the one that matters operationally (time to acknowledge), so a second acknowledgement keeps the first person and the first time and writes no second audit row, the same shape as a re-raise of an open condition returning the existing incident.

Un-acknowledging is not a verb. If it turns out to matter, it is a second verb and a second decision, not a nullable field somebody flips (#728).

alarm and action are stateful entities that hold their current state directly (not event-sourced); the physical layout (the owner arc, partitioning) lives on storage.

The built alarm row is component-local: no owner arc and no event_rule. It does carry its acknowledgement (#728), as two nullable columns rather than a status enum, because an enum would force acknowledged and cleared into one column and make them mutually exclusive, which they are not. The target design shape adds event_rule, the owner arc, status, opened_at, and resolved_at.

alarm (telemetry): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
severity text not null How bad; the rollup and the console sort on it
message text not null, default ''::text
raised_at timestamp with time zone not null, default now() When the incident opened
cleared_at timestamp with time zone NULL while open; the one-open index keys on it
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
component_id uuid FK → component.id, not null The component the incident is about (the thin-cut owner)
dedup_key text not null The condition identity (ADR-0075): one open alarm per (component, dedup_key), enforced by the partial unique index while open
acknowledged_at timestamp with time zone When a human first recorded seeing it; NULL while nobody has. Orthogonal to cleared_at
acknowledged_by uuid FK → principal.id Who recorded it; ON DELETE SET NULL, so purging a principal keeps the time, and audit_log keeps the name
CHECK constraints and unique indexes on alarm
  • alarm_severity_check: CHECK ((severity = ANY (ARRAY['info'::text, 'warning'::text, 'critical'::text])))
  • alarm_open_condition_key: CREATE UNIQUE INDEX alarm_open_condition_key ON public.alarm USING btree (component_id, dedup_key) WHERE (cleared_at IS NULL)

A command is two real tables: the command_type registry and the command invocation row with its computed settlement; see commands. The event_rule / action_rule config rows live with the rule families.

An abstract action (reboot) resolves to different concrete commands by component type / model through the cascade, so one reboot targets a heterogeneous fleet and each device runs the command its model declares; the edge dispatch and the command declaration live in templates / nodes.