Skip to content

Commands

Partial

A 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.

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: Settle tests windowSeconds > 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-input targets video-input) or target_metric_type_id (set-volume targets 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:

  1. the command row (the invocation: owner, command_type, params, actor, status='issued'), over the same exclusive owner arc as every sample and event;
  2. a caused event (origin=caused, typed command-issued), the record that a command happened; and
  3. for a settleable command, an intended series row in the target lane (provenance=intended, its ts the moment of issue, command_id naming 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).

The physical layout (the owner arc, the caused-event lineage, partitioning) lives on storage.

command_type (telemetry): generated from the migrated schema by make gen
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)
command (telemetry): generated from the migrated schema by make gen
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).