Appearance
Error codes
The Devices op surface does not use string error codes. Failures are encoded as { ok: false, ... } returns. This page enumerates each failure and the cause.
system.devices failure shapes
devices.register → { ok: false, deviceId: '', created: false }
Cause: payload missing deviceId / hostId. The op cannot identify which Device to register.
Fix: include either deviceId or hostId in the payload.
devices.register → { ok: false, deviceId: <X>, created: false }
Cause: an existing record for deviceId = X has populated identity scope (principalId, spaceId, authorityRoot, hostLinkId, publicNamespace, or routeKey) and the new payload disagrees. See deviceRecordCanCarryForward() in SystemDevicesActor.ts:510-536.
Fix: either pass the same scope as the existing record (correct case, e.g. heartbeat from the rightful owner) or remove the existing record first by revoking the prior Host Link. This guards against accidental ownership swaps.
devices.heartbeat → { ok: false, known: false, deviceId: '' }
Cause: payload missing deviceId / hostId.
Fix: include either field.
devices.heartbeat → { ok: true, known: false, deviceId: <X> }
Cause: no record exists for deviceId = X. Heartbeat is a no-op.
Fix: register first via devices.register. Heartbeat does not auto-create.
devices.heartbeat → { ok: false, known: true, deviceId: <X> }
Cause: a record exists for deviceId = X, but the payload's scope disagrees with it.
Fix: same as devices.register ownership-mismatch fix.
devices.deregister → { ok: false, known: false, deviceId: '' }
Cause: payload missing deviceId / hostId.
devices.deregister → { ok: true, known: false, deviceId: <X> }
Cause: no record for X. No-op.
devices.deregister → { ok: false, known: true, deviceId: <X> }
Cause: record exists, payload scope disagrees. Refused.
devices.list (no error)
devices.list always returns { ok: true, count, devices }. Passing unknown / mistyped scope fields produces an empty result, not an error.
devices.get → { ok: false, device: null }
Cause: no Device matches the requested deviceId (and, if provided, the scope).
devices.runtimes.register → { ok: false, deviceId, runtimeId, created: false }
Three sub-causes:
- payload missing
deviceIdorruntimeId→deviceIdandruntimeIdmay be empty strings in the response - no Device record for
deviceId - payload scope disagrees with the existing Device record
devices.runtimes.list → { ok: false, deviceId, count: 0, runtimes: [] }
Cause: no Device record for deviceId, OR the scope-filter rejects the existing record.
system.auth Host Link failure shapes
These return null from store methods; the bus op layer wraps them as { ok: false, error?: string }. Common cases:
auth.hostLink.create errors
Throws if principalId, spaceId, or authorityRoot is empty:
Error: Host link requires principalId, spaceId, and authorityRootHostLinkStore.create() at host-auth.ts:799-859.
auth.hostLink.get / .refresh / .revoke / .verifyHeartbeat → null
null returns mean "not found" or "scope mismatch". The op layer returns { ok: false }. Specific causes:
idnot found inhostLinks[]hostIdnot found amongactivelinksidfound buthostIdfilter mismatches the record'shostIdidfound butprincipalIdfilter mismatches
verifyHeartbeatToken returns null for additional reasons:
- record exists but
status !== 'active'(revoked) - record has no
heartbeatTokenHash(was never rotated, or rotation was lost) - bearer token's SHA-256 does not equal the stored hash (
safeEqualStringsreturnsfalse)
auth.device.start / .approve / .exchange errors
Each store method returns null on a state-machine violation. The op layer surfaces { ok: false }.
State machine refusals:
- approve when status
cancelledorexchanged→ returnsnull - exchange when status is anything other than
approved→ returnsnull(or returns the record unchanged if alreadyexchanged) - get/cancel against a non-existent code → returns
null
Cloud HTTP heartbeat error codes
POST /_auth/host-link/heartbeat returns standard HTTP statuses; the gateway is the producer:
| Status | Cause | Fix |
|---|---|---|
200 | success | n/a |
400 | malformed body, missing fields | client bug |
401 | no Authorization header, or token has wrong format | include the bearer; check <host-home>/credentials/hivecast-host-link-token |
403 | verifyHeartbeatToken rejected (revoked, token mismatch, body identity disagrees with bearer-derived identity) | re-pair |
404 | Host Link not found | re-pair |
5xx | server-side error | retry |
The body→bearer identity-mismatch case (P1.17 adversarial scenario D) is rejected at the verification layer because the cloud uses bearer-derived identity, not body identity.
How errors propagate to the bus client
Bus clients (RequestReply.execute()) receive the JSON return shape directly. There is no separate error envelope. Callers MUST check response.ok === true before treating the data as authoritative. Callers SHOULD log the unhappy path before surfacing to the user.
Logging
host.control emits two failure events on its own mount:
device.heartbeat.failed { deviceId?, reason }— most common.device.heartbeat.failed { reason: '<error message>' }— for thrown errors (e.g. cloud HTTP failures).
system.devices does NOT emit devices.error events; failures are returned in the op response.
See also
- Operations / Troubleshoot — symptom → cause → fix.
- Inventory / Ownership — the scope mismatch rule.
- Pairing / Device flow — state machine for pending pairings.
Source:
projects/matrix-3/packages/system/src/SystemDevicesActor.ts(return paths);projects/matrix-3/packages/system-auth/src/host-auth.ts(nullreturn paths inHostLinkStore,HostDeviceLinkStore,HostPairingStore).