> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.sleepcycle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS SDK

> Advanced sleep analysis capabilities for iOS applications

# Sleep Cycle SDK - iOS Documentation

## Overview

The Sleep Cycle SDK for iOS enables developers to integrate advanced sleep analysis capabilities into their applications. The SDK provides real-time sleep tracking using audio and motion sensors, delivering detailed sleep insights, stage transitions, and detected events throughout the night.

## System Requirements

Minimum iOS Version:

* iOS: 16.0+
* macOS: 13.0+

Swift:

* Swift Version: 5.9+
* The SDK is written in Swift and provides a Swift-native API with async/await support

## Installation

### Swift Package Manager

Add the Sleep Cycle SDK to your project using Swift Package Manager:

1. In Xcode, select **File** → **Add Package Dependencies**
2. Enter the package URL: `https://github.com/MDLabs/sleepcycle-sdk-swift`
3. Select the version you want to use (Semantic Versioning)
4. Add the package to your target

Alternatively, add it to your `Package.swift`:

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/MDLabs/sleepcycle-sdk-swift", from: "1.2.0")
]
```

> The SDK requires an API key for authorization. Contact Sleep Cycle to obtain credentials.

## Prerequisites

### Permissions

The SDK requires microphone access for audio-based sleep analysis. Add the following to your `Info.plist`:

```xml theme={null}
<key>NSMicrophoneUsageDescription</key>
<string>We need access to the microphone to analyze your sleep patterns and detect snoring.</string>
```

For motion-based analysis, you may also need:

```xml theme={null}
<key>NSMotionUsageDescription</key>
<string>We use motion data to track your sleep movements.</string>
```

### Background modes

To ensure continuous sleep analysis throughout the night, enable the appropriate background modes in your app's capabilities:

1. Open your project in Xcode
2. Select your app target
3. Go to "Signing & Capabilities"
4. Add "Background Modes" capability
5. Enable "Audio"

Alternatively, add this to your `Info.plist`:

```xml theme={null}
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>
```

The SDK uses the audio background mode to maintain continuous audio processing during sleep analysis. Your app should also implement proper session management to prevent iOS from suspending the analysis process.

## General

The SDK is thread-safe and uses Swift concurrency (async/await) for all asynchronous operations.

## Initialize the SDK

The SDK requires authentication before use. The initialization process validates your credentials and determines available features.

```swift theme={null}
import SleepCycleSDK

Task {
    do {
        let features = try await SleepCycleSdk.initialize(
            logLevel: .info,
            apiKey: "your-api-key-here"
        )
        print("Authorized with features: \(features)")
    } catch {
        print("Initialization error: \(error)")
    }
}
```

Parameters:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>logLevel</code> <span style={{ opacity: 0.5 }}>LogLevel</span> - SDK log verbosity (<code>.debug</code>, <code>.info</code>, <code>.warning</code>, <code>.error</code>, <code>.noLog</code>)<br />
  <code>logger</code> <span style={{ opacity: 0.5 }}>Logger?</span> - Optional custom logger (defaults to the system logger)<br />
  <code>apiKey</code> <span style={{ opacity: 0.5 }}>String</span> - Your Sleep Cycle API key<br />
  <code>forceTokenRefresh</code> <span style={{ opacity: 0.5 }}>Bool</span> - Force a fresh token fetch even if a cached token exists (defaults to false)
</div>

The returned `SleepAnalysisFeatures` indicates which capabilities are available for your API key:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>sleepStaging</code> <span style={{ opacity: 0.5 }}>Bool</span> - Sleep staging analysis<br />
  <code>smartAlarm</code> <span style={{ opacity: 0.5 }}>Bool</span> - Smart alarm functionality<br />
  <code>audioEvents</code> <span style={{ opacity: 0.5 }}>Bool</span> - Audio event detection<br />
  <code>snoringDetection</code> <span style={{ opacity: 0.5 }}>Bool</span> - Snoring detection<br />
  <code>realTimeSleepStaging</code> <span style={{ opacity: 0.5 }}>Bool</span> - Real-time sleep staging<br />
  <code>multiChannelAnalysis</code> <span style={{ opacity: 0.5 }}>Bool</span> - Multi-channel analysis (stereo, two channels)<br />
  <code>extendedAudioEvents</code> <span style={{ opacity: 0.5 }}>Bool</span> - Extended audio event types beyond the standard set
</div>

Access feature flags at runtime:

```swift theme={null}
if SleepCycleSdk.isFeatureEnabled(\.audioEvents) {
    // Present snore/talk event UI
}
```

## Get the SDK state

Monitor SDK state changes using the AsyncStream:

```swift theme={null}
import SleepCycleSDK

