iOS Agent — Background Execution & Compute Modes
iOS workers can contribute compute in three modes with full background execution and Bluetooth-local fallback. This page documents the compute mode system, background task scheduling, power-aware policy, and Bluetooth Local transport.
Compute modes
| Mode | Description | Rewards |
|---|---|---|
| Off | App is idle; no compute offered. | None |
| On | Connects to the internet swarm via coordinator heartbeat loop. | Yes |
| Bluetooth Local | Advertises compute over CoreBluetooth to nearby Mac nodes; no internet required. | No |
Mode is controlled via the three-way picker in the Swarm tab (ComputeMode enum). The selected mode is persisted to UserDefaults and restored on next launch.
Automatic mode downgrade
When the device is in On mode and the network path becomes unsatisfied (Wi-Fi drops, airplane mode), NWPathMonitor automatically switches the agent to Bluetooth Local mode and logs an event. When internet returns, the app notifies the user but does not auto-escalate back — the user must switch back to On manually to avoid silently re-enrolling in the rewards network.
Background execution
UIBackgroundModes declared
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>BGTaskScheduler tasks
| Task ID | Type | Purpose |
|---|---|---|
io.edgecoder.ios.runtime | BGProcessingTask | Long-running compute burst; keeps heartbeat loop alive while backgrounded. Re-schedules itself on expiry for continuous coverage. |
io.edgecoder.ios.heartbeat | BGAppRefreshTask | Short-lived 15-minute wake for a single heartbeat when the processing task is not running. |
Both tasks are registered in AppDelegate.application(_:didFinishLaunchingWithOptions:). Background tasks are scheduled automatically when the app enters the background (via UIApplication.didEnterBackgroundNotification) if computeMode != .off.
Scheduling flow
App → Background
└─ if computeMode != .off:
BGProcessingTaskRequest(id: "io.edgecoder.ios.runtime", requiresNetworkConnectivity: false)
BGAppRefreshTaskRequest(id: "io.edgecoder.ios.heartbeat", earliestBeginDate: +15min)
iOS grants BGProcessingTask:
└─ sendHeartbeatIfNeeded() every 15s
└─ Re-schedules itself before expiry
└─ task.setTaskCompleted(success:) on cancellation
iOS grants BGAppRefreshTask:
└─ One sendHeartbeatIfNeeded() call
└─ Re-schedules next BGAppRefreshTaskEntitlements required
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>io.edgecoder.ios.runtime</string>
<string>io.edgecoder.ios.heartbeat</string>
</array>
<key>com.apple.developer.bluetooth-always-authorization</key>
<true/>Power-aware scheduling
| Signal | Meaning |
|---|---|
onExternalPower | Charging / external power connected |
batteryLevelPct | Battery percentage (0–100) |
lowPowerMode | iOS Low Power Mode active |
These are reported to the coordinator in every heartbeat's powerTelemetry payload. The runOnlyWhileCharging toggle (persisted in UserDefaults) gates the start() call: if true and not on external power, the runtime refuses to start with a status message.
Assignment decision
Bluetooth Local transport
When in Bluetooth Local mode, BluetoothTransport makes the iPhone act as a CBPeripheralManager (BLE server):
BLE service layout
| UUID suffix | Characteristic | Properties | Role |
|---|---|---|---|
...0002 | requestChar | write / writeWithoutResponse | Mac sends inference request JSON |
...0003 | responseChar | notify / read | Phone sends inference result back |
...0004 | statusChar | notify / read | Phone pushes modelState + batteryPct |
Service UUID: EC0D-0001-EC0D-EC0D-EC0D-EC0D0001EC0D
Wire protocol (JSON over BLE)
Request (Mac → Phone, written to requestChar):
{ "id": "<uuid>", "prompt": "<text>", "maxTokens": 512 }Response (Phone → Mac, notified on responseChar):
{ "id": "<uuid>", "output": "<text>", "ok": true, "durationMs": 450 }Status (Phone → Mac, notified on statusChar):
{ "modelState": "ready", "batteryPct": 87 }Responses larger than 512 bytes are chunked and sent sequentially with 10ms pauses between chunks.
Mac-side integration
The Mac side uses edgecoder-ble-proxy — a Swift CBCentralManager companion binary that:
- Scans for peripherals advertising the EdgeCoder BLE service UUID.
- Connects, subscribes to
responseCharandstatusChar. - Exposes a local HTTP server on
127.0.0.1:11435:GET /status→ BLE connection state, device name, battery, model state.POST /api/generate→{ prompt, maxTokens }→ forwards over BLE, returns result.
The IDE provider server (port 4304) starts this binary automatically and registers its status URL with IntelligentRouter so the routing waterfall (bluetooth-local → ollama-local → swarm → edgecoder-local) works transparently.
Building the proxy:
npm run build:ble-proxy
# Compiles src/bluetooth/swift-ble-proxy/ and installs to /opt/edgecoder/bin/Manual test:
/opt/edgecoder/bin/edgecoder-ble-proxy --port 11435 &
curl http://127.0.0.1:11435/statusIDE task tracking (iOS app)
When the phone is in Bluetooth Local mode and a Mac IDE submits a task, it appears in the IDE tab of the iOS app in real-time:
- Running — spinner while inference runs on the phone
- Success — green check, output viewable in detail sheet
- Failed — red cross with error
The IDE tab badge shows the count of in-progress tasks. Tapping any entry opens a full detail view with prompt, generated output, duration, and timestamp.
Operator practices
- Keep iOS fleet policy conservative by default (low battery threshold,
runOnlyWhileChargingfor intensive models). - Track assignment drop reasons to understand power-policy impact.
- Use separate worker pools for mobile vs desktop if reliability goals diverge.
- Bluetooth Local mode is unrewarded — communicate this clearly to users.
- The
edgecoder-ble-proxybinary requires Bluetooth access via System Settings → Privacy & Security → Bluetooth on the Mac.