Conversation
A peer's key package is valid from an hour before it was minted, judged by the receiver's clock. Between two devices whose clocks disagree by more than that, no session forms and messages and connection requests stay pending forever, and the only trace was a debug line. Two Android phones that had never been online (one set to February 2024, one to October 2025) reproduced it during the v0.28 smoke test. - MlsError::KeyPackageOutsideValidityWindow, from OpenMLS's KeyPackageVerifyError::InvalidLifetime, on every route that admits a peer's package (import, cache read, add_group_member) through one helper. - SecurityWarningCode::KeyPackageOutsideValidityWindow (KEY_PACKAGE_OUTSIDE_VALIDITY_WINDOW), raised by establish_secure_session once per peer through the control-gate throttle: the package has not proved its sender when its window is checked. - The window itself is unchanged.
kivtxs
force-pushed
the
chore/release-0.28.0
branch
from
October 5, 2026 23:22
129b946 to
b10b3ce
Compare
…droid drops dead links when Bluetooth goes off Found in the v0.28 device test: two Android phones a metre apart, both advertising the mesh service (a Mac scan saw both), never connected over Bluetooth for fifteen minutes. - Density counted every advert in range as mesh density. The estimate read 32 in a house (televisions, earbuds, watches), so the dense-mesh filters passed over 44% of peers, chosen by a hash of the address alone: each phone's address hashed under the line on the other (0.091 and 0.284), so each was passed over on every advert. Density now counts distinct mesh candidates (devices the discovery gate admits) in the last five seconds, and the pass-over is drawn per address per one-minute slot, mixed with the murmur3 finalizer so consecutive slots are independent draws. The unknown-device bootstrap keeps the all-advert count, which is what it is about. Same change on iOS, where the hash was Swift's per-process seed; both platforms compute the same bucket for the same id (pinned by tests on both sides). - Android: Bluetooth switched off under a running transport left every link in place, since the stack delivers no disconnect for most of them. The dead links counted against the connection cap, kept peers mapped to old addresses, and held GATT client registrations the stack had forgotten. The transport now reports each identified peer lost and clears the link state when it first sees the adapter off, mirroring iOS's dropLinksAfterRadioLoss, including the non-mesh cache that had marked a peer probed mid-recovery as not a mesh device for five minutes. BleDensityPolicy on both platforms (9 JVM tests, 8 Swift tests), a registry test, and the iOS restoration guard updated for the new call.
kivtxs
force-pushed
the
chore/release-0.28.0
branch
from
October 6, 2026 01:45
b10b3ce to
21f5278
Compare
…uding a stack crash Found on the phones while validating the previous commit: the Infinix's Bluetooth stack crashed (SIGSEGV in libbluetooth_jni.so, connection_manager::on_connection_complete) as the app dialled the other phone. The stack restarted in under a second and took the app's GATT server, advertiser and pending connect with it. The transport polls the adapter once a minute from the scan watchdog, which read "enabled" throughout, so nothing was rebuilt: the pending connect pinned the peer's address as "connecting", and the other phone could not verify this one until the app restarted. Android reports a crash as an ordinary ON -> TURNING_OFF broadcast. The facade now registers for ACTION_STATE_CHANGED on the BLE handler once it runs, and unregisters behind the shutdown barrier: - TURNING_OFF / OFF: drop the dead links (once per outage), follow the dead scan locally, report BLE unavailable, arm recovery. - ON after an outage: run the recovery now (scan, GATT server, advertising) instead of waiting for the ladder's next rung (12 to 29 s measured). AdapterStateTransition maps the states (3 JVM tests); a Rust guard pins the registration after RUNNING, the unregistration after the barrier, and the radio-lost arm dropping links.
kivtxs
force-pushed
the
chore/release-0.28.0
branch
from
October 6, 2026 02:28
21f5278 to
5720309
Compare
…a system dialog Closes the residual #465 left open ("the Android manager does not form a group itself"). With wifiDirect.autoAccept on Android 10+: - Devices of one app find each other over Wi-Fi P2P DNS-SD: the stream chapter's _offlineprotocol._tcp record (txtvers, addr) plus `app` (a tag of the app id) and `net` (the group name, while owning one). - The lowest address creates a group under a name derived from its address; the others join it with a passphrase derived from the app id. Joining by credentials shows no dialog on either phone, unlike connect() by device address, whose invitation a phone in a pocket never answers. - The query names the instance, because a query by type returns only the PTR record and never reaches the TXT listener. A group owner answers no service discovery query, so joiners target the name the lowest peer will create, and an owner whose group stays empty for 45 s dissolves it so it can be found again. The rules are a pure object (WifiDirectGroupFormation) with 20 JVM tests. Off by default; groupOwnerIntent stays unused. Measured on an Android 13 and an Android 15 phone: launch to proved stream in 26-55 s over five clean starts, chat 3/3 and 4/4 each way at ~100 ms.
Turning Wi-Fi P2P off reported the slot down; turning it on reported nothing, restarted no discovery and, with group formation on, left the framework's dropped service request and record unregistered, so every later discoverServices failed with NO_SERVICE_REQUESTS. An app started with Wi-Fi off never got Wi-Fi Direct. The manager tracks whether it last reported the layer up, reports it up again when P2P returns (a repeated broadcast is a no-op), restarts peer discovery, re-registers formation's service request and record, and re-registers the request on NO_SERVICE_REQUESTS. Formation pauses while the layer is down instead of failing BUSY. On the phones: app started with Wi-Fi off, then Wi-Fi on: group formed and streams proved in 73 s; Wi-Fi off and on mid-session on one phone: re-formed in 19 s.
…tials The first formation design named the group after its owner, so a joiner had to hear the owner's DNS-SD record before it could join. On two phones that failed: a device that owns or is joining a group answers no service discovery query, and the two often heard each other minutes apart or never. Of nine clean starts, two never formed and two took about two minutes. - The group's name and passphrase derive from the app id alone, so a joiner needs nothing from the owner. The `net` record entry is gone. - The lowest address heard creates; others join, and take over creating after three joins found no group (the lower peer may never hear them). - A device that heard no record joins only as a paced probe (once a minute) when a group owner is nearby: Wi-Fi Direct televisions are owners too, and joining on every step kept both phones deaf. - A join attempt is cancelled after 15 s (it must scan, and the supplicant retries a rejected association) and attempts are 30 s apart, so a joiner stays discoverable half the time. - An owner whose group stays empty for a randomised 30-60 s dissolves it and joins twice before it may create again, so groups formed at once merge. Eight clean starts on an Android 13 and an Android 15 phone next to two Wi-Fi Direct televisions: all formed, 18-225 s, median about a minute, no system dialog. 18 JVM tests cover the rules, including the asymmetric cases.
kivtxs
force-pushed
the
chore/release-0.28.0
branch
from
October 6, 2026 04:38
5720309 to
dcb5e2f
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The 0.28.0 cut, following CONTRIBUTING's "Cutting a Release". It touches the same file set as #460 (0.27.0).
Merge order
Merge this last. It renames
## [Unreleased]to## [0.28.0] — 2026-10-06, so anything that lands after it would be described under a release it isn't in.Already on
main: #504 (Android library artifact id) and #505 (Android Wi-Fi Direct fixes). #508 (demo app) was closed. Still to merge, in any order:mainafter fix(bindings): Android Wi-Fi Direct sends at once, rejoins its group, and starts from the config #505's squash, so it shows only its own three commits);This branch currently stacks the remaining three (
main+ merges of each + the release commit), so its CI tests exactly the tree 0.28.0 would ship. You can also merge it alone: it carries their commits unchanged.Then rebase this branch; I'll do it if you'd like. The only expected conflict is the changelog heading line. Update the date to the day you tag.
docs/UPGRADING.md§26 already describes #505, #506, #507 and #511's change toneighbor_lost. If any of them doesn't make 0.28.0, drop its paragraph; I can do that too.What changed
Cargo.toml:[workspace.package]plus the 11 internal deps;offline-protocol-sealedandoffline-protocol-leaf;Cargo.lock,tools/embedded-footprint/Cargo.lock,tools/mls-interop/Cargo.lock(refreshed withcargo metadata; only our own crates moved);bindings/python/pyproject.toml;bindings/react-native/package.jsonand its lockfile (npm version 0.28.0 --no-git-tag-version).THIRD-PARTY-NOTICES.md(×3): regenerated withscripts/generate-third-party-notices.shand cargo-about 0.9.1, the pinned version.CHANGELOG.md: Unreleased becomes 0.28.0. The 0.27.0 section moves to the newdocs/changelog/0.27.md, with its title and release table, and a row in both archive tables.](docs/…)becomes](../…)(four of them).HEAD:CHANGELOG.mdand applying the same rewrite gives the archived text exactly, apart from one trailing blank line.SECURITY.md: the current line is0.28.x; earlier lines are≤ 0.27.x.docs/UPGRADING.md:v0.28.x;InternetManagernow requiresapp_id=;Verification
cargo test -p offline-protocol-core -p offline-protocol-leaf --lib local_dep_versions(the version guards): pass.cargo check --workspace --locked: clean.cargo test -p offline-protocol-uniffi --lib(the bridge guards): 189 passed.](path#anchor)in every tracked file, resolved against real headings. No new breakage; the only two flagged are the illustrative](docs/…)and](path#anchor)in CONTRIBUTING itself.Device validation of this tree (two Android phones, Infinix NOTE 12 on Android 13 and Seeker on Android 15)
neighbor_lostper phone; relinked in 7, 21, 4 s (never, before)be01882a)neighbor_lostper phone when nothing is leftKEY_PACKAGE_OUTSIDE_VALIDITY_WINDOWonce per phone; demo alert shown; messaging after the clock is fixedping.v1discovered, pong receivedSeen and not fixed in 0.28.0:
message_receivedin the receiver's JS log, and once a receipt was missing in a 40-round soak. Not reproduced in the 86 messages after native-side event logging was added; I'll keep watching.After merge (from CONTRIBUTING)
v0.28.0-rc.1, or aworkflow_dispatchwithdry_run: true, version: 0.28.0. That checks npm OIDC before anything is permanent.v0.28.0.gh workflow run publish.yml -R Offline-Protocol/offline-protocol-swift -f version=0.28.0. This is the first release with a Swift package.MAVEN_CENTRAL_PUBLISH,PYPI_PUBLISH), unchanged here.For the release run
One flaky test appeared in CI today:
mesh_forwarding::everyone_hearing_everyone_does_not_multiply_the_traffic(#510, about 1.5%). It failed on #508, a demo-only change, and on an earlier run on main (36897622339); the failure is non-delivery (far's inbox empty). A flake in the tag's run means a re-run, not a new patch version, but it's worth knowing before you push the tag.