Appearance
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:
| Reason | Meaning |
|---|---|
register-failed | system.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 401 | Bearer token rejected. |
Cloud device heartbeat failed: HTTP 403 | Link revoked or body identity mismatch. |
Linked Host heartbeat requires hostLinkTokenRef | Local 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.exchangedo NOT emit tosystem.devices. They mutatesystem.authstate directly.auth.hostLink.create,auth.hostLink.revokesimilarly.
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:
| Event | Why it matters |
|---|---|
devices.added | Who 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
- Security logs — the broader log story.
- Package events — runtime / package events.
- Admin actions — what isn't an event today.
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')).