Skip to content

Device events

Device lifecycle events are emitted by system.devices (cloud-side authority) and host.control (Device-side heartbeat sender). Both can be subscribed to.

Events from system.devices

Source: SystemDevicesActor.emit(...) calls.

devices.added { deviceId }

Emitted: first registration of deviceId via devices.register.

Subscriber use: trigger UI updates that say "a new Device just appeared in this principal's Space."

devices.offline { deviceId, reason }

Emitted: from _pruneStaleDevices() (reason: 'timeout') or devices.deregister (reason: 'deregistered').

Subscriber use: dashboard UI grays out the row; alerting code may notify operators if a critical Device drops.

Events from host.control

Source: HostControlActor emit calls.

device.heartbeat { deviceId, runtimeCount, registryRoot? }

Emitted: each successful heartbeat cycle. Includes the count of runtimes the Host advertised. registryRoot is set when a cloud heartbeat was also posted.

Subscriber use: dashboards showing "last seen N seconds ago"; alerting on missing heartbeats.

device.heartbeat.failed { deviceId?, reason }

Emitted: any heartbeat-cycle failure.

Common reason values:

ReasonMeaning
register-failedsystem.devices.devices.register returned { ok: false }. Most often a scope mismatch.
<exception message>Anything thrown during _publishDeviceHeartbeat, including cloud HTTP failures.
Cloud device heartbeat failed: HTTP 401Bearer token rejected.
Cloud device heartbeat failed: HTTP 403Link revoked or body identity mismatch.
Linked Host heartbeat requires hostLinkTokenRefLocal creds incomplete.

Subscriber use: alerting on persistent heartbeat failures; auto-recovery hooks (target state).

What's NOT a Device event

Pairing flow events:

  • auth.device.start, auth.device.approve, auth.device.exchange do NOT emit to system.devices. They mutate system.auth state directly.
  • auth.hostLink.create, auth.hostLink.revoke similarly.

To audit "this Device was paired at time T", inspect the createdAt timestamp on the corresponding IHostLinkRecord via auth.hostLink.list.

Per-op invocation traces are also NOT emitted today. There's no devices.invoked { op, principalId, deviceId, payload } stream.

Cross-Device events

A principal owning multiple Devices receives events for all their Devices on the same authority root. NATS account isolation prevents seeing other principals' Device events.

The bus subjects look like:

<authority-root>.system.devices.$events.devices.added
<authority-root>.system.devices.$events.devices.offline
<authority-root>.host.control.$events.device.heartbeat
<authority-root>.host.control.$events.device.heartbeat.failed

(Subjects depend on how MatrixActor.emit and the wire-format construct event subjects. host.control mounts under the Device's authority root; system.devices mounts under the same root.)

Subscribing

From the bus:

bash
# All Device events on this root:
nats sub "<authority-root>.system.devices.$events.>"
nats sub "<authority-root>.host.control.$events.device.>"

From an actor:

ts
class MyActor extends MatrixActor {
  static subscribes = {
    'devices.offline': { description: 'React to a Device going offline' },
  };

  async onDevicesOffline(event: { deviceId: string; reason: string }) {
    // ...
  }
}

What to record for audit

If you're building a partial audit subscriber today, the high-value records are:

EventWhy it matters
devices.addedWho paired what when.
devices.offline { reason: 'deregistered' }Explicit takedown — record the operator if known.
device.heartbeat.failed (especially HTTP 401/403)Suspicious activity, possibly a leaked token being used post-revoke.
factotum.credential-stored (cross-section)Auth surface change — provider creds added.

These four cover most "something changed in security state" cases.

See also

Source: projects/matrix-3/packages/system/src/SystemDevicesActor.ts (emit('devices.added'), emit('devices.offline')); projects/matrix-3/packages/host-control/src/HostControlActor.ts (emit('device.heartbeat'), emit('device.heartbeat.failed')).