Skip to content

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

statusSource dataMeaning
onlinelive presence record, last heartbeat younger than heartbeatTtlMsDevice is up and reaching system.devices
offlinelive presence record, last heartbeat older than heartbeatTtlMsDevice was up recently but its heartbeat lapsed; pruned by _pruneStaleDevices()
linkeddurable Host Link status: 'active' AND no in-memory presenceDevice exists on this account but is not currently running, or is running but has not yet reported in
revokeddurable 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',
  // ...
};

onlineoffline (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.

onlineoffline (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.

offlineonline

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

linkedonline / 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

StatusUI copyCan address actors?Should rotate creds?
online"Online" greenyesnormal rotation schedule
offline"Offline" greyNO (Device is unreachable)n/a
linked"Not running" or "Connected — offline"NOyes if NATS creds are stale
revoked"Disconnected" or "Removed"NOn/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

Source: projects/matrix-3/packages/system/src/SystemDevicesActor.ts (onDevicesRegister(), onDevicesDeregister(), _pruneStaleDevices(), hostLinkDeviceView()).