Task {
    for await state in await SleepCycleSdk.stateStream {
        switch state {
        case .uninitialized:
            print("SDK not initialized")
        case .initialized:
            print("SDK ready")
        case .running:
            print("Analysis in progress")
        }
    }
}
```

Get the current state:

```swift theme={null}
let currentState = await SleepCycleSdk.currentState
```

## Start a sleep analysis session

Once initialized, you can start a sleep analysis session. The method returns a `UUID` that identifies the session.

```swift theme={null}
import SleepCycleSDK

Task {
    do {
        let sessionId: UUID = try await SleepCycleSdk.startAnalysis(
            config: SleepAnalysisConfig(
                useAudio: true,
                useAccelerometer: true
            )
        )
        print("Analysis started with session ID: \(sessionId)")
    } catch {
        print("Failed to start analysis: \(error)")
    }
}
```

Parameters:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>config</code> <span style={{ opacity: 0.5 }}>SleepAnalysisConfig</span> - Configuration object specifying which sensors to use<br />
  <code>at</code> <span style={{ opacity: 0.5 }}>Date</span> - The start time for the analysis (defaults to current time)<br />
  <code>using</code> <span style={{ opacity: 0.5 }}>DataSource?</span> - Optional data source (e.g., live or file replay)<br />
  <code>eventListeners</code> <span style={{ opacity: 0.5 }}>\[AudioEventListener]</span> - Optional array of listeners that receive callbacks during audio analysis. Use this to capture audio samples and events in real-time
</div>

## Resume a session

The SDK supports resuming a previously started analysis session. This is useful when your app restarts or the background task is terminated by the system.

```swift theme={null}
if await SleepCycleSdk.isResumePossible() {
    let resumedSessions: [ResumedSession] = try await SleepCycleSdk.resumeAnalysis()
    for session in resumedSessions {
        print("Resumed \(session.sessionId) on channel \(session.channel)")
    }
}
```

`resumeAnalysis()` returns an array of `ResumedSession`, each containing the `sessionId`, `channel`, and any persisted `smartAlarmConfig` for the resumed session.

## Stop a session

To stop an active analysis session and retrieve the results:

```swift theme={null}
Task {
    do {
        let result = try await SleepCycleSdk.stopAnalysis()

        print("Session ID: \(result.sessionId)")

        if let statistics = result.statistics {
            print("Total sleep duration: \(statistics.totalSleepDuration ?? 0)")
            print("Sleep efficiency: \(statistics.sleepEfficiency ?? 0)")

            if let snoreSessions = statistics.snoreSessions {
                for session in snoreSessions {
                    print("Snoring session: \(session.interval)")
                }
            }
        }

        for event in result.events {
            print("\(event.type) detected: \(event.interval), p=\(event.probability)")
        }

        for breathingRate in result.breathingRates {
            print("Breathing rate: \(breathingRate.bpm) bpm at \(breathingRate.timestamp)")
        }

        for stageInterval in result.sleepStageIntervals {
            print("\(stageInterval.stage): \(stageInterval.interval)")
        }

        if let audioStats = result.audioStatistics {
            for interval in audioStats.healthIntervals {
                print("Audio \(interval.status): \(interval.interval)")
            }
        }
    } catch {
        print("Failed to stop analysis: \(error)")
    }
}
```

## Analysis result

The `AnalysisResult` contains the complete output of a sleep analysis session:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>sessionId</code> <span style={{ opacity: 0.5 }}>UUID</span> - Unique session identifier<br />
  <code>startTime</code> <span style={{ opacity: 0.5 }}>Date</span> - Session start time<br />
  <code>endTime</code> <span style={{ opacity: 0.5 }}>Date</span> - Session end time<br />
  <code>timeZone</code> <span style={{ opacity: 0.5 }}>TimeZone</span> - Time zone captured at session start<br />
  <code>events</code> <span style={{ opacity: 0.5 }}>\[Event]</span> - Detected sleep events<br />
  <code>breathingRates</code> <span style={{ opacity: 0.5 }}>\[BreathingRate]</span> - Breathing rate measurements<br />
  <code>sleepStageIntervals</code> <span style={{ opacity: 0.5 }}>\[SleepStageInterval]</span> - Sleep stage data<br />
  <code>realTimeSleepStageIntervals</code> <span style={{ opacity: 0.5 }}>\[SleepStageInterval]</span> - Sleep stages emitted live during the session<br />
  <code>statistics</code> <span style={{ opacity: 0.5 }}>SleepStatistics?</span> - Aggregated sleep statistics (optional)<br />
  <code>audioStatistics</code> <span style={{ opacity: 0.5 }}>AudioStatistics?</span> - Audio health statistics (optional)
</div>

### SleepStatistics

When available, `statistics` contains aggregated metrics about the sleep session:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>totalSleepDuration</code> <span style={{ opacity: 0.5 }}>Double?</span> - Total time spent sleeping<br />
  <code>sleepOnsetLatency</code> <span style={{ opacity: 0.5 }}>Double?</span> - Time to fall asleep<br />
  <code>sleepEfficiency</code> <span style={{ opacity: 0.5 }}>Double?</span> - Ratio of sleep to time in bed (0.0 to 1.0)<br />
  <code>finalWakeTime</code> <span style={{ opacity: 0.5 }}>Date?</span> - Time of final awakening<br />
  <code>numberOfAwakenings</code> <span style={{ opacity: 0.5 }}>Int?</span> - Number of awakenings during the night<br />
  <code>snoreTime</code> <span style={{ opacity: 0.5 }}>Double?</span> - Total time spent snoring<br />
  <code>snoreSessions</code> <span style={{ opacity: 0.5 }}>\[SnoreSession]?</span> - Individual snoring sessions<br />
  <code>sleepStageDurations</code> <span style={{ opacity: 0.5 }}>\[SleepStage: Double]?</span> - Duration per sleep stage
</div>

### AudioStatistics

When available, `audioStatistics` contains information about audio input health throughout the session.

## Sleep score

After a session completes, you can compute a sleep score for the night with `SleepScoring.compute(result:history:dateOfBirth:chronoType:)`. The score combines the night's result with a short history of previous nights.

```swift theme={null}
import SleepCycleSDK

