logs.gokuls.in

3 pull requests merged across 1 repo

Offline-Protocol/offline-protocol-sdk

0.28.0 merged in #509 (d3aed186) but was never tagged: no v0.28.0 tag, nothing on npm, crates.io or PyPI. Since then #516, #515, #517 and #518 landed, and #515/#516 opened a new [Unreleased] above [0.28.0].

This folds them into 0.28.0 instead of cutting 0.28.1 over a number that never shipped:

  • CHANGELOG: the five [Unreleased] entries move to the top of 0.28.0's ### Fixed and ### Added; no empty [Unreleased] left. The summary blockquote gains the Android Hello / 15 s neighbor_lost sentences and a paragraph for the dense-cluster forwarding fix, the probe outbox fix and native_event_gap.
  • UPGRADING §26: Android neighbor_lost timing, and the new native_event_gap diagnostic.
  • No version file changes: every manifest already reads 0.28.0. scripts/generate-third-party-notices.sh produces no diff.
  • #517 (example apps only) and #518 (a fix to unreleased #516 behaviour) need no entry of their own.

After merge: push v0.28.0-rc.1 first (the #464 staged-publish fix has never run on a real tag), then v0.28.0, then the Swift pull workflow.

, #512 and #513. Refs #514 (instrumentation; no cause has been found yet).

One commit per issue, plus one that tightens two of the new source guards.

#510: a message could die inside a dense cluster

A forwarder that got a second copy of a frame while its own forward waited cancelled the forward. The reasoning was that a neighbour's transmission had covered the same ground. It had not: every carrier hands a frame to three chosen neighbours rather than broadcasting it. So the one device next to the way out could stand down, and the frame then died with its id suppressed on every cluster member, where the sender's retries could not rescue it.

Now a duplicate takes only its sender off the forward's fan-out (PendingRelay::heard_from). A forward is dropped only when every neighbour it could go to has handed it a copy. The is_last_hop exception is no longer needed and has been removed.

  • everyone_hearing_everyone_does_not_multiply_the_traffic: 4/200 → 0/200 failures locally.
  • New a_device_hearing_a_frame_from_every_side_still_carries_it_onward is deterministic (no hash tie-break involved). It fails every run under the old rule (checked by mutation).
  • Worst-case cost is unchanged: one fan-out per device, inside the same budgets. A simulation of the test's exact lock-step semantics gave 2.46% failures before (matching the measured ~2%) and 0 in 200,000 after, with at most 19 transmissions against the test's bound of 21.

#513: a peer with Bluetooth off stayed a neighbour for 60+ s

Two disconnect paths left the peer listed:

  • Server-side clean disconnects (status 0/19) started nothing, so a server-only peer stayed listed indefinitely.
  • Client links retried on any status with a 5→60 s backoff, about 135 s before giving up.

Now both paths start a 15 s grace per peer once no link to it is up. A returning link cancels it. On expiry the facade does the teardown a non-clean disconnect does, for every address the peer used. Reconnects carry on behind it.

The registry now tracks *established* client links: a dial in flight registers a gatt for the whole connect timeout, and must not count as a link.

#512: writes from an unmapped central waited on a dial-back (up to ~40 s)

Nothing on BLE proved the central to the peripheral. New optional GATT characteristic Hello (6E400006-…):

  • Android central: writes the value it already serves on Identity, with response, after the CCCD ack and before link-ready, so it lands before any Message write. If it can't (no characteristic, MTU too small, identity not primed, refused write, no callback within 3 s), the link goes ready as before.
  • Server: bounds the write, verifies it with the one verifier, and binds the writer's address to the derived address only. It flushes the per-peer MTU before announcing, and announces only if no other link already has.
  • Peers without Hello (iPhones included): resolved exactly as today.

Documented in docs/spec/ble-framing.md and threat model R16.

#514: rare missing message_received / message_delivered

Not reproduced, so this PR makes the next occurrence attributable rather than fixing anything. Both native modules now number every event handed to JS (seq, given out and emitted under one lock). The TS layer emits a diagnostic event (native_event_gap, with the missing range) when a number is skipped. A gap means React Native lost the event; no gap and no event means it was never produced. Older native modules (no seq) and JS reloads report nothing.

Verification

  • cargo fmt --check, cargo clippy --workspace -D warnings, cargo test --workspace --lib and cargo doc -D warnings are clean. mesh_forwarding passes 18/18.
  • Android JVM suite: 626/626 (CI harness, copied to a clean directory). New tests: CentralGattClientHelloTest (4), StaleAddressDisconnectTest (+2), StaleAddressRegistryTest (+1), PeripheralGattServerTest (+2).
  • iOS: OfflineProtocolModule.swift typechecks with the React header recipe in BRIDGE_MAINTENANCE.md. A planted error is reported, so the run is not vacuous.
  • New Rust source guards: peer-lost grace wiring, hello UUID and ordering, and the seq field name. Each was mutation-checked: removing the clean-branch schedule, the stop-time cancel, the runnable's re-check, or the MTU-before-announce order fails a guard.
  • tsc --noEmit is clean.

Not done / owed

  • No device run. #512 and #513 change Android BLE behaviour and need the two-phone soak from the issues (p95 latency, and the Bluetooth-toggle case) before release.
  • #510 follow-up: narrowing is deterministic in a fully-meshed cluster but not in every graph. Letting a retransmission past the ACK timeout re-enter forwarding as a new relay generation would make delivery eventual everywhere; that is a wire change and deserves its own issue.
  • iOS: does not write or serve Hello yet.
  • Swift package / Android library assembly jobs were not run locally.

What

The Android library publishes as com.offlineprotocol:offline-protocol-sdk instead of com.offlineprotocol:offline-protocol-android. That matches the offline-protocol-sdk name on PyPI.

Nothing has been published to Maven Central yet (MAVEN_CENTRAL_PUBLISH is not set), so no consumer depends on the old artifact id. This has to merge before the first Maven Central rehearsal, because the Portal validates the deployment under its artifact id.

Changed

  • artifactId in bindings/kotlin/offline-protocol-android/build.gradle.kts
  • the consumer check's dependency (bindings/kotlin/consumer-check/app/build.gradle.kts)
  • the staged-repository paths to the AAR and sources jar in ci.yml and release.yml
  • scripts/maven-central-bundle.sh, scripts/maven-central-upload.sh and scripts/tests/test-maven-central.sh
  • bindings/kotlin/README.md, CONTRIBUTING.md and the two unreleased CHANGELOG.md entries

Not changed

The Gradle module :offline-protocol-android and the bindings/kotlin/offline-protocol-android/ directory keep their names. Consumers never see either, and guards read the library by that path.

Verification

  • python3 -B -m unittest scripts/tests/test_check_android_aar.py: 43 passed locally.
  • Gradle and gpg are not installed on the machine this was written on. Two CI checks carry the rest: the Android Library job publishes to the staging repository and builds the consumer check against the new coordinates, so a missed reference fails there. scripts/tests/test-maven-central.sh tests the bundle and upload scripts.