> ## Documentation Index
> Fetch the complete documentation index at: https://www.offlineprotocol.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Events Reference

> All 72 event types emitted by the Mesh SDK: messages, connections, network, identity, security, groups, files, presence, service discovery, and DORS.

# Events Reference

Register listeners with `protocol.on(eventType, listener)`. See
[Event Listeners](/docs/mesh-sdk/methods#events) for the full listener API.

<Warning>
  Register listeners synchronously after construction, because the SDK subscribes to native events in
  its own constructor. Delivery is **at-least-once**, so handlers must be idempotent.
</Warning>

**Field naming.** Every event payload field is `snake_case`, matching the native event JSON.
The only exceptions are `type` and the optional `seenAt`, a local timestamp in milliseconds
recorded when the JS bridge observed the event.

**Types.** `ProtocolEvent` is the discriminated union of every event; `EventType` is
`ProtocolEvent['type']`.

## Message Events

### message\_sent

```typescript theme={null}
interface MessageSentEvent {
  type: 'message_sent';
  message_id: string;
  sender: string;
  recipient: string;
  content: string;
  priority: 'low' | 'medium' | 'high' | 'critical';
  requires_ack: boolean;
  timestamp: number;
  lamport_clock: number;   // causal ordering; 0 for legacy messages
  forward_info?: ForwardInfo;
}
```

### message\_received

```typescript theme={null}
interface MessageReceivedEvent {
  type: 'message_received';
  message_id: string;
  sender: string;
  recipient: string;
  content: string;
  hop_count: number;
  transport: string;
  timestamp: number;
  lamport_clock: number;
  encrypted?: boolean;         // true when MLS-encrypted and auto-decrypted
  reply_to_msg?: string;
  reply_context?: ReplyContext;
  content_type?: string;
  media_metadata?: MediaMetadataEvent;
  forward_info?: ForwardInfo;
}
```

`encrypted` is `false` only for plaintext accepted under the `requireEncryption: false`
opt-out.

### message\_delivered

Terminal success: the recipient ACKed.

```typescript theme={null}
interface MessageDeliveredEvent {
  type: 'message_delivered';
  message_id: string;
  latency_ms: number;
  hop_count: number;
  transport: string;
}
```

### message\_failed

**Terminal failure.** This is the only terminal failure signal for a message.

```typescript theme={null}
interface MessageFailedEvent {
  type: 'message_failed';
  message_id: string;
  reason: string;
  retry_count: number;
}
```

### message\_retrying

Non-terminal: a retry has been scheduled after a failed attempt.

```typescript theme={null}
interface MessageRetryingEvent {
  type: 'message_retrying';
  message_id: string;
  recipient: string;
  retry_count: number;
  next_retry_at: number;   // absolute Unix ms
}
```

### message\_deferred

The message was queued because no transport was available. Not a failure.

```typescript theme={null}
interface MessageDeferredEvent {
  type: 'message_deferred';
  message_id: string;
  recipient: string;
  reason: string;          // e.g. 'peer_not_reachable', 'transport_not_connected'
  retry_count: number;
  next_retry_at?: number;
}
```

`reason` is a stable classification from a fixed local vocabulary, not a rendered error string.

### message\_undeliverable

A transport reported the recipient unreachable for an in-flight message.

```typescript theme={null}
interface MessageUndeliverableEvent {
  type: 'message_undeliverable';
  message_id: string;
  recipient: string;
  reason: string;      // 'recipient_unreachable' on the relay-verdict path
  file_id?: string;    // owning media transfer, when the message is a media chunk
}
```

<Warning>
  **This is not terminal and it repeats.** The message stays in the outbox, and the event fires
  again for the same `message_id` while the recipient is offline, on an escalating probe (15s
  doubling to a 600s cap). A terminal `message_failed` arrives only at the absolute outbox cap,
  about 28 days on default settings. Do not settle a message in your UI on this event.
</Warning>

### message\_relayed

This node forwarded a message on behalf of another peer.

```typescript theme={null}
interface MessageRelayedEvent {
  type: 'message_relayed';
  message_id: string;
  sender: string;
  recipient: string;
  hop_count: number;
  remaining_ttl: number;
}
```

### message\_decryption\_failed

```typescript theme={null}
interface MessageDecryptionFailedEvent {
  type: 'message_decryption_failed';
  message_id: string;
  sender: string;
  code: DecryptionFailureCode;
  reason: string;
}

type DecryptionFailureCode =
  | 'INVALID_PAYLOAD'
  | 'NOT_INITIALIZED'
  | 'INVALID_CIPHERTEXT'
  | 'IDENTITY_MISMATCH'
  | 'CRYPTO_FAILURE'
  | 'PENDING_QUEUE_DROPPED'
  | 'UNKNOWN';
```

<Warning>
  **Advisory, not terminal.** It fires once per failed *attempt*, not once per message, because
  a receiver that cannot decrypt a frame withholds the delivery ACK so the sender's resend can
  deliver. Settle on `message_failed` or `file_receive_failed` instead.

  `PENDING_QUEUE_DROPPED` is the exception worth handling: the message was dropped from the
  pending-decryption queue on overflow or TTL, and it *was* ACKed on receipt, so the sender will
  not retransmit.
</Warning>

## Identity Events

### identity\_ready

Fires once per successful startup, before any message can be sent.

```typescript theme={null}
interface IdentityReadyEvent {
  type: 'identity_ready';
  address: string;   // this device's self-certifying off1… address
}
```

The address is derived from the identity key in this profile's storage and is stable across
restarts for the same `profile`. Also readable with `localAddress()`. See
[Identity & Addressing](/docs/mesh-sdk/identity).

## Connection Request Events

### connection\_request\_received

```typescript theme={null}
interface ConnectionRequestReceivedEvent {
  type: 'connection_request_received';
  sender: string;
  sender_name: string;
  timestamp: number;
  key_package?: number[];
  initial_message?: string;
}
```

`sender_name` and `initial_message` arrived **in plaintext**, because connection requests
precede the MLS session.

### connection\_request\_undeliverable

```typescript theme={null}
interface ConnectionRequestUndeliverableEvent {
  type: 'connection_request_undeliverable';
  recipient: string;
  message_id: string;
  reason: string;   // 'recipient_unreachable' | 'max_retries_exceeded'
                    // | 'outbox_lifetime_exceeded' | 'outbox_capacity_exceeded'
}
```

<Note>
  A status signal, not proof of permanent failure. The original request may still be delivered
  by the retry machinery, so a user-initiated resend can duplicate on the recipient's side.
</Note>

### connection\_accepted

```typescript theme={null}
interface ConnectionAcceptedEvent {
  type: 'connection_accepted';
  accepted_by: string;
  accepted_by_name: string;
  timestamp: number;
  key_package?: number[];
}
```

### connection\_rejected

```typescript theme={null}
interface ConnectionRejectedEvent {
  type: 'connection_rejected';
  rejected_by: string;
}
```

### connection\_request\_cancelled

```typescript theme={null}
interface ConnectionRequestCancelledEvent {
  type: 'connection_request_cancelled';
  cancelled_by: string;
}
```

## Network Events

### neighbor\_discovered

```typescript theme={null}
interface NeighborDiscoveredEvent {
  type: 'neighbor_discovered';
  peer_id: string;     // the peer's canonical off1… address
  transport: string;
  rssi?: number;
}
```

`peer_id` is the value that peer derived from its own identity key. Use it directly as
`recipient`, regardless of which transport discovered the peer.

### neighbor\_lost

```typescript theme={null}
interface NeighborLostEvent {
  type: 'neighbor_lost';
  peer_id: string;
}
```

### transport\_switched

```typescript theme={null}
interface TransportSwitchedEvent {
  type: 'transport_switched';
  from: string | null;
  to: string;
  reason: string;
}
```

### network\_metrics

```typescript theme={null}
interface NetworkMetricsEvent {
  type: 'network_metrics';
  neighbor_count: number;
  relay_count: number;
  delivery_ratio: number;
  avg_latency_ms: number;
}
```

## Internet Transport Events

### internet\_status\_changed

```typescript theme={null}
interface InternetStatusChangedEvent {
  type: 'internet_status_changed';
  connected: boolean;
  authenticated: boolean;
}
```

`authenticated: true` is the positive gate for `sendRawServerCommand`. The
`connected: true, authenticated: false` window is where the socket is up but the relay has not
yet accepted the auth token. Emitted only on actual transitions, so query the current value with
`isInternetReady()`.

### internet\_session\_superseded

The relay displaced this device's connection: a newer registration for the same identity took
the relay slot.

```typescript theme={null}
interface InternetSessionSupersededEvent {
  type: 'internet_session_superseded';
  reason?: string;
}
```

<Warning>
  The SDK does **not** auto-reconnect from this state. Recovery is a deliberate
  `enableTransport('internet', { serverAddress })`.

  Treat it as **state, not an edge**: it can repeat, and it is one of the two one-shot events
  replayed to a late listener. Handlers must be idempotent. The pull-side counterpart is
  `isInternetSuperseded()`.
</Warning>

### internet\_server\_message

A raw relay frame your app needs outside or in addition to the SDK's own processing.

```typescript theme={null}
interface InternetServerMessageEvent {
  type: 'internet_server_message';
  json: string;   // the verbatim relay frame
}
```

Carries invite-link lifecycle responses (`GroupInviteLinkCreated`, `GroupJoinedViaInvite`,
`GroupInviteJoinPending`), `GroupRoleChanged`, `GroupDeleted`, `RateLimited`, and any future
relay message types. `GroupError`, `GroupInfo`, and `UserGroups` are dual-emitted here *in
addition to* their typed events. Apply state from one channel, not both, as there is no
cross-channel ordering guarantee.

<Warning>
  Raw frames can contain profile data, invite tokens, and key packages. Do not log them
  indiscriminately.
</Warning>

### mesh\_stopped\_by\_user

Android only. The user stopped the mesh from the foreground-service notification's Stop action
rather than through `stop()`.

```typescript theme={null}
interface MeshStoppedByUserEvent {
  type: 'mesh_stopped_by_user';
}
```

Everything is already torn down, so this is a notification, not a request to act. It is the
second one-shot event; a held copy is dropped by `start()`.

## Security Events

### secure\_session\_established

```typescript theme={null}
interface SecureSessionEstablishedEvent {
  type: 'secure_session_established';
  peer_id: string;
  group_id: string;
  is_session: boolean;         // true for 1:1, false for a group
  initiated_by_local: boolean;
}
```

### secure\_session\_failed

```typescript theme={null}
interface SecureSessionFailedEvent {
  type: 'secure_session_failed';
  peer_id: string;
  reason: string;
}
```

<Warning>
  As of v0.21.0 this can fire while the session with that peer stays **live**. An app that tears
  down session state on this event alone must stop doing so.
</Warning>

### security\_warning

```typescript theme={null}
interface SecurityWarningEvent {
  type: 'security_warning';
  peer_id: string;
  reason_code: SecurityWarningCode;
  reason: string;
}

type SecurityWarningCode =
  | 'SENDER_ADDRESS_MISMATCH'
  | 'TRANSPORT_IDENTITY_MISMATCH'
  | 'CONTROL_SIGNATURE_INVALID'
  | 'UNSIGNED_CONTROL_REJECTED'
  | 'MEDIA_SENDER_GROUP_MISMATCH'
  | 'PLAINTEXT_SEND'
  | 'PLAINTEXT_RECEIVE_REJECTED'
  | 'SESSION_SENDER_GROUP_MISMATCH'
  | 'SESSION_REKEY_TRIGGERED'
  | 'NOSTR_KEY_PACKAGE_SLOT_EXHAUSTED'
  | 'PUSH_KEY_PACKAGE_POOL_EXHAUSTED'
  | 'RELAY_ADDRESS_BINDING_MISMATCH'
  | 'RELAY_ADDRESS_DECLARATION_REFUSED'
  | 'GROUP_LEAF_IDENTITY_UNPROVEN';
```

| Code                                | What it means                                                                                                            |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `SENDER_ADDRESS_MISMATCH`           | Impersonation. There is no benign reading; this is not a re-keyed peer.                                                  |
| `PLAINTEXT_SEND`                    | Emitted once per peer when `requireEncryption: false` lets a plaintext send through.                                     |
| `SESSION_REKEY_TRIGGERED`           | Rate-based, not per-event. An epoch mismatch triggered a session re-key.                                                 |
| `PUSH_KEY_PACKAGE_POOL_EXHAUSTED`   | Rate-based, not per-event.                                                                                               |
| `RELAY_ADDRESS_BINDING_MISMATCH`    | A broken or hostile relay. `peer_id` is the foreign address it echoed.                                                   |
| `RELAY_ADDRESS_DECLARATION_REFUSED` | Operational. `peer_id` is this device's own id; `reason` is the relay's verbatim text.                                   |
| `GROUP_LEAF_IDENTITY_UNPROVEN`      | A forged leaf already seated in local group state requires **leaving and re-creating the group**, not removing a member. |

### Welcome delivery events

```typescript theme={null}
interface WelcomeSendAttemptedEvent {
  type: 'welcome_send_attempted';
  peer_id: string;
  message_id: string;
  group_id: string;
  attempt: number;
}

interface WelcomeSendSucceededEvent {
  type: 'welcome_send_succeeded';
  peer_id: string;
  message_id: string;
  group_id: string;
  attempt: number;
}

interface WelcomeSendFailedEvent {
  type: 'welcome_send_failed';
  peer_id: string;
  message_id: string;
  group_id: string;
  attempt: number;
  reason_code: WelcomeReasonCode;
  transport_error?: string;
  retryable: boolean;
  next_retry_at?: number;
}

interface WelcomeSendExpiredEvent {
  type: 'welcome_send_expired';
  peer_id: string;
  message_id: string;
  attempt: number;          // note: no group_id
  reason_code: WelcomeReasonCode;
}

type WelcomeReasonCode =
  | 'TRANSPORT_UNAVAILABLE'
  | 'PEER_UNREACHABLE'
  | 'PEER_DISCONNECTED'
  | 'TIMEOUT'
  | 'INTERNAL_ERROR'
  | 'RETRY_EXHAUSTED';
```

<Note>
  `welcome_send_succeeded` followed by `welcome_send_failed` for the same Welcome is a **legal**
  sequence over the internet transport: the bridge confirms on socket write, but the relay
  stores nothing for offline recipients, so its later delivery error corrects the earlier
  success. Treat these as state, not a terminal verdict. A `PEER_UNREACHABLE` failure always
  carries `next_retry_at`.
</Note>

### convergence\_diag

A receiver-side breadcrumb for the Welcome receive/adopt/confirm path. Carries no protocol
effect.

```typescript theme={null}
interface ConvergenceDiagEvent {
  type: 'convergence_diag';
  stage: string;     // e.g. 'welcome_received' | 'welcome_branch' | 'decrypt_success'
  peer_id: string;
  detail: string;    // free-form key=value context
}
```

## Group Events

### group\_created

```typescript theme={null}
interface GroupCreatedEvent {
  type: 'group_created';
  group_id: string;
  name: string;
}
```

### group\_message\_received

```typescript theme={null}
interface GroupMessageReceivedEvent {
  type: 'group_message_received';
  group_id: string;
  sender: string;
  content: string;
  timestamp: string;        // note: string, not number
  message_id: string;
  reply_to_msg?: string;
  forward_info?: ForwardInfo;
  media_metadata?: MediaMetadataEvent;
  content_type?: string;
}
```

### group\_message\_sent

```typescript theme={null}
interface GroupMessageSentEvent {
  type: 'group_message_sent';
  group_id: string;
  message_ids: string[];
  member_count: number;
}
```

### group\_message\_partial\_failure

```typescript theme={null}
interface GroupMessagePartialFailureEvent {
  type: 'group_message_partial_failure';
  group_id: string;
  failed_members: string[];
  succeeded_members: string[];
}
```

### group\_message\_delivery\_report

The relay's settled per-recipient report for a relay-broadcast group message, emitted **after**
the SDK has already acted on it.

```typescript theme={null}
interface GroupMessageDeliveryReportEvent {
  type: 'group_message_delivery_report';
  group_id: string;
  message_id: string;
  delivered: string[];        // relay-side write-ack confirmed
  pushed: string[];           // offline at the relay, took a device push
  missed_reissued: string[];  // reached neither way: re-sent per-member automatically
}
```

<Note>
  This is observability, not a failure signal. `missed_reissued` members were already re-sent.
  It fires once per broadcast whose report arrived, seconds after `group_message_sent`, so
  correlate by `message_id`, never by order.
</Note>

### group\_member\_added / group\_member\_removed

```typescript theme={null}
interface GroupMemberAddedEvent {
  type: 'group_member_added';
  group_id: string;
  user_id: string;
  added_by: string;
  group_name?: string;
  authorized?: boolean;
}

interface GroupMemberRemovedEvent {
  type: 'group_member_removed';
  group_id: string;
  user_id: string;
  removed_by: string;
  authorized?: boolean;
}
```

<Warning>
  **An absent `authorized` means "not evaluated", not "authorized."** It is omitted for your own
  join from a Welcome, for relay reconciliation frames, and by older cores.

  `authorized: false` means the change *did* happen (MLS accepted the commit) but the
  committer was not a known admin. The judgment is made against this device's local,
  best-effort-replicated role state, which can lag and which members can disagree about. Do not
  act on it automatically.
</Warning>

### group\_unauthorized\_membership\_change

```typescript theme={null}
interface GroupUnauthorizedMembershipChangeEvent {
  type: 'group_unauthorized_membership_change';
  group_id: string;
  committer: string;      // the MLS-authenticated committer
  added: string[];        // sorted; empty for a pure removal
  removed: string[];      // sorted; empty for a pure addition
  reason: string;         // 'sender_not_admin' | 'affected_member_mismatch'; treat as opaque
  enforced: boolean;
}
```

<Warning>
  **Read `enforced` first.**

  * `enforced: false` (default config): the change **has been applied**, and roster events
    accompany it. An admin can undo it with `meshRemoveFromGroup` / `meshInviteToGroup`.
  * `enforced: true` (with `group.enforceAdminCommits` on): the commit was **refused** before
    merging. Nothing changed locally, but this device is now an epoch behind and must be
    re-invited. This is a partition alarm.

  It can false-positive when the local role replica lags. Known limitation: the member removed
  by an unauthorized Remove does not receive this event.
</Warning>

### group\_info

```typescript theme={null}
interface GroupInfoEvent {
  type: 'group_info';
  group_id: string;
  name: string;
  created_by: string;
  created_at: string;
  members: Array<{
    user_id: string;
    role: string;
    joined_at: string;
  }>;
}
```

Dual-emitted as `internet_server_message`. Use the raw event for app-owned fields such as
descriptions, avatars, and pending join requests, but do not apply state from both.

### user\_groups

```typescript theme={null}
interface UserGroupsEvent {
  type: 'user_groups';
  groups: Array<{
    group_id: string;
    name: string;
    created_at: string;
  }>;
}
```

### group\_role\_changed

```typescript theme={null}
interface GroupRoleChangedEvent {
  type: 'group_role_changed';
  group_id: string;
  user_id: string;
  new_role: string;
  changed_by: string;
}
```

### group\_renamed

```typescript theme={null}
interface GroupRenamedEvent {
  type: 'group_renamed';
  group_id: string;
  new_name: string;
  old_name: string | null;
  renamed_by: string;
}
```

Emitted for renames performed or received via `meshRenameGroup`. Renames observed only as
relay-native frames surface through `internet_server_message` instead.

### group\_relay\_sync\_changed

```typescript theme={null}
interface GroupRelaySyncChangedEvent {
  type: 'group_relay_sync_changed';
  group_id: string;
  synced: boolean;
  reason: string;   // 'registered' | 'error' | 'removed' | 'left'
                    // | 'internet_dropped' | 'ack_timeout'
}
```

`synced: true` fires only on the relay's positive registration ack. Await it with
`ensureGroupRegistered()`.

### group\_rich\_extras\_dropped

```typescript theme={null}
interface GroupRichExtrasDroppedEvent {
  type: 'group_rich_extras_dropped';
  group_id: string;
  unknown_members: string[];
}
```

Rich media metadata was dropped from an outbound group message because the group is not fully
rich-capable. The text was still sent; members receive it without the attachment.
`unknown_members` is empty when the local `richPayloadEnabled` kill switch caused the drop
instead. Use `meshGroupRichReadiness()` to warn *before* sending.

### group\_epoch\_fork\_detected / group\_epoch\_fork\_resolved

Concurrent MLS commits caused members to diverge. The deterministic leader attempts automatic
resolution.

```typescript theme={null}
interface GroupEpochForkDetectedEvent {
  type: 'group_epoch_fork_detected';
  group_id: string;
  local_epoch?: number;
}

interface GroupEpochForkResolvedEvent {
  type: 'group_epoch_fork_resolved';
  group_id: string;
  resolved_epoch: number;
  failed_members: string[];   // may need re-inviting
}
```

### group\_error

```typescript theme={null}
interface GroupErrorEvent {
  type: 'group_error';
  reason: 'not_found' | 'sync_denied' | 'error';
  group_id?: string;
}
```

<Note>
  `reason` is a fixed code minted locally, **not** the relay's wording. Do not parse it as prose
  or show it to users as-is. Apps needing the exact wording (invite-link flows correlating by
  request ID) should read the raw `GroupError` frame from `internet_server_message`.
</Note>

## File & Media Events

### file\_progress

```typescript theme={null}
interface FileProgressEvent {
  type: 'file_progress';
  file_id: string;
  chunks_sent: number;
  total_chunks: number;
  percentage: number;
}
```

### file\_received

```typescript theme={null}
interface FileReceivedEvent {
  type: 'file_received';
  file_id: string;
  file_name: string;
  file_size: number;
  sender: string;
  content_type: string;
  media_metadata?: MediaMetadataEvent;
  file_data: string;          // base64-encoded reassembled file
  timestamp?: number;
  caption?: string;
  reply_to_msg?: string;
  reply_context?: ReplyContext;
  forward_info?: ForwardInfo;
}
```

### file\_receive\_failed

**Terminal**, at most once per transfer. No `file_received` will follow for this `file_id`;
the sender must re-send under a fresh one.

```typescript theme={null}
interface FileReceiveFailedEvent {
  type: 'file_receive_failed';
  file_id: string;
  file_name: string;
  sender: string;
  reason: string;   // 'too_many_transfers' | 'sender_quota_exceeded'
                    // | 'buffer_budget_exhausted' | 'integrity_check_failed'
                    // | 'stale_timeout'
}
```

### media\_sent

All chunks were ACK-delivered.

```typescript theme={null}
interface MediaSentEvent {
  type: 'media_sent';
  file_id: string;
  content_type: string;
  recipient: string;
}
```

### media\_send\_failed

The outbound transfer aborted before all chunks were delivered. No `media_sent` will follow.

```typescript theme={null}
interface MediaSendFailedEvent {
  type: 'media_send_failed';
  file_id: string;
  recipient: string;
  reason: string;
}
```

### media\_resend\_required

An outbound media transfer was in flight when the previous process died.

```typescript theme={null}
interface MediaResendRequiredEvent {
  type: 'media_resend_required';
  file_id: string;
  recipient: string;
  file_name: string;
  file_size: number;
}
```

The SDK persists only the transfer descriptor, never chunk bytes, so your app must re-supply
the file bytes via `sendMedia` **with this `file_id`**. They are checksum-validated against the
original transfer.

## Presence & Typing Events

### presence\_updated

```typescript theme={null}
interface PresenceUpdatedEvent {
  type: 'presence_updated';
  peer_id: string;
  status: 'online' | 'away' | 'offline';
  timestamp: number;
  last_seen_ms?: number;
  source: 'internet' | 'peer';
}
```

One unified stream for both sources. `internet` is relay-observed presence: an authoritative
`CheckPresence` answer or relay-derived reachability. `peer` is a peer-sent self-report.
Apps rendering relay-style presence UI should filter on `internet`.

<Note>
  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.
</Note>

`last_seen_ms` is present only for relay-sourced presence, and only when the relay knows it.

### typing\_indicator\_received

```typescript theme={null}
interface TypingIndicatorReceivedEvent {
  type: 'typing_indicator_received';
  sender: string;
  conversation_id: string;
  is_typing: boolean;
  timestamp: number;
}
```

### read\_receipt\_received

```typescript theme={null}
interface ReadReceiptReceivedEvent {
  type: 'read_receipt_received';
  sender: string;
  message_ids: string[];
  timestamp: number;
}
```

## Relay Role Events

### relay\_promoted

```typescript theme={null}
interface RelayPromotedEvent {
  type: 'relay_promoted';
  connection_count: number;
  battery_level: number;
}
```

### relay\_demoted

```typescript theme={null}
interface RelayDemotedEvent {
  type: 'relay_demoted';
  reason: string;
}
```

### relay\_demoted\_battery

```typescript theme={null}
interface RelayDemotedBatteryEvent {
  type: 'relay_demoted_battery';
  battery_level: number;
  min_required: number;
}
```

## Blocking Events

Emitted for local UI notification only.

```typescript theme={null}
interface UserBlockedEvent {
  type: 'user_blocked';
  user_id: string;
}

interface UserUnblockedEvent {
  type: 'user_unblocked';
  user_id: string;
}
```

## Service Discovery Events

### service\_discovered

```typescript theme={null}
interface ServiceDiscoveredEvent {
  type: 'service_discovered';
  query_id: string;
  service_id: string;
  version: string;
  provider_peer_id: string;
  capabilities: Record<string, string>;
  hop_count: number;
}
```

### service\_request\_received

```typescript theme={null}
interface ServiceRequestReceivedEvent {
  type: 'service_request_received';
  request_id: string;
  service_id: string;
  method: string;
  body: string;
  sender: string;
}
```

### service\_response\_received

```typescript theme={null}
interface ServiceResponseReceivedEvent {
  type: 'service_response_received';
  request_id: string;
  service_id: string;
  status: string;
  body: string;
  provider_peer_id: string;
}
```

## DORS Events

```typescript theme={null}
interface DorsScoreUpdatedEvent {
  type: 'dors_score_updated';
  scores: Array<[string, number]>;   // [transport, score] pairs
}

interface DorsTransportSelectedEvent {
  type: 'dors_transport_selected';
  from: string | null;
  transport: string;
  reason_code: DorsReasonCode;
  score?: number;
}

interface DorsTransportSwitchedEvent {
  type: 'dors_transport_switched';
  from: string | null;
  to: string;
  reason_code: DorsReasonCode;
  reason_detail?: string;
}

interface DorsEscalationTriggeredEvent {
  type: 'dors_escalation_triggered';
  phase: 'TRIGGERED' | 'APPLIED';
  from: string;                      // not nullable here
  to: string;
  reason_code: DorsEscalationReasonCode;
  reason_detail?: string;
}

type DorsReasonCode =
  | 'INITIAL_SELECTION'
  | 'PRIMARY_SELECTED'
  | 'PRIMARY_SUCCESS'
  | 'FALLBACK_SUCCESS'
  | 'ESCALATION_APPLIED'
  | 'CURRENT_UNAVAILABLE';

type DorsEscalationReasonCode =
  | 'FALLBACK_SUCCESS'
  | 'RETRY_THRESHOLD'
  | 'POOR_SIGNAL'
  | 'CONGESTION'
  | 'LOW_TTL'
  | 'LOW_SUCCESS_RATE';
```

`phase: 'TRIGGERED'` is a recommendation; `'APPLIED'` means the fallback succeeded.

## Resource Pressure Events

### ack\_evicted

```typescript theme={null}
interface AckEvictedEvent {
  type: 'ack_evicted';
  message_id: string;
  priority: string;
  reason: string;
}
```

### fragment\_assembly\_evicted

```typescript theme={null}
interface FragmentAssemblyEvictedEvent {
  type: 'fragment_assembly_evicted';
  message_id: string;
  completion_percent: number;   // 0-100 when evicted
  reason: string;
}
```

## Diagnostic Events

### diagnostic

```typescript theme={null}
interface DiagnosticEvent {
  type: 'diagnostic';
  level: 'info' | 'warning' | 'error';
  message: string;
  context?: Record<string, unknown>;
}
```

<Card title="Next: Types & Internals" icon="arrow-right" href="/docs/mesh-sdk/types-and-internals">
  Type definitions, DORS scoring, mesh architecture, and troubleshooting.
</Card>