// Build a history entry from each completed result and persist it for future nights
let historyEntry = result.toSleepScoreHistoryEntry()

let score = SleepScoring.compute(
    result: result,
    history: recentHistoryEntries,
    dateOfBirth: userDateOfBirth,   // optional, age-adjusts the quality subscore
    chronoType: userChronoType      // optional, selects the timing subscore window
)

print("Sleep score: \(score.total) (duration=\(score.duration), quality=\(score.quality), routine=\(score.routine))")
```

Each `SleepScore` field is in the range 0.0–1.0, where higher is better:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>total</code> <span style={{ opacity: 0.5 }}>Float</span> - Overall sleep score for the night<br />
  <code>duration</code> <span style={{ opacity: 0.5 }}>Float</span> - How much the user slept<br />
  <code>quality</code> <span style={{ opacity: 0.5 }}>Float</span> - How well the user slept<br />
  <code>routine</code> <span style={{ opacity: 0.5 }}>Float</span> - The user's sleep schedule
</div>

`SleepScoring.identifyChronoType(history:)` derives the user's `ChronoType` (`.extremeMorning`, `.morning`, `.intermediate`, `.evening`, `.extremeEvening`) from recent history, or returns `nil` when there isn't enough data. The result can be passed back into `compute(...)` as the `chronoType` argument.

## Real-time events

The SDK provides real-time event updates during analysis through an AsyncStream API:

```swift theme={null}
Task {
    for await events in await SleepCycleSdk.eventStream {
        for event in events {
            switch event.type {
            case .movement:
                handleMovement(event)
            case .snoring:
                handleSnoring(event)
            case .talking:
                handleTalking(event)
            case .coughing:
                handleCoughing(event)
            default:
                handleOtherEvent(event)
            }
        }
    }
}
```

When the `extendedAudioEvents` feature is enabled for your API key, additional `EventType` values are reported alongside the standard ones: `.bird`, `.cat`, `.digestive`, `.dog`, `.fart`, `.music`, `.sneeze`, `.throatClearing`, `.traffic`, `.water`, and `.wind`. Use `EventType.standardTypes` and `EventType.extendedTypes` to distinguish the two sets.

Each `Event` contains:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>type</code> <span style={{ opacity: 0.5 }}>EventType</span> - The type of event<br />
  <code>interval</code> <span style={{ opacity: 0.5 }}>DateInterval</span> - Time interval of the event<br />
  <code>probability</code> <span style={{ opacity: 0.5 }}>Double</span> - Confidence score (0.0 to 1.0)<br />
  <code>source</code> <span style={{ opacity: 0.5 }}>EventSource</span> - Source of detection<br />
  <code>sessionId</code> <span style={{ opacity: 0.5 }}>UUID</span> - The session this event belongs to<br />
  <code>signature</code> <span style={{ opacity: 0.5 }}>\[Float]?</span> - Optional feature vector (for snoring events)
</div>

## Real-time breathing rate

The SDK provides real-time breathing rate measurements during analysis:

```swift theme={null}
Task {
    for await breathingRate in await SleepCycleSdk.breathingRateStream {
        print("Breathing rate: \(breathingRate.bpm) bpm (confidence: \(breathingRate.confidence))")
    }
}
```

Each `BreathingRate` contains:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>timestamp</code> <span style={{ opacity: 0.5 }}>Date</span> - The time when the measurement was recorded<br />
  <code>bpm</code> <span style={{ opacity: 0.5 }}>Double</span> - Breathing rate in breaths per minute<br />
  <code>confidence</code> <span style={{ opacity: 0.5 }}>Double</span> - Confidence score of the measurement (0.0 to 1.0)<br />
  <code>sessionId</code> <span style={{ opacity: 0.5 }}>UUID</span> - The session this measurement belongs to
</div>

### Real-time sleep staging (Experimental)

<Warning>
  This feature is experimental and may change in future releases. The API and behavior are subject to modification without notice.
</Warning>

The SDK can provide real-time sleep stage predictions during analysis. This feature requires the `realTimeSleepStaging` capability to be enabled for your API key.

```swift theme={null}
Task {
    for await stageInterval in await SleepCycleSdk.sleepStageStream {
        switch stageInterval.stage {
        case .awake:
            print("Awake: \(stageInterval.interval)")
        case .light:
            print("Light sleep: \(stageInterval.interval)")
        case .deep:
            print("Deep sleep: \(stageInterval.interval)")
        case .rem:
            print("REM sleep: \(stageInterval.interval)")
        }
    }
}
```

The stream emits `SleepStageInterval` objects approximately every 30 seconds during analysis, providing near real-time feedback on sleep state transitions.

### Real-time audio health

The SDK monitors the health of the audio input during analysis and emits status updates when the audio state changes:

```swift theme={null}
Task {
    for await update in await SleepCycleSdk.audioHealthStream {
        switch update.status {
        case .healthy:
            print("Audio input healthy")
        case .flatline:
            print("Audio flatline detected")
        case .missingInput:
            print("Audio input missing")
        }
    }
}
```

`AudioHealthStatus` values:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>.healthy</code> - Audio input contains a varying signal<br />
  <code>.flatline</code> - Constant value detected (non-functional microphone or muted input)<br />
  <code>.missingInput</code> - No audio input received for an extended period
</div>

## Smart alarm

The smart alarm monitors movement during a wake-up window and emits events when the user is in a light sleep phase, allowing you to wake them at an optimal moment. This requires the `smartAlarm` feature to be enabled for your API key and is only supported on the primary channel.

Configure a wake-up window with `SmartAlarmConfig` and pass it when starting analysis:

```swift theme={null}
import SleepCycleSDK

let wakeupWindow = DateInterval(start: windowStart, end: windowEnd)

try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: true),
    smartAlarmConfig: SmartAlarmConfig(wakeupWindow: wakeupWindow)
)
```

Observe alarm lifecycle events through `smartAlarmStream`:

```swift theme={null}
Task {
    for await event in await SleepCycleSdk.smartAlarmStream {
        switch event.type {
        case .armed:
            print("Smart alarm armed")
        case .triggered(let reason):
            switch reason {
            case .optimalWakeUp, .userInteraction, .windowEnd:
                ringAlarm()
            }
        }
    }
}
```

The `smartAlarmConfig` parameter is also available on `startMultiChannelAnalysis(...)`.

## Event signatures

For snoring events, the `Event.signature` property contains a 16-dimensional feature vector that represents unique characteristics of the detected snore. Snore events from the same person are grouped close to each other in the signature space, allowing clustering of events by person.

## Audio event listener

The `AudioEventListener` protocol allows you to receive real-time audio analysis updates during a session. Implement this protocol to access raw audio samples, event detection, and volume information as analysis progresses.

```swift theme={null}
class MyAudioListener: AudioEventListener {
    func onAudioAnalysisBatchCompleted(
        sessionId: UUID,
        audioSamples: [Float],
        audioSampleRate: Int,
        audioStartTime: Date,
        audioEndTime: Date,
        audioProbability: [EventType: Float],
        eventsStarted: [EventStartedInfo],
        eventsEnded: [EventEndedInfo],
        dbSpl: [Float]
    ) {
        // Process audio samples and events
    }
}

let listener = MyAudioListener()
try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true),
    eventListeners: [listener]
)
```

The `audioSamples` parameter contains all processed audio data in sequence, without any gaps or overlap between batches. Each batch continues exactly where the previous batch ended, ensuring complete coverage of all analyzed audio.

The `audioProbability` parameter provides the per-batch probability for each `EventType` in the analyzed time window. The `dbSpl` parameter provides the A-weighted sound volume in dB SPL for each time frame in the batch.

## Audio clips

The SDK can capture short audio recordings when specific sleep events are detected, such as snoring, sleep talking, or coughing.

Create an `AudioEventListener` using `AudioClipsConfig` and `AudioClipsReceiver`:

```swift theme={null}
import SleepCycleSDK

let listener = SleepCycleSdk.createAudioClipsProducer(
    audioClipsConfig: AudioClipsConfig(
        activeTypes: [
            .snoring: EventTypeConfig(minDuration: 0.5),
            .talking: EventTypeConfig(minDuration: 0.5)
        ],
        clipLength: 5.0
    ),
    receiver: MyAudioHandler()
)

try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true),
    eventListeners: [listener]
)
```

Each `AudioClip` contains:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>startTime</code> <span style={{ opacity: 0.5 }}>Date</span> - Start timestamp<br />
  <code>type</code> <span style={{ opacity: 0.5 }}>EventType</span> - The event type that triggered the capture<br />
  <code>samples</code> <span style={{ opacity: 0.5 }}>\[Float]</span> - Raw audio samples<br />
  <code>sampleRate</code> <span style={{ opacity: 0.5 }}>Int</span> - Sample rate in Hz<br />
  <code>sessionId</code> <span style={{ opacity: 0.5 }}>UUID</span> - The session this clip belongs to
</div>

## Multi-channel analysis

The SDK supports analyzing two channels simultaneously using a stereo audio source. Multi-channel analysis separates the data source lifecycle from individual session lifecycles, allowing you to start and stop sessions on each channel independently.

The stereo stream is expected to come from two separate mono microphones combined into a single stereo stream, with one microphone per channel.

This requires the `multiChannelAnalysis` feature to be enabled for your API key.

### Channel separation

When using stereo input, `ChannelSeparationConfig` controls how audio events are assigned to channels. Built-in presets:

* `.bedSideMics` — bedside microphones placed apart (default)
* `.centeredMicArray` — closely spaced microphone array
* `.detectionStrengthOnly()` — no spatial filtering, uses only detection confidence

All parameters (mic distance, ambiguous zone, confidence threshold, per-event-type settings) can be tuned individually to fit your specific hardware setup and use case.

### Data source lifecycle

Start the data source with a stereo audio configuration before starting individual sessions:

```swift theme={null}
import SleepCycleSDK

let dataSource = SleepCycleSdk.createLiveDataSource()

try await SleepCycleSdk.startDataSource(
    using: dataSource,
    channelSeparationConfig: .bedSideMics
)
```

### Starting sessions on each channel

Once the data source is running, start a session on each channel:

```swift theme={null}
let primarySessionId: UUID = try await SleepCycleSdk.startMultiChannelAnalysis(
    channel: .primary,
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: true)
)

let secondarySessionId: UUID = try await SleepCycleSdk.startMultiChannelAnalysis(
    channel: .secondary,
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: false)
)
```

`AnalysisChannel` values:

<div style={{ background: 'rgba(128,128,128,0.04)', border: '1px solid rgba(128,128,128,0.15)', borderRadius: '0.5rem', padding: '0.75rem 1rem', fontSize: '0.8rem', lineHeight: '2.2' }}>
  <code>.primary</code> - First audio channel (or mono)<br />
  <code>.secondary</code> - Second audio channel in stereo
</div>

### Stopping sessions independently

Each session can be stopped independently to retrieve its result:

```swift theme={null}
let primaryResult = try await SleepCycleSdk.stopAnalysis(sessionId: primarySessionId)
let secondaryResult = try await SleepCycleSdk.stopAnalysis(sessionId: secondarySessionId)
```

### Stopping the data source

After all sessions have been stopped, stop the data source:

```swift theme={null}
await SleepCycleSdk.stopDataSource()
```

Calling `stopDataSource()` while sessions are still active will force-stop them and discard their results. To retrieve results, stop each session first.
