# Troubleshooting

**A restarted device pushes but never pulls.**
Subscriptions are not restored from disk — only your `openGraph`/`subscribe`
calls create them. Call `openGraph` for each graph on every boot.

**A graph stays `waiting` forever.**
No device that holds the key has synced since this one registered. Open the app
on a device that has the key, or check whether `autoGrant` is disabled. If a
device holds keys but is not granting, look for `error` events with scope `keys.gaps`.
Under `linking: 'approve'`, also check the other device for a `device.pending`
it never showed anybody: a device nobody approved is never wrapped to, and it
waits exactly like a device nobody has synced for.

**`device.pending` fires for a device that is obviously ours.**
Possession-pinning needs that device to have _written_ something this one
could open and verify, and to have registered a `selfSig`. A read-only member,
or a device registered by a build older than `selfSig`, will always need a
human. `trustedDevices()` shows which approvals were inferred and which a
person actually made.

**`device.conflict`, or a device that will not be wrapped to again.**
The keys the relay publishes for a device this one already approved have
changed. Device keys are set-once, so there is no benign explanation — treat it as
an incident. `forgetDevice(deviceId)` is the way back if you establish there
was one (a relay restored from a backup taken before that device registered,
say), and it puts the device back to "ask me again" rather than approving it.

**A rotation excluded somebody you did not expect.**
The `graph.rotate` relay event carries `excluded`. Those are devices the
rotating device's approval gate blocked. They keep reading up to the old
epoch and nothing after; approve them and the next sync fills the gap.

**`linking.unenforced` in the logs.**
`linking` is still `'observe'`, so unapproved devices were handed the key
anyway. That is the default for one release. Set `linking: 'approve'` once
your relay is new enough to understand `excluding` — an older one drops the
field and refuses the rotation as `incomplete-rotation`.

**`status.pending` never reaches zero.**
Look for an `error` event. Scope `keys.behind` means this device lacks the
current key — another device must sync. A repeated `push` error with
`retryable: false` means the relay is refusing the batch; check
`record.rejected` for the reason.

**`status.rejected` is climbing.**
The relay refused records for good. `record.rejected` with
`failure.reason === 'relay-rejected'` carries the relay's reason:
`rev-too-far-ahead` (a clock problem), `graph-record-quota` or
`graph-byte-quota` (a full graph).

**Records vanish on one device.**
Almost always a schema mismatch: that device's `KindRegistry` rejects what
another device wrote. Watch `record.rejected` with reason `schema` or
`unknown-kind`. Consider `onUnknown: 'accept'` during rolling deploys.

**`deviceOk` is false.**
This device ID is bound to different public keys upstream — usually a keystore
that was wiped while the device ID survived (a cleared browser profile with the
ID still in `localStorage`). Mint a new device ID; never retry the registration.

**409 `device-key-conflict` on boot.**
This is the same issue from the relay's side. Use a new device ID.

**403 `device-not-registered`.**
Your `authenticate` returned a device ID that is not registered to that member.
Check the token binds the device the engine is actually using.

**Two tabs disagree about unsynced writes.**
OPFS is one tab at a time and the second fell back to IndexedDB. Use
`IdbGraphStore` everywhere, or run one engine in a SharedWorker.

**A rotation returns 400 `incomplete-rotation`.**
Your `Directory.membersOfGroup` disagrees with the relay's device roster — the
rotation did not cover every active device of every current member. The
response names the missing device IDs.

**Everything is slow on a big import.**
The per-record cost is dominated by ECDSA (~310µs to sign, ~300µs to verify).
An app that genuinely does not need authorship can set `sign: false` and
roughly halve both, at the cost of `by` becoming a claim again.

---
