@@ -168,19 +168,30 @@ class PlatformHealthSync(
168168 val overlays = healthDao.getOverlayEntriesAfter(lastTimestamp, allTypes)
169169 if (overlays.isEmpty()) return
170170
171- val healthRecords = mutableListOf<HealthRecord >()
172-
173171 val sleepOverlays = overlays.filter { it.type in sleepTypes }
174172 val exerciseOverlays = overlays.filter { it.type in exerciseTypes }
175173
176- // Build sleep sessions: group adjacent sleep overlays (within 2h gap)
177- healthRecords + = buildSleepSessions(sleepOverlays)
174+ var allSucceeded = true
175+
176+ // Write sleep sessions separately so exercise failures don't block sleep
177+ val sleepRecords = buildSleepSessions(sleepOverlays)
178+ if (sleepRecords.isNotEmpty()) {
179+ logger.d { " Writing ${sleepRecords.size} sleep sessions to health platform" }
180+ val result = healthManager.writeData(sleepRecords)
181+ if (result.isSuccess) {
182+ logger.d { " Synced ${sleepRecords.size} sleep records" }
183+ } else {
184+ allSucceeded = false
185+ logger.e { " Failed to write sleep records: ${result.exceptionOrNull()} " }
186+ }
187+ }
178188
179- // Build exercise records
189+ // Write exercise records separately
190+ val exerciseRecords = mutableListOf<HealthRecord >()
180191 for (overlay in exerciseOverlays) {
192+ if (overlay.duration <= 0 ) continue
181193 val startTime = Instant .fromEpochSeconds(overlay.startTime)
182194 val endTime = startTime + overlay.duration.seconds
183- if (overlay.duration <= 0 ) continue
184195
185196 val overlayType = OverlayType .fromValue(overlay.type) ? : continue
186197 val exerciseType = when (overlayType) {
@@ -190,7 +201,7 @@ class PlatformHealthSync(
190201 else -> continue
191202 }
192203
193- healthRecords + = ExerciseSessionRecord (
204+ exerciseRecords + = ExerciseSessionRecord (
194205 startTime = startTime,
195206 endTime = endTime,
196207 exerciseType = exerciseType,
@@ -204,51 +215,66 @@ class PlatformHealthSync(
204215 metadata = createMetadata(overlay.startTime, " exercise" ),
205216 )
206217 }
207-
208- if (healthRecords.isNotEmpty()) {
209- val result = healthManager.writeData(healthRecords)
218+ if (exerciseRecords.isNotEmpty()) {
219+ val result = healthManager.writeData(exerciseRecords)
210220 if (result.isSuccess) {
211- tracker.lastSyncedOverlayTimestamp = overlays.maxOf { it.startTime }
212- logger.d { " Synced ${healthRecords.size} sleep/exercise records" }
221+ logger.d { " Synced ${exerciseRecords.size} exercise records" }
213222 } else {
214- logger.e { " Failed to write sleep/exercise records: ${result.exceptionOrNull()} " }
223+ allSucceeded = false
224+ logger.e { " Failed to write exercise records: ${result.exceptionOrNull()} " }
215225 }
216- } else {
226+ }
227+
228+ // Only advance tracker if all writes succeeded (or there was nothing to write)
229+ if (allSucceeded) {
217230 tracker.lastSyncedOverlayTimestamp = overlays.maxOf { it.startTime }
218231 }
219232 }
220233
221234 private fun buildSleepSessions (overlays : List <OverlayDataEntity >): List <SleepSessionRecord > {
222235 if (overlays.isEmpty()) return emptyList()
223236
224- val sorted = overlays.sortedBy { it.startTime }
225- val sessions = mutableListOf< SleepSessionRecord >()
226- var currentGroup = mutableListOf (sorted.first() )
237+ // Filter to only overlays with positive duration before grouping
238+ val valid = overlays.filter { it.duration > 0 }.sortedBy { it.startTime }
239+ if (valid.isEmpty()) return emptyList( )
227240
228- for (i in 1 until sorted.size) {
241+ val groups = mutableListOf<MutableList <OverlayDataEntity >>()
242+ var currentGroup = mutableListOf (valid.first())
243+
244+ for (i in 1 until valid.size) {
229245 val prev = currentGroup.last()
230246 val prevEnd = prev.startTime + prev.duration
231- val curr = sorted [i]
247+ val curr = valid [i]
232248
233249 // Group overlays within 2 hours of each other into one session
234250 if (curr.startTime - prevEnd <= 2 * 3600 ) {
235251 currentGroup.add(curr)
236252 } else {
237- sessions + = createSleepSession( currentGroup)
253+ groups + = currentGroup
238254 currentGroup = mutableListOf (curr)
239255 }
240256 }
241- sessions + = createSleepSession(currentGroup)
242-
243- return sessions
257+ groups + = currentGroup
258+
259+ return groups.mapNotNull { group ->
260+ try {
261+ createSleepSession(group)
262+ } catch (e: Exception ) {
263+ logger.e(e) {
264+ " Failed to create sleep session from ${group.size} overlays: " +
265+ group.joinToString { " type=${it.type} ,start=${it.startTime} ,dur=${it.duration} " }
266+ }
267+ null
268+ }
269+ }
244270 }
245271
246272 private fun createSleepSession (overlays : List <OverlayDataEntity >): SleepSessionRecord {
247273 val sessionStart = Instant .fromEpochSeconds(overlays.minOf { it.startTime })
248274 val sessionEnd = Instant .fromEpochSeconds(overlays.maxOf { it.startTime + it.duration })
249275
276+ // Build non-overlapping stages sorted by start time
250277 val stages = overlays
251- .filter { it.duration > 0 }
252278 .map { overlay ->
253279 val stageType = when (OverlayType .fromValue(overlay.type)) {
254280 OverlayType .DeepSleep , OverlayType .DeepNap -> SleepStageType .Deep
@@ -261,6 +287,25 @@ class PlatformHealthSync(
261287 )
262288 }
263289 .sortedBy { it.startTime }
290+ .fold(mutableListOf<SleepSessionRecord .Stage >()) { acc, stage ->
291+ val prev = acc.lastOrNull()
292+ if (prev != null && stage.startTime < prev.endTime) {
293+ // Overlapping stage — trim its start to previous end, or skip if fully contained
294+ if (stage.endTime > prev.endTime) {
295+ acc + = SleepSessionRecord .Stage (
296+ startTime = prev.endTime,
297+ endTime = stage.endTime,
298+ type = stage.type,
299+ )
300+ }
301+ // else: fully contained, skip
302+ } else {
303+ acc + = stage
304+ }
305+ acc
306+ }
307+
308+ logger.d { " Sleep session: ${stages.size} stages, start=$sessionStart , end=$sessionEnd " }
264309
265310 return SleepSessionRecord (
266311 startTime = sessionStart,
0 commit comments