Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 74 additions & 71 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,75 +11,6 @@ This file holds unreleased changes and the current release. Older releases are
archived by series under [docs/changelog/](docs/changelog/); see the
[archive index](docs/changelog/README.md).

## [Unreleased]

### Fixed

- **Confirmation probes no longer crowd real messages out of the outbox.** A
session the peer never confirms is probed every five seconds, and each probe
used to enter the outbox with its own retry ladder on top of the unanswered
ones before it. Only a relay verdict backed that off, so a mesh-only link
never did. An account with ten such contacts stacked about two probes a
second, and once the outbox hit its 500-entry cap, eviction failed the user's
own messages with `Outbox capacity exceeded`. Seen on a device test between
an Android and an iPhone, offline over BLE. Each new probe now supersedes
the last, quietly and without counting against the carrier, whether the
periodic scan or the Welcome fast path sent it. Probes are no longer written
to storage, and those an older build wrote are dropped at restore. A peer
holds one probe in the outbox and is still probed on the same cadence.

- **A message carried through a dense cluster no longer dies inside it**
([#510](https://github.com/Offline-Protocol/offline-protocol-sdk/issues/510)).
A forwarder that received a second copy of a frame while its own forward
waited cancelled the forward, on the assumption that a neighbor's
transmission had covered the same ground. It had not: every carrier hands a
frame to a few chosen neighbors rather than broadcasting it. In a cluster
where everyone hears everyone, the one device beside the way out could
stand down after two copies reached it from neighbors that never chose that
way, and the frame died with its id suppressed on every member, so the
sender's retries could not rescue it. A copy now takes only the neighbor
that sent it off the forward's fan-out, and a forward is dropped only when
every neighbor it could go to has handed it a copy. The
`mesh_forwarding::everyone_hearing_everyone_does_not_multiply_the_traffic`
flake (about 2% of runs) went from 4 failures in 200 runs to none. The
worst-case cost is unchanged: still at most one fan-out per device, inside
the same per-second budgets.
- **Android reports a Bluetooth peer lost once no link to it is left**
([#513](https://github.com/Offline-Protocol/offline-protocol-sdk/issues/513)).
When the other phone switched Bluetooth off, its links closed cleanly, and
this phone kept it a neighbour until the reconnect backoff gave up (over a
minute in the device test, about two in the worst case) or, for a peer
reached only through this phone's GATT server, until the transport stopped.
`neighbor_lost` now fires 15 seconds after the last link to the peer goes
down, unless one comes back first. Reconnect attempts continue during those
15 seconds; once the peer is reported lost it returns through discovery and
is announced again, as after any other loss. A dial still in flight does
not count as a link.
- **Android Bluetooth messages no longer wait on a dial back to the sender**
([#512](https://github.com/Offline-Protocol/offline-protocol-sdk/issues/512)).
A peer's writes reach this phone's GATT server from the peer's central-role
address. When nothing had mapped that address yet, the server queued the
fragments and dialled the address to read the peer's identity, and since
that address does not advertise, the dial took from seconds to about 40 s
(11 s at p95 in a 30-round soak, 40 s after a Bluetooth toggle). An Android
central now writes its identity assertion to a new optional GATT
characteristic, Hello (`6E400006-…`), before its first message write, and
the server binds the address it proves with the same verifier the Identity
read uses. A peer without the characteristic, an iPhone among them, is
resolved as before. [BLE framing](docs/spec/ble-framing.md) specifies it.

### Added

- **An event lost between the native module and JavaScript is reported**
(refs [#514](https://github.com/Offline-Protocol/offline-protocol-sdk/issues/514)).
Both native modules number every event they hand to JavaScript, and the SDK
emits a `diagnostic` event (`message: "native_event_gap"`, with the missing
range) when a number is skipped. A bridgeless React Native emit can fail
without the native side seeing it, and three device-test gaps (a receipt
with no `message_received`, a delivery with no `message_delivered`) could
not be placed in the core or the bridge. This does not recover a lost
event; it says which side lost it.

## [0.28.0] — 2026-10-06

> **The peer-stream slot carries traffic on phones.** The mobile managers
Expand All @@ -93,8 +24,10 @@ archived by series under [docs/changelog/](docs/changelog/); see the
> **Bluetooth LE holds up where it used to give out.** Two phones in a room
> full of other Bluetooth devices find each other, an iPhone's Bluetooth
> power-cycle no longer strands either end, and Android notices Bluetooth
> switched off or a stack crash and relinks. `neighbor_lost` fires once, and
> only when nothing nearby reaches the peer.
> switched off or a stack crash and relinks. An Android phone takes a peer's
> first write at once instead of dialling the peer back to learn who it is.
> `neighbor_lost` fires once, and only when nothing nearby reaches the peer;
> on Android, 15 seconds after the last link closes.
>
> **The SDK ships outside React Native.** A release now builds, tests and
> publishes a Swift package, an Android library
Expand All @@ -108,6 +41,13 @@ archived by series under [docs/changelog/](docs/changelog/); see the
> `KEY_PACKAGE_OUTSIDE_VALIDITY_WINDOW` instead of leaving messages queued
> with nothing to say why.
>
> **Messages are not lost on the way.** A frame forwarded through a cluster
> where every device hears every other no longer dies inside it, and a contact
> that never confirms its session no longer fills the outbox with probes until
> the user's own messages are evicted. React Native reports an event lost
> between the native module and JavaScript as a `native_event_gap`
> diagnostic.
>
> **Breaking, narrowly:** Python's `InternetManager` requires `app_id=`, and
> an exhaustive TypeScript `switch` over `SecurityWarningCode` needs the new
> member. At run time, an iPhone on 0.28 does not see one on 0.27 or earlier
Expand All @@ -117,6 +57,16 @@ archived by series under [docs/changelog/](docs/changelog/); see the

### Added

- **An event lost between the native module and JavaScript is reported**
(refs [#514](https://github.com/Offline-Protocol/offline-protocol-sdk/issues/514)).
Both native modules number every event they hand to JavaScript, and the SDK
emits a `diagnostic` event (`message: "native_event_gap"`, with the missing
range) when a number is skipped. A bridgeless React Native emit can fail
without the native side seeing it, and three device-test gaps (a receipt
with no `message_received`, a delivery with no `message_delivered`) could
not be placed in the core or the bridge. This does not recover a lost
event; it says which side lost it.

- **Android forms its Wi-Fi Direct group itself.** With
`wifiDirect: { enabled: true, autoAccept: true }` on Android 10 and later,
devices of the same app find each other over Wi-Fi P2P service discovery
Expand Down Expand Up @@ -482,6 +432,59 @@ archived by series under [docs/changelog/](docs/changelog/); see the

### Fixed

- **Confirmation probes no longer crowd real messages out of the outbox.** A
session the peer never confirms is probed every five seconds, and each probe
used to enter the outbox with its own retry ladder on top of the unanswered
ones before it. Only a relay verdict backed that off, so a mesh-only link
never did. An account with ten such contacts stacked about two probes a
second, and once the outbox hit its 500-entry cap, eviction failed the user's
own messages with `Outbox capacity exceeded`. Seen on a device test between
an Android and an iPhone, offline over BLE. Each new probe now supersedes
the last, quietly and without counting against the carrier, whether the
periodic scan or the Welcome fast path sent it. Probes are no longer written
to storage, and those an older build wrote are dropped at restore. A peer
holds one probe in the outbox and is still probed on the same cadence.

- **A message carried through a dense cluster no longer dies inside it**
([#510](https://github.com/Offline-Protocol/offline-protocol-sdk/issues/510)).
A forwarder that received a second copy of a frame while its own forward
waited cancelled the forward, on the assumption that a neighbor's
transmission had covered the same ground. It had not: every carrier hands a
frame to a few chosen neighbors rather than broadcasting it. In a cluster
where everyone hears everyone, the one device beside the way out could
stand down after two copies reached it from neighbors that never chose that
way, and the frame died with its id suppressed on every member, so the
sender's retries could not rescue it. A copy now takes only the neighbor
that sent it off the forward's fan-out, and a forward is dropped only when
every neighbor it could go to has handed it a copy. The
`mesh_forwarding::everyone_hearing_everyone_does_not_multiply_the_traffic`
flake (about 2% of runs) went from 4 failures in 200 runs to none. The
worst-case cost is unchanged: still at most one fan-out per device, inside
the same per-second budgets.
- **Android reports a Bluetooth peer lost once no link to it is left**
([#513](https://github.com/Offline-Protocol/offline-protocol-sdk/issues/513)).
When the other phone switched Bluetooth off, its links closed cleanly, and
this phone kept it a neighbour until the reconnect backoff gave up (over a
minute in the device test, about two in the worst case) or, for a peer
reached only through this phone's GATT server, until the transport stopped.
`neighbor_lost` now fires 15 seconds after the last link to the peer goes
down, unless one comes back first. Reconnect attempts continue during those
15 seconds; once the peer is reported lost it returns through discovery and
is announced again, as after any other loss. A dial still in flight does
not count as a link.
- **Android Bluetooth messages no longer wait on a dial back to the sender**
([#512](https://github.com/Offline-Protocol/offline-protocol-sdk/issues/512)).
A peer's writes reach this phone's GATT server from the peer's central-role
address. When nothing had mapped that address yet, the server queued the
fragments and dialled the address to read the peer's identity, and since
that address does not advertise, the dial took from seconds to about 40 s
(11 s at p95 in a 30-round soak, 40 s after a Bluetooth toggle). An Android
central now writes its identity assertion to a new optional GATT
characteristic, Hello (`6E400006-…`), before its first message write, and
the server binds the address it proves with the same verifier the Identity
read uses. A peer without the characteristic, an iPhone among them, is
resolved as before. [BLE framing](docs/spec/ble-framing.md) specifies it.

- **A key package refused by this device's clock is reported.** A peer's key
package is valid from an hour before it was minted until 30 days after,
judged by the receiver's clock. A device more than an hour behind a peer
Expand Down
9 changes: 8 additions & 1 deletion docs/UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2466,7 +2466,14 @@ still linked over the other was reported lost. An app that removed a peer on
`neighbor_lost` now keeps a reachable one; one that counted the events now
sees one per departure. Bluetooth switched off, on this phone or by a stack
restart, ends every Bluetooth link at once, so it then fires for each peer
nothing else reaches; Android used to report none of them.
nothing else reaches; Android used to report none of them. On Android it fires
15 seconds after the last link closes, unless one comes back first, rather
than when the reconnect backoff gives up (a minute or more).

**React Native: a new `diagnostic` message.** `diagnostic` events with
`message: "native_event_gap"` report an event the native module sent that
never reached JavaScript. An app that treats every diagnostic as an error will
now see these; the event itself is not recovered.

**`transport_switched` to Wi-Fi Direct follows a peer, not the layer.** It
fires when the first Wi-Fi Direct link proves and, to `None`, when the last
Expand Down
Loading