How BitChat Fragment Recovery Works for Stalled BLE Re‑assemblies

BitChat detects stalled broadcast fragment streams by scanning BLEFragmentAssemblyBuffer timestamps, encodes missing fragment IDs into a REQUEST_SYNC packet filter, and solicits retransmission from peers who match the 8‑byte stream identifiers.

BitChat fragments large BLE payloads into smaller packets for broadcast transmission. When packet loss or sender pauses interrupt a stream, the BitChat fragment recovery mechanism automatically identifies the stall and coordinates peer‑assisted reconstruction. This article dissects the three‑stage recovery pipeline implemented in the permissionlesstech/bitchat repository.

Stage 1: Detecting Stalled Broadcast Streams

Detection begins in BLEFragmentAssemblyBuffer.swift, which maintains per‑stream metadata including lastFragmentAt timestamps. The method stalledBroadcastFragmentIDs(stalledAfter:retryAfter:now:) implements the stall detection logic in [BLEFragmentAssemblyBuffer.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift#L170-L199).

The scanner applies three filters to active assemblies:

  • Incomplete streams – fragments.count < total indicates missing pieces
  • Stall threshold – No new fragment received for stalledAfter seconds (configured via TransportConfig.bleFragmentResyncStallSeconds)
  • Rate limiting – lastResyncRequestAt plus retryAfter interval prevents duplicate queries

The method returns up to RequestSyncPacket.maxFragmentIdFilterCount (60) fragment‑ID Data values, marking selected streams with a fresh lastResyncRequestAt timestamp.

Stage 2: Sending Targeted REQUEST_SYNC Packets

During its periodic maintenance cycle, BLEService.performCleanup invokes the detection method and forwards results to the sync layer. In [BLEService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift#L7143-L7151), the service passes stalled IDs to GossipSyncManager.requestMissingFragments.

The manager encodes the IDs using RequestSyncPacket.encodeFragmentIdFilter into the 0x06 fragment‑ID filter TLV field. This creates a compact bit‑filter carried by a REQUEST_SYNC packet broadcast to every connected peer. The wire format respects a 1024‑byte decoder cap, ensuring the filter never exceeds protocol limits.

Stage 3: Peer‑Side Filtering and Retransmission

When a peer receives the request, GossipSyncManager.handleRequestSync in [GossipSyncManager.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipSyncManager.swift#L39-L50) decodes the filter via RequestSyncPacket.decodeFragmentIdFilter ([RequestSyncPacket.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/RequestSyncPacket.swift#L37-L46)).

For each stored fragment packet, the handler checks whether the packet’s first 8 bytes (the stream ID) exists in the decoded filter set. Matching packets are retransmitted with two critical modifications:

  • TTL reset to 0 – Prevents further propagation
  • isRSR flag set to true – Marks the packet as a Requested Solicited Response, distinguishing it from unsolicited broadcasts

This allows the original receiver to accept the missing fragments and resume assembly.

Key Design Constraints

Broadcast‑Only Eligibility Only broadcast fragments qualify for recovery. Directed fragments are stored locally for gossip synchronization; requesting them from peers would never succeed because they exist only on the original recipient device.

Rate Limiting The lastResyncRequestAt timestamp combined with the retryAfter parameter prevents the network from flooding peers with repeated requests for the same stalled stream.

Wire‑Size Limits The implementation caps fragment‑ID filters at 60 entries (maxFragmentIdFilterCount) to stay within the 1024‑byte decoder capacity, ensuring predictable memory usage during packet parsing.

Fast‑Path Execution Detection runs on the BLE service cleanup thread, identifying stalled streams before the next generic GCS fragment round, which reduces recovery latency compared to waiting for periodic full synchronization.

Code Implementation Example

The following Swift patterns illustrate the recovery pipeline:

// 1. Buffer incoming fragments
var buffer = BLEFragmentAssemblyBuffer()
let header = BLEFragmentHeader(packet: incomingPacket)!  
let result = buffer.append(header, maxInFlightAssemblies: 50)

// 2. Periodic cleanup and stall detection
let now = Date()
buffer.removeExpired(before: now.addingTimeInterval(-TransportConfig.bleFragmentLifetimeSeconds))

let stalledIDs = buffer.stalledBroadcastFragmentIDs(
    stalledAfter: TransportConfig.bleFragmentResyncStallSeconds,
    retryAfter:  TransportConfig.bleFragmentResyncRetrySeconds,
    now: now
)

// 3. Request missing fragments from peers
if !stalledIDs.isEmpty {
    gossipSyncManager.requestMissingFragments(fragmentIDs: stalledIDs)
}

When handling requests on the peer side:

// 4. Process REQUEST_SYNC and retransmit matches
if request.types?.contains(.fragment) == true {
    let wantedIDs = RequestSyncPacket.decodeFragmentIdFilter(request.fragmentIdFilter)
    
    for pkt in fragments.allPackets(isFresh: isPacketFresh) {
        guard wantedIDs?.contains(Data(pkt.payload.prefix(8))) ?? true else { continue }
        
        var reply = pkt
        reply.ttl = 0
        reply.isRSR = true  // Mark as solicited response
        delegate?.sendPacket(to: peerID, packet: reply)
    }
}

Summary

  • Detection relies on BLEFragmentAssemblyBuffer.stalledBroadcastFragmentIDs scanning incomplete broadcast streams for timestamp gaps exceeding the stall threshold.
  • Requesting uses GossipSyncManager.requestMissingFragments to encode up to 60 fragment IDs into a REQUEST_SYNC packet’s 0x06 filter.
  • Retransmission occurs when peers match the 8‑byte stream ID prefix against stored fragments, returning them with the isRSR flag set and TTL zeroed.
  • Constraints include broadcast‑only scope, rate limiting via lastResyncRequestAt, and a 1024‑byte wire‑size cap on filters.

Frequently Asked Questions

Why does BitChat only recover broadcast fragments and not directed fragments?

Directed fragments are delivered to specific recipients and stored only on the target device, not gossiped to the broader network. Because peers do not possess directed fragments belonging to other nodes, requesting them would always fail; thus, the protocol restricts recovery to broadcast fragments which all peers cache and can resupply.

How does the protocol prevent spamming peers with repeated recovery requests?

BLEFragmentAssemblyBuffer tracks lastResyncRequestAt timestamps for each stalled stream. The stalledBroadcastFragmentIDs method skips streams that have been queried within the retryAfter interval, enforcing a cooldown period that prevents request flooding while still allowing eventual retries if the stream remains incomplete.

What is the maximum number of fragment IDs that can be requested in a single recovery packet?

A single REQUEST_SYNC packet can carry at most 60 fragment IDs, defined by RequestSyncPacket.maxFragmentIdFilterCount. This limit ensures the encoded filter remains within the 1024‑byte decoder cap and prevents oversized packets that could congest the BLE link layer.

How does a receiver distinguish between a solicited recovery response and a regular broadcast?

Peers set the isRSR (Requested Solicited Response) flag to true when retransmitting fragments in response to a REQUEST_SYNC filter match. The receiving node examines this boolean flag to identify the packet as a targeted recovery response rather than an unsolicited broadcast fragment, allowing proper routing to the assembly buffer.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →