Commands
PartialA command is the “do” half of the telemetry model, alongside a metric or property (know) and an event (happen): it records what a component was told. Its registry is command_type, the driver-owned twin of the sample and event catalogs.
The command_type registry
Section titled “The command_type registry”command_type describes every command: (name, label, params_schema, settle_window_seconds, target_property_type_id, target_metric_type_id, official). Two facts are the driver’s, not the abstract signal’s:
settle_window_seconds: how long the device physically takes to actuate (a projector warms up in twenty seconds, a matrix switch flips in one), so a difference from the reported value is not called drift until the device has had time to act. It is a duration, so the schema floors it at 0 and both write paths refuse a negative one:SettletestswindowSeconds > 0, so a negative window behaves exactly as zero does, and a value that is stored, read back, and then never honoured is worse than a refusal.- The two-armed target: a settleable command names the value it sets in exactly one lane,
target_property_type_id(set-inputtargetsvideo-input) ortarget_metric_type_id(set-volumetargets a numeric level), never both (a CHECK enforces the arc). A command with no target is fire-and-forget (reboot): it records the invocation and a caused event, with no value to settle.
The registry is seeded official and operator-extensible, official rows read-only, on the same shape as the sample and event catalogs (a console Command Types page, /command-types CRUD gated by command_type:*).
Issuing a command composes the whole model
Section titled “Issuing a command composes the whole model”Issuing is one transaction writing three things:
- the
commandrow (the invocation: owner, command_type, params, actor,status='issued'), over the same exclusive owner arc as every sample and event; - a caused
event(origin=caused, typedcommand-issued), the record that a command happened; and - for a settleable command, an
intendedseries row in the target lane (provenance=intended, itststhe moment of issue,command_idnaming this command as its lineage), the told in the want/told/is pivot.
POST /components/{name}/commands:issue is the write, gated by command:issue and scope-injected
through the component. The fence is the issue scope, not the read scope
(ADR-0117):
command is a component-tier resource, so a principal holding a wide component read beside a
room-scoped command grant may command that room and no more. The read scope decides only which
refusal it is owed, a non-disclosing 404 outside it and a 403 for a component it can see but not
command (the three-way split).
Settlement: the verdict is computed, the outcome is recorded
Section titled “Settlement: the verdict is computed, the outcome is recorded”A command’s effect is judged as a pure function of the intended value, the observed value, and the settle window:
- none: nothing was told (fire-and-forget, or no intended value).
- pending: still within the settle window, so a difference from observed is not yet drift.
- settled: past the window, observed matches intended.
- failed: past the window, observed does not match (or is absent).
This is the windowed form of the told versus is comparison the reconciliation read opens.
A zero window is terminal by construction, never by arithmetic
(ADR-0108).
settle_window_seconds: 0 is the way to say “settle immediately”, which is a claim about intent
rather than about elapsed time, so the verdict for such a command never consults a timestamp and
never comes back pending. And the comparison reads one clock, the database’s: a sample’s ts
is written by Postgres, so the now a settle-check judges against is read from Postgres too, in the
same transaction. A server whose clock differs from its database’s gets the same verdict either way.
Beside the computed verdict, the command row records its lifecycle: status is issued
until a terminal settled, failed, or timed-out, and settled_at stamps the terminal moment
(a CHECK ties the two: issued exactly while settled_at is null). The recorded status carries
the finer vocabulary the verdict collapses (failed versus timed-out; a fire-and-forget records
settled while its verdict stays none), so a stuck command is distinguishable from one nobody
has checked (ADR-0079).
Storage
Section titled “Storage”The physical layout (the owner arc, the caused-event lineage, partitioning) lives on storage.
| Column | Type | Constraints | Notes |
|---|---|---|---|
id | uuid | PK, default uuidv7() | |
name | text | not null | |
label | text | ||
description | text | not null, default ''::text | |
params_schema | jsonb | The JSON Schema an invocation's params must satisfy | |
settle_window_seconds | integer | not null, default 0 | How long settlement watches for the observed value to agree |
target_property_type_id | uuid | FK → property_type.id | One arm of the target arc: the property the command intends to set |
official | boolean | not null, default false | Shipped-canonical versus org-local; official rows are read-only |
registered_at | timestamp with time zone | not null, default now() | |
target_metric_type_id | uuid | FK → metric_type.id | The other arm: the metric the command intends to set; never both |
CHECK constraints and unique indexes on command_type
-
command_type_target_arc_check:CHECK (((target_property_type_id IS NULL) OR (target_metric_type_id IS NULL))) -
command_type_name_key:CREATE UNIQUE INDEX command_type_name_key ON public.command_type USING btree (name)
| Column | Type | Constraints | Notes |
|---|---|---|---|
id | bigint | PK | |
ts | timestamp with time zone | not null, default now() | |
owner_kind | text | not null | |
instance | text | not null, default ''::text | |
params | jsonb | The invocation arguments, validated against the registry schema | |
actor | uuid | Who told it | |
component_id | uuid | FK → component.id | |
system_id | uuid | FK → system.id | |
location_id | uuid | FK → location.id | |
node_id | uuid | FK → node.principal_id | |
command_type_id | uuid | FK → command_type.id, not null | What the component was told to do |
caused_event_id | bigint | FK → event.id | The recorded event this invocation caused |
status | text | not null, default 'issued'::text | The recorded lifecycle: issued until a terminal settled, failed, or timed-out |
settled_at | timestamp with time zone | The terminal moment; null exactly while status is issued |
CHECK constraints and unique indexes on command
-
command_owner_arc_check:CHECK ((((owner_kind = 'component'::text) AND (component_id IS NOT NULL) AND (system_id IS NULL) AND (location_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'system'::text) AND (system_id IS NOT NULL) AND (component_id IS NULL) AND (location_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'location'::text) AND (location_id IS NOT NULL) AND (component_id IS NULL) AND (system_id IS NULL) AND (node_id IS NULL)) OR ((owner_kind = 'node'::text) AND (node_id IS NOT NULL) AND (component_id IS NULL) AND (system_id IS NULL) AND (location_id IS NULL)))) -
command_owner_kind_check:CHECK ((owner_kind = ANY (ARRAY['component'::text, 'system'::text, 'location'::text, 'node'::text]))) -
command_settlement_recorded_check:CHECK (((status = 'issued'::text) = (settled_at IS NULL))) -
command_status_check:CHECK ((status = ANY (ARRAY['issued'::text, 'settled'::text, 'failed'::text, 'timed-out'::text])))
Related: properties, events, config, secrets, and variables, and alarms and actions (where a reconcile policy issues a command).