Skip to content

fix: contain workout-complete sync throws and isolate premium refresh failures (#569 review) - #570

Closed
cursor[bot] wants to merge 2 commits into
mainfrom
cursor/critical-bug-identification-a8c5
Closed

fix: contain workout-complete sync throws and isolate premium refresh failures (#569 review)#570
cursor[bot] wants to merge 2 commits into
mainfrom
cursor/critical-bug-identification-a8c5

Conversation

@cursor

@cursor cursor Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Deep review of PR #569 (#566 foreground crash fix) found two high-severity gaps in the same coroutine exception-containment work.

Bug 1 — Workout-complete sync can still SIGABRT (crash)

Impact: Same class of iOS-on-mac SIGABRT as #566, but triggered immediately after workout save when sync throws.

Root cause: PR #569 contained exceptions in onAppForeground() only. onWorkoutCompleted() still called attemptSync() without containment, and ActiveSessionEngine launches it via unguarded scope.launch { syncTriggerManager?.onWorkoutCompleted() } after every session persist. Any raw Ktor/IO/SqlDelight throwable from syncManager.sync() propagates to processUnhandledExceptionabort().

Fix: Extract attemptSyncSafely() (try/catch + onSyncFailure, rethrow CancellationException) and route both onWorkoutCompleted() and onAppForeground() through it.

Bug 2 — Premium refresh failure blocked portal sync + advanced backoff

Impact: After wake/resume, a transient premium refresh throw skipped attemptSync() entirely and recorded a sync failure in RetryState, delaying legitimate portal sync for up to the backoff window.

Root cause: PR #569 wrapped premium refresh and attemptSync in a single try/catch. Health body-weight sync already used an isolated catch; premium refresh did not.

Fix: Add refreshPremiumStatusFromConnectedPlatform() with the same isolated catch contract as health body-weight sync. Premium refresh failures are logged and swallowed; portal sync always proceeds.

Validation

  • :shared:compileKotlinIosArm64 and :shared:compileTestKotlinIosArm64 pass
  • Added/updated SyncTriggerManagerTest cases:
    • onAppForegroundStillSyncsWhenPremiumRefreshThrows
    • onWorkoutCompletedRecordsFailureAndDoesNotPropagateWhenSyncThrows
    • Renamed cancellation test for premium refresh path

Base

Builds on PR #569 (fix/issue-566-foreground-coroutine-crash).

Open in Web View Automation 

Phoenix Bugfix Agent and others added 2 commits June 17, 2026 15:44
…eground sync (#566)

AppLifecycleObserver (App.kt) launched syncTriggerManager.onAppForeground()
in an unguarded scope.launch using rememberCoroutineScope() (plain Job, no
SupervisorJob/CoroutineExceptionHandler). A non-CancellationException throwable
from the foreground sync chain (refreshPremiumStatusFromServer / attemptSync ->
Ktor/IO) propagated to propagateExceptionFinalResort -> processUnhandledException
-> terminateWithUnhandledException -> abort(), killing the iOS-on-mac process
with SIGABRT after wake (TestFlight 0.9.1 / 20260607234).

Changes:
- App.kt: wrap the scope.launch body in try/catch (rethrow CancellationException,
  log+swallow others via Logger.e). This is the crash-prevention boundary.
- SyncTriggerManager.kt: wrap onAppForeground() body in try/catch (rethrow
  CancellationException, log via Logger.e and record via onSyncFailure) so
  foreground sync failures are recorded in RetryState for backoff/retry UI.
- AppLifecycleCoroutineContainmentTest: pins the containment pattern at the
  coroutine-scope level (non-Cancellation throwable does not reach
  CoroutineExceptionHandler; CancellationException is rethrown and cancels job).
- SyncTriggerManagerTest: regression tests that a raw throwable from
  refreshPremiumStatusFromServer() is recorded in RetryState and does not
  propagate, and that CancellationException is rethrown (not recorded).

RCA: GPT-5.5 xhigh (issue #566 comment 4734443850).

Fixes #566
… failures

PR #569 fixed foreground SIGABRT (#566) but left two gaps:

1. onWorkoutCompleted() still called attemptSync() without containment.
   ActiveSessionEngine launches it via unguarded scope.launch after every
   workout save — the same Ktor/IO/SqlDelight throw path that aborted on
   foreground would crash post-workout.

2. onAppForeground() wrapped premium refresh and attemptSync in one try/catch,
   so a premium refresh throw skipped portal sync and incorrectly advanced
   backoff. Mirror the health body-weight pattern: isolate premium refresh,
   always attempt sync, only record onSyncFailure for attemptSync throws.

Adds regression tests for both paths.

Co-authored-by: Devil <9thLevelSoftware@users.noreply.github.com>
@9thLevelSoftware
9thLevelSoftware deleted the cursor/critical-bug-identification-a8c5 branch June 17, 2026 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants