Appearance
Online / offline status
A Device view has one of four status values. The transitions between them have specific causes; this page enumerates each.
The four states
status | Source data | Meaning |
|---|---|---|
online | live presence record, last heartbeat younger than heartbeatTtlMs | Device is up and reaching system.devices |
offline | live presence record, last heartbeat older than heartbeatTtlMs | Device was up recently but its heartbeat lapsed; pruned by _pruneStaleDevices() |
linked | durable Host Link status: 'active' AND no in-memory presence | Device exists on this account but is not currently running, or is running but has not yet reported in |
revoked | durable Host Link status: 'revoked' | Pairing was cancelled. Even a successful heartbeat would be rejected (token verification fails). |
Where each transition happens
(none) → online
devices.register (called by a heartbeat from host.control). SystemDevicesActor.onDevicesRegister() always sets status: 'online' when registering or refreshing a record:
ts
// projects/matrix-3/packages/system/src/SystemDevicesActor.ts:147-168
const next: IDeviceRecord = {
// ...
status: 'online',
source: 'presence',
// ...
};online → offline (presence-side)
_pruneStaleDevices() runs every PRUNE_INTERVAL_MS = 15_000 ms. It walks the in-memory map and marks any record whose lastHeartbeatAt + heartbeatTtlMs < now as offline:
ts
// projects/matrix-3/packages/system/src/SystemDevicesActor.ts:348-359
private _pruneStaleDevices(): void {
const now = Date.now();
for (const device of this._devices.values()) {
const ttl = device.heartbeatTtlMs > 0 ? device.heartbeatTtlMs : 0;
if (ttl === 0 || device.status === 'offline') continue;
if (now - device.lastHeartbeatAt > ttl) {
device.status = 'offline';
device.updatedAt = now;
this.emit('devices.offline', { deviceId: device.deviceId, reason: 'timeout' });
}
}
}devices.offline { deviceId, reason: 'timeout' } is emitted on transition.
online → offline (caller-driven)
devices.deregister flips a record to offline and emits devices.offline { deviceId, reason: 'deregistered' }. The caller must match the existing record's identity scope (principal/Space/authority); otherwise the call returns { ok: false, known: true } and nothing changes.
offline → online
The next successful devices.register re-registers and resets status to online. Heartbeats are idempotent.
(none) → linked
This transition is computed at view time, not stored. When devices.list is called with a principalId, the actor queries auth.hostLink.list({ principalId }) and projects each Host Link via hostLinkDeviceView(). If there is no live presence record for that hostId, the view sets status: 'linked'.
linked → online / merged
When a linked Device starts and posts its first heartbeat, the next devices.list will see both presence and Host Link, and the merge in hostLinkDeviceView() will produce a merged view with status: 'online'.
* → revoked
auth.hostLink.revoke flips the Host Link's status to revoked. From the next devices.list onwards, the projected view sets status: 'revoked'. The presence map may still have an online record for the revoked hostId; the view function explicitly demotes:
ts
// projects/matrix-3/packages/system/src/SystemDevicesActor.ts:563-568
status: link.status === 'revoked'
? 'revoked'
: presence?.status === 'online'
? 'online'
: 'linked',Heartbeats will continue arriving from the Device until its bearer token is invalidated, but cloud-side verifyHeartbeatToken() will reject them because the link is revoked.
What each status means for callers
| Status | UI copy | Can address actors? | Should rotate creds? |
|---|---|---|---|
online | "Online" green | yes | normal rotation schedule |
offline | "Offline" grey | NO (Device is unreachable) | n/a |
linked | "Not running" or "Connected — offline" | NO | yes if NATS creds are stale |
revoked | "Disconnected" or "Removed" | NO | n/a — re-pair instead |
Filtering with includeOffline
devices.list({ includeOffline: false }) (the default) omits offline and revoked views. includeOffline: true includes them — useful for the "all devices ever paired" page, but not for the live-status dashboard.
Edge cases
Heartbeat from a revoked Device
Cloud-side verifyHeartbeatToken() returns null if the link is revoked. host.control will see HTTP 403 from /_auth/host-link/heartbeat and emit device.heartbeat.failed. The Device's local system.devices (which doesn't see the cloud's revoke) may still show online status against its own bus until the next re-pairing. This is fine — local system.devices mirrors local presence, not cloud authority.
Two records for the same hostId
Should not happen in storage: devices.register uses deviceId (== hostId) as the map key. But the merge in compactDeviceViews() defends against it at view time anyway, preferring online over offline and most-recent updatedAt on tie.
A Device that pairs but never heartbeats
status: 'linked' forever (until revoked). Common during dev when the user pairs but never starts the runtime, or when a Device is paired in preparation for shipping to its eventual location.
See also
- Heartbeats — what causes the transitions.
- Device records — the underlying record shape.
- Operations / Disconnect — going from
onlineback to nothing.
Source:
projects/matrix-3/packages/system/src/SystemDevicesActor.ts(onDevicesRegister(),onDevicesDeregister(),_pruneStaleDevices(),hostLinkDeviceView()).