OfflineProtocol before startup. Event names use the type discriminator. The payloads below come from the published v0.31.0 declarations; fields inherited from BaseEvent are shown first.
Delivery outcomes
message_delivered and message_failed settle a send. message_deferred, message_retrying and message_undeliverable keep it pending. A decryption failure describes an attempt, not application rejection. A service response is a separate application result; correlate its request ID and provider.
BaseEvent
Base event interfacemessage_sent
Message sent eventmessage_received
Message received eventmessage_delivered
Message delivered eventmessage_failed
Message failed event. Terminal: the SDK has stopped retrying the message when this fires, and nomessage_delivered follows it for the same
message_id. Handle it idempotently all the same: a restore that is
retried after a failed MLS initialization can report an id it already
reported.
message_decryption_failed
Failed to decrypt an inbound encrypted message.transport_switched
Transport switched eventrelay_promoted
Relay promoted eventrelay_demoted
Relay demoted eventrelay_demoted_battery
Relay role was demoted due to battery constraints.neighbor_discovered
Neighbor discovered eventneighbor_lost
Neighbor lost eventpeer_rssi
A live signal-strength reading for a linked Bluetooth LE neighbor. Delivered only while at least oneon('peer_rssi', ...) listener is
registered (an 'all' listener does not turn it on), at most four a second
per peer. Only a peer that has proved its address on a link is named.
rssi is raw dBm and swings several dB between samples; smooth it for a
proximity display, for example next = 0.7 * prev + 0.3 * rssi.
identity_ready
This device’s own address is known. Fires once per successful startup, before any message can be sent. The address is derived from the identity key held in this profile’s storage: the app does not choose it. It is this device’ssender on every outbound
frame and the string peers must use as recipient to reach it, and it is
stable across restarts of the same profile.
Also readable at any time after startup via localAddress().
network_metrics
Network metrics eventfile_progress
File progress eventfile_received
File received eventfile_receive_failed
An inbound file transfer was dropped before completion: the receiver hit a resource limit (too many concurrent transfers, per-sender quota, or the buffered-bytes budget), the reassembled file failed its integrity checks, or the transfer went stale (no chunks within the stale timeout). Terminal and fired at most once per transfer: the failed transfer’s remaining in-flight chunks are dropped silently. Nofile_received will follow for
this file_id; the sender must re-send the file (under a fresh file_id)
to retry.
media_sent
Media sent event - all chunks ACK-deliveredmedia_send_failed
An outbound media transfer was aborted before all chunks were delivered (chunk encryption failed, or a chunk failed terminally). Nomedia_sent
will follow for this file_id; retry with a new sendMedia call.
ack_evicted
A pending ACK was evicted due to capacity constraints.fragment_assembly_evicted
A fragment assembly was evicted to make room for new fragments.diagnostic
Diagnostic event emitted by native modules for debugging purposessecure_session_established
Secure session established event Emitted when an MLS session is successfully established with a peersecure_session_failed
Secure session failed event Emitted when an MLS session fails to be establishedconvergence_diag
Convergence diagnostic event (1:1 MLS convergence instrumentation). Pure receiver-side breadcrumb for the Welcome receive / adopt / confirm path: carries no protocol effect. Surfaces stages the receiver otherwise emits nothing for (welcome reassembled, branch taken, decrypt landed).welcome_send_attempted
Welcome send attempted eventwelcome_send_succeeded
Welcome send succeeded eventwelcome_send_failed
Welcome send failed event. Note:welcome_send_succeeded → welcome_send_failed is a LEGAL sequence
for the same welcome over the internet transport. The bridge confirms on
socket-write success, but the relay stores nothing for offline
recipients: its later DeliveryError corrects the earlier success
(reason_code PEER_UNREACHABLE, retryable: true). Recovery is
automatic: the SDK keeps an escalating reachability probe running on
every carrier (a PEER_UNREACHABLE failure therefore always carries
next_retry_at), with presence-driven re-send as the faster edge when
the platform polls presence. This event may repeat once per probe round
while the peer stays offline; treat it as state, not as a terminal
verdict.
welcome_send_expired
Welcome send expired eventconnection_request_received
Connection request received eventconnection_request_undeliverable
An outbound connection request could not be delivered: the transport reported the recipient unreachable (e.g. the relay’s DeliveryError for an offline peer), or the request exhausted its retry budget (a genericmessage_failed also fires for the same id in that case). Correlate via
the message id returned by sendConnectionRequest. This is a status
signal, not proof of permanent failure: the original request may still
be delivered by the retry machinery if the peer comes back online, so
treat a user-initiated resend as potentially duplicating the original on
the recipient’s side.
connection_accepted
Connection accepted eventconnection_rejected
Connection rejected eventconnection_request_cancelled
Connection request cancelled eventgroup_created
Group created event (from relay)group_message_received
Group message received event (from relay)group_member_added
Group member added event (from relay)group_member_removed
Group member removed event (from relay)group_info
Stable SDK projection of a GroupInfo relay frame. The same frame is also emitted asinternet_server_message; use that raw
event for application-owned fields such as descriptions, avatars, pending
join requests, or future server extensions. Do not apply standard group
state from both events, and do not rely on their arrival order.
user_groups
Stable SDK projection of a UserGroups relay frame. The same frame is also emitted asinternet_server_message; use that raw
event for profile, membership, or future application-owned fields. Do not
apply standard group state from both events, and do not rely on their
arrival order.
group_error
Group error event (from relay).reason is a fixed code minted locally, not the relay’s wording:
not_found (the relay has no such group: invite links and relay fan-out
for it are dead, not merely refused), sync_denied (the relay refused to
register or sync the group for this caller), or error (any other
refusal). Do not parse it as prose or show it to users as-is.
The relay’s text is deliberately not carried here: __GROUP_ERROR__ is
accepted unsigned on the relay ingest path, so its wording is chosen by
whoever put the frame on that socket. Apps that genuinely need the exact
wording, invite-link flows correlating by request_id, for instance, should read the raw GroupError frame, which both bridges still dual-emit
on the server-message channel.
group_id is present when the relay scoped the error to a group.
group_relay_sync_changed
The relay-side registration state of a group changed.synced: true fires only on the relay’s positive registration
acknowledgment (including the idempotent re-sync ack after a membership
change): the signal that relay-dependent server commands for the group
(invite links via sendRawServerCommand, server-side fan-out) can be
issued. Await it via ensureGroupRegistered. synced: false fires when
that trust is revoked; reason says why: registered (relay ack),
error (group-scoped relay error: details arrive separately as
group_error / the raw GroupError frame), removed (we were removed
from the group), left (local leave), internet_dropped (transport
lost; re-registration re-arms on reconnect), ack_timeout (the relay
never answered: likely a relay without group support). Not emitted for
groups the relay was never asked about.
username_resolved
A username resolution finished, carrying every claim found.The set is the whole point
Anyone may publish any username claim, so a name resolves to a set of assertions, never to an answer. Even a single-device user is a set of one. This event fires exactly once per resolution and carries the complete set precisely so an app cannot accidentally treat the first arrival as the winner: there is no per-claim event to race and no ordering to mistake for a ranking. Let the user choose. Silently picking one claim converts a non-authoritative directory into an authoritative-looking one, which is worse than not shipping the feature: the user believes they are talking to the name, and the protocol only ever promised them a key. Present the claims, have a human confirm out of band (a QR code, a shared secret, a voice call), and store the address rather than the name. An emptyclaims list is an ordinary outcome, not an error.
group_message_sent
Group message sent event: a group message was sent to all members via mesh (MLS-encrypted fan-out).group_message_partial_failure
Group message partial failure: some members could not be reached.group_message_delivery_report
The relay’s settled per-recipient delivery report for a group message sent via relay broadcast, after the SDK acted on it. Fires once per broadcast whose report arrived, seconds aftergroup_message_sent (the relay reports only when its whole fan-out has
settled: correlate by message_id, never by order). Members in
delivered took the message over a live relay socket; members in
pushed took a device push carrying the ciphertext. Every other MLS
roster member has already been re-sent a per-member copy through the
ordinary delivery ladder (missed_reissued): no app action is
required; this is delivery observability, not a failure signal. Not
emitted when the report itself is lost: the SDK then re-broadcasts
(bounded) and finally downgrades the message to per-member fan-out.
group_rich_extras_dropped
Rich media metadata was dropped from an outbound group message because the group is not fully rich-capable (not every member advertised sealed rich payload support, or the local kill switch disabled it). The text was still sent; members receive it without the media attachment. Use this to warn the sender that an attachment did not go through.group_unauthorized_membership_change
An MLS membership change was made that its committer was not authorized to make. Readenforced first: it decides what this event means.
With group.enforceAdminCommits off (the default), the change has been
applied and enforced is false. MLS authenticated the committer as a
group member and accepted the commit. The SDK’s admin model is an
application-layer overlay on MLS (which has no admin concept), enforced
when sending; refusing the change on receipt would mean refusing the
MLS merge, permanently forking this member away from everyone who
accepted it. So the change stands and is reported here instead.
With that flag on, the commit was instead refused before merging and
enforced is true: nothing changed locally, and this device is now an
epoch behind the group. See the field doc below.
This signal can false-positive: “unauthorized” is judged against this
device’s local replica of role state, which replicates best-effort and
can lag: a legitimate change may be reported, and different members can
disagree about the same commit.
Treat this as a moderation signal for a human admin: an admin can undo
it with meshRemoveFromGroup / meshInviteToGroup. Never reverse a
change automatically off a single member’s event: corroborate first. A
member added this way can read all subsequent group traffic until removed.
Reports are rate-limited per (group, committer, enforced): a repeat of the
same outcome within a short window is not re-emitted, but every affected
group_member_added / group_member_removed still carries
authorized: false.
Known limitation: the member removed by an unauthorized Remove does not
receive this event: only the remaining members report the removal.
group_epoch_fork_detected
Epoch fork detected: concurrent MLS commits caused members to diverge onto different branches. The deterministic leader will attempt automatic resolution.group_epoch_fork_resolved
Epoch fork resolved: the leader re-established a canonical epoch. Members infailed_members could not be reached with the resolution
commit and may need re-inviting.
group_role_changed
Group role changed: a member’s role was changed (e.g. admin ↔ member).group_renamed
Group renamed: emitted when a rename is performed or received viameshRenameGroup. Renames observed only as relay-native GroupRenamed
frames surface through internet_server_message instead.
service_discovered
A service was discovered on the mesh in response to a discovery query.service_request_received
A service request was received from another peer.service_response_received
A response to a service request was received.presence_updated
Presence updated event: one unified stream for both sources (discriminated bysource): a peer-sent __PRESENCE__ update, or the
internet relay’s presence answer (driven by the SDK’s automatic watch
loop or an explicit checkInternetPresence).
Emission is 1:1 with the underlying signal: the SDK never dedupes
unchanged statuses, so every relay answer re-emits this event even when
nothing changed.
typing_indicator_received
Typing indicator received eventread_receipt_received
Read receipt received eventmessage_relayed
Message relayed event: this node forwarded a message for another peermessage_deferred
Message deferred event: a message was queued because no transport was availablemessage_retrying
Message retrying event: a retry was scheduled after a failed attempt (transport send error or ACK timeout). Non-terminal: MessageDelivered or MessageFailed still settles the message.message_undeliverable
Message undeliverable event: the transport reported the recipient unreachable for an in-flight message (e.g. the internet relay’s DeliveryError). Non-terminal: the message stays in the outbox. A plain DM is parked, its ACK retry budget stops burning while the peer is provably offline, and is re-driven on reachability edges (transport reconnect, peer discovery, presence-online; the SDK adds parked recipients to the presence watchlist). It settles only via MessageDelivered or, at outbox-lifetime expiry, MessageFailed. Media chunks are not parked and keep the normal retry machinery. Fires repeatedly for the same message_id while the recipient remains offline: a parked DM keeps an escalating reachability probe on every carrier (15s doubling to a 600s cap, the ladder shared per recipient), and each probe that reaches the relay while the peer is still offline earns a fresh verdict and re-emits this event. Treat it as a repeatable status signal, never as a terminal one. Note the probes refresh the outbox entry’s last-send timestamp, so a parked DM’s terminal MessageFailed arrives at the absolute outbox cap (4x the configured lifetime, ~28 days on defaults) rather than the sliding ~7-day window: plan pending-message UI accordingly.media_resend_required
Media resend required event: an outbound media transfer was in flight when the previous process died. The SDK persists only the transfer descriptor (never chunk bytes), so the app must re-supply the file bytes via sendMedia with this file_id; they are checksum-validated against the original transfer.dors_score_updated
DORS score updated event (per-transport scores).dors_transport_selected
DORS transport selected event (current choice and reason).dors_transport_switched
DORS transport switched event (transition with reason).dors_escalation_triggered
DORS escalation triggered (BLE→Wi‑Fi recommendation or applied).security_warning
A security-relevant anomaly was detected for a peer.SENDER_ADDRESS_MISMATCH has no benign reading: an address is the hash of
its owner’s identity key, so a frame signed by anyone else re-derives to a
different address. A peer that reinstalls does not re-key an address: it
gets a new address, and reaches you as a new contact. Treat this as an
impersonation attempt, not as a peer to re-trust.
UNSIGNED_CONTROL_REJECTED fires when a control frame arrives without a
signature. This is unconditional and not configurable: every peer running
this protocol signs its control traffic.
SESSION_REKEY_TRIGGERED is rate-based rather than per-event: a genuine
epoch fork heals this way occasionally, but the frame that triggers it is
not authenticated (see schedule_session_rekey), so a sustained rate for
one peer indicates injected frames. Delivery is delayed, never lost.
PUSH_KEY_PACKAGE_POOL_EXHAUSTED is also rate-based: each peer normally
gets its own single-use MLS init key, and this reports that the pool of
unconsumed packages hit its ceiling so one is being shared. Nothing fails, it clears as packages are consumed or expire, but a sustained rate means
the device is accumulating advertisements to peers that never establish a
session.
RELAY_ADDRESS_BINDING_MISMATCH has no benign reading either: the relay
acknowledged binding an address that is not this device’s, which only a
broken or hostile relay can produce. peer_id is the foreign address it
echoed. The connection is not torn down: a relay that controls the socket
already controls what a local refusal would protect.
RELAY_ADDRESS_DECLARATION_REFUSED is operational, not adversarial: the
relay declined the declaration, so this connection stays attributed by
account name. Existing conversations keep working; what breaks is
establishing new encrypted sessions over the relay, since the key-package
and welcome frames are identity-checked. peer_id is this device’s own id
and reason is the relay’s text, verbatim and opaque.
GROUP_LEAF_IDENTITY_UNPROVEN has no benign reading, and unlike the others
it accuses someone the user is already in a room with. Every member of a
group carries an identity key whose hash is their address, so an honest
member always re-derives their own name; a member that does not was built
around someone else’s. Emitted from four sites. Three refuse a claim as it
arrives, a declined group invite, a refused membership change, and a
declined 1:1 session Welcome, and in each the frame is dropped, so nothing
is delivered.
The session case can fire while an existing session with that peer stays
live, the refusal is non-destructive, so a secure_session_failed
alongside it means “this handshake attempt failed”, not “the session ended”.
The fourth site needs a different response: a roster read finding such a leaf
already seated in local group state. No wire gate can have admitted it,
so it was written to this device’s secure store directly, or joined by a
build predating those gates. It is kept out of every roster and cannot speak,
but it holds live group secrets and reads everything sent to the group: so
the remedy is to leave and re-create the group, not to remove a member.
peer_id is who the finding concerns, not always who to blame. On the three
refusal sites it is the peer that delivered the forgery, the inviter, or the
sender of the membership change, and it is proved: those frames are
signature-gated against the sender’s own address. On the fourth there is no
delivering peer and it is this device’s own id. reason is diagnostic text,
must not be parsed, and carries no identifier: the impersonated address stays
in the logs of the device that refused it.
STALE_CONTROL_FRAME refuses a frame for what its signature says about
when: stamped too far in the past, too far ahead of this device’s clock, or
carrying the older signing payload from a peer that has proved it can produce
the newer one. Check this device’s clock first. The signed timestamp is
compared against it, so a device whose own clock is wrong refuses honest
peers in bulk, and the event stream then looks like an attack rather than a
local fault. Many different peer_ids in a short window is a clock; one
peer_id while others are fine is that peer, either replaying captured
frames or running a broken build. Nothing is torn down: the frame is
dropped, unacknowledged, and a peer whose frame was genuinely just slow
re-sends a fresh one.
KEY_PACKAGE_OUTSIDE_VALIDITY_WINDOW is the same kind of finding for key
packages, and again the first thing to check is a clock. A peer’s key
package is valid from an hour before it was made until 30 days after,
judged by this device’s clock. A device more than an hour behind a peer
cannot start a session with it (the peer still can, and usually does).
Past 30 days each side refuses the other’s package, both report this, and
no encrypted session forms: messages and connection requests wait until
the clocks agree. This is what happens between phones that have not been
online to set their time. Many peer_ids is this device’s clock; one is that
peer’s. A window that has already closed may instead be an old package a
relay held for days; that package is discarded and the next one the peer
sends replaces it, and reason names both causes. Reported once per peer,
whether the refusal came when the package arrived or on a later send, and
peer_id is a claim, since the package has not proved its sender when its
window is checked. Show the user a prompt to check the date and time.
user_blocked
A user was blocked. Emitted for local UI notification only.user_unblocked
A user was unblocked. Emitted for local UI notification only.data_changed
A replicated document changed and the change reached storage. Emitted after the delta is durable, never before, so a UI that re-renders on this event is rendering state that survives a crash.data_doc_removed
A replicated document was removed from every replica of its space. Distinct from a document merely disappearing here: this one is a decision that travels. It fires when this device removes a document and when it learns of a removal from a peer, so a UI holding the document open can close it either way. A removal loses to a concurrent edit, so a document may come back after this fires. That arrives as an ordinarydata_changed on a document the
app thought was gone, which is the honest report: the edit won, and its
contents are the contents.
data_doc_size_warning
A replicated document is approaching the per-document size cap. Fires while there is still room to act (archive, split, prune). Without it the cap would be a cliff the app meets for the first time as a failed write.data_attachment_requested
A peer asked for the bytes behind an attachment reference. The SDK never holds blob bytes, so it cannot answer this on its own: answer withDataStore.provideAttachment, or say you cannot with
DataStore.declineAttachment. An app that handles neither leaves the
asking peer with a reference it can see and never open.
data_attachment_received
Attachment bytes arrived and matched the hash that asked for them. The bytes are handed over rather than stored: blobs never enter protocol state, so where the file lives is the app’s decision and its storage.data_attachment_unavailable
A fetch ended without bytes: the peer no longer holds the blob, or the transfer failed. Both are ordinary: an attachment reference outlives the bytes it names, because the reference replicates and the bytes do not.data_doc_unsyncable
A document cannot be replicated in any form this protocol has: too large for a sync frame, and the peer cannot carry it over the media path either. The two replicas will not converge until the document gets smaller.internet_server_message
A raw relay server frame that apps need outside or in addition to SDK-owned processing: invite-link lifecycle responses (GroupInviteLinkCreated,
GroupJoinedViaInvite, GroupInviteJoinPending, …), GroupRoleChanged,
GroupDeleted, RateLimited, and any future/unknown relay message types.
GroupError, GroupInfo, and UserGroups are dual-emitted with their
stable typed events so apps can consume request correlation or
application-owned extension fields without losing the SDK projection.
json is the verbatim relay frame; parse it and dispatch on its type
field. Pair with sendRawServerCommand for request/response flows. Typed
and raw events have no cross-channel ordering guarantee. Raw frames can
contain sensitive profile data, invite tokens, and key packages; avoid
logging them indiscriminately.
internet_status_changed
Internet-transport readiness transition.authenticated: true is the
positive gate for sendRawServerCommand: the replacement for app-side
relayStatus === 'authenticated' tracking against a separate app-owned
socket. connected: true, authenticated: false is the window where the
socket is up but the relay has not accepted the auth token yet (raw
sends still return false). Emitted only on actual transitions; query the
current value with isInternetReady().
internet_session_superseded
The relay displaced this device’s internet connection: a newer registration for the same identity took over the relay slot, and the relay closed this socket with code 4000 (optionally preceded by aSessionSuperseded notice). The SDK does not auto-reconnect after this: a blind reconnect would just re-displace the other socket in a tight loop
(the fleet-wide eviction storm the relay-displacement rollout guards
against). The transport is left stopped; the app should surface a
“connected elsewhere” state and reconnect only on explicit user action
(re-enabling the internet transport), or on foreground with long jitter.
reason is the relay-supplied close/notice reason when present.
Treat this as state, not as an edge. Delivery is at-least-once: nothing
else ever restates the fact it reports, so every layer works to make sure it
is not lost, and the cost is that it can repeat. Android redelivers a copy
held while JS could not take it (on your next subscribe or foreground); iOS
re-derives it from the live latch on every app foreground while the
transport stays superseded; and on both platforms the SDK holds a copy that
arrived before you had a listener for it and replays it to your first
on(...). Handlers must therefore be idempotent: setting a “connected
elsewhere” flag is fine, pushing a screen or firing a notification per event
is not. Repeats stop as soon as the transport is re-enabled, which also
drops any held copy.
The pull-side counterpart is isInternetSuperseded(), which answers the
same question on demand and survives the windows no in-memory delivery can
(a late subscribe, a JS reload, a process restart). An app that reconciles
against it on foreground needs nothing from this event but the prompt.
mesh_stopped_by_user
The user stopped the mesh from the Android foreground-service notification’s “Stop” action rather than through astop() call. The SDK has already torn
down the transports, the process scheduler, the keep-alive service and the
protocol core by the time this arrives: it is a notification, not a request
to act. Apps that track mesh state themselves must reconcile it here, or
they will keep reporting an active mesh against stopped transports.
Treat this as state, not as an edge. Like
InternetSessionSupersededEvent, this is one-shot, nothing ever
restates it, so it is held for redelivery when it cannot be delivered:
natively while JS is unreachable, and by the SDK when it arrives before you
have a listener for it (replayed to your first on(...)). Delivery is
at-least-once and handlers must be idempotent. A held copy is dropped by
start(), which is a mesh coming back up.
Android only; iOS has no equivalent notification affordance.

