Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -6678,29 +6678,41 @@ class ActiveSessionEngine(

private suspend fun saveJustLiftDefaultsFromWorkout() {
val params = coordinator._workoutParameters.value
if (!params.isJustLift) return

val eccentricLoadPct = if (params.isEchoMode) params.eccentricLoad.percentage else 100
val echoLevelVal = if (params.isEchoMode) params.echoLevel.levelValue else 0

val defaults = toJustLiftDefaultsDocumentOrNull(params) ?: return
try {
val defaults = JustLiftDefaultsDocument(
workoutModeId = params.programMode.modeValue,
weightPerCableKg = params.weightPerCableKg.coerceAtLeast(0.1f),
weightChangePerRep = params.progressionRegressionKg,
eccentricLoadPercentage = eccentricLoadPct,
echoLevelValue = echoLevelVal,
stallDetectionEnabled = params.stallDetectionEnabled,
repCountTimingName = params.repCountTiming.name,
restSeconds = params.justLiftRestSeconds,
)
settingsManager.saveJustLiftDefaultsDocument(defaults)
Logger.d { "Saved Just Lift defaults: mode=${params.programMode.modeValue}, weight=${params.weightPerCableKg}kg, restSeconds=${params.justLiftRestSeconds}" }
} catch (e: Exception) {
Logger.e(e) { "Failed to save Just Lift defaults: ${e.message}" }
}
}

/**
* Shared Just Lift defaults conversion used by both the legacy manual
* `saveJustLiftDefaultsFromWorkout()` path and the automatic-completion
* snapshot persistence path. Centralising the conversion guarantees the
* two paths cannot drift.
*
* Returns null when [params] is not a Just Lift workout. See issue #714.
*/
internal fun toJustLiftDefaultsDocumentOrNull(params: com.devil.phoenixproject.domain.model.WorkoutParameters): JustLiftDefaultsDocument? {
if (!params.isJustLift) return null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: You imported WorkoutParameters at line 78 like a responsible engineer, then immediately wrote com.devil.phoenixproject.domain.model.WorkoutParameters on line 6699 like you didn't notice. The import is sitting right there, patient, waiting to be used. It's giving "I copied this from a different file and never looked back."

🩹 The Fix:

Suggested change
if (!params.isJustLift) return null
internal fun toJustLiftDefaultsDocumentOrNull(params: WorkoutParameters): JustLiftDefaultsDocument? {

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


val eccentricLoadPct = if (params.isEchoMode) params.eccentricLoad.percentage else 100
val echoLevelVal = if (params.isEchoMode) params.echoLevel.levelValue else 0

return JustLiftDefaultsDocument(
workoutModeId = params.programMode.modeValue,
weightPerCableKg = params.weightPerCableKg.coerceAtLeast(0.1f),
weightChangePerRep = params.progressionRegressionKg,
eccentricLoadPercentage = eccentricLoadPct,
echoLevelValue = echoLevelVal,
stallDetectionEnabled = params.stallDetectionEnabled,
repCountTimingName = params.repCountTiming.name,
restSeconds = params.justLiftRestSeconds,
)
}

suspend fun getSingleExerciseDefaults(
exerciseId: String,
): com.devil.phoenixproject.data.preferences.SingleExerciseDefaults? = settingsManager.getSingleExerciseDefaultsDocument(exerciseId)?.toLegacySingleExerciseDefaults()
Expand Down Expand Up @@ -8955,6 +8967,11 @@ class ActiveSessionEngine(
cycleId = context.cycleId,
dayNumber = context.cycleDayNumber,
)
// Issue #714: capture Just Lift defaults from the pre-teardown params
// so automatic completion persists the user's confirmed Just Lift mode.
// Read here (before reset) and freeze into the immutable snapshot so the
// persistSnapshot write cannot read mutable live state after teardown.
val capturedJustLiftDefaults = toJustLiftDefaultsDocumentOrNull(params)
return WorkoutExitSnapshot(
lease = lease,
completion = completion,
Expand All @@ -8966,6 +8983,7 @@ class ActiveSessionEngine(
biomechanicsRepResults = biomechanicsSummary?.repResults.orEmpty()
.map { it.deepCopyForExitSnapshot() },
singleExerciseDefaults = captureSingleExerciseDefaultsFromWorkout(),
justLiftDefaults = capturedJustLiftDefaults,
presentationSummary = presentationSummary,
exerciseIndex = exerciseIndex,
setIndex = setIndex,
Expand Down Expand Up @@ -9015,6 +9033,20 @@ class ActiveSessionEngine(
}
}

// Issue #714: writes the Just Lift defaults captured in the immutable exit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: You wrote a 9-line essay on a 6-line helper. The function name persistCapturedJustLiftDefaultsSnapshot is already half a documentation page. The rest is just you retelling the PR description to the file with stage directions.

🩹 The Fix: Collapse the comment to the one load-bearing sentence. Something like:

Suggested change
// Issue #714: writes the Just Lift defaults captured in the immutable exit
// Issue #714: extracted so the completion job can persist Just Lift defaults
// synchronously before the WorkoutState.Idle flip that navigates back to setup.
internal suspend fun persistCapturedJustLiftDefaultsSnapshot(snapshot: WorkoutExitSnapshot) {

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// snapshot. Split out from `persistSnapshot` so the Just Lift completion
// job can call it synchronously before publishing `SetSummary` / flipping
// to `Idle`, and the async path can still call it for retained-snapshot
// retry and process recovability. The async re-write is idempotent
// (same captured value).
internal suspend fun persistCapturedJustLiftDefaultsSnapshot(snapshot: WorkoutExitSnapshot) {
snapshot.justLiftDefaults?.let { justLiftDefaults ->
settingsManager.mutateWorkout(snapshot.lease.profileId) { workoutPreferences ->
workoutPreferences.copy(justLiftDefaults = justLiftDefaults)
}
}
}

private fun retryRetainedWorkoutExitPersistence() {
scope.launch {
exitSnapshotStore.retainedSnapshots().forEach(::launchSnapshotPersistence)
Expand Down Expand Up @@ -9072,6 +9104,12 @@ class ActiveSessionEngine(
)
}
}
// Issue #714: persist Just Lift defaults captured in the immutable
// exit snapshot so the user's confirmed Just Lift mode survives the
// return-to-setup reload. Uses the same profile-scoped mutateWorkout
// and the snapshot's lease profile id, so it cannot read mutable
// coordinator state after teardown.
persistCapturedJustLiftDefaultsSnapshot(snapshot)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent older snapshots from restoring stale Just Lift defaults

When two Just Lift sets complete before the first persistSnapshot reaches this call, the second completion can synchronously save its newer defaults and then this older coroutine can acquire the preferences mutex afterward and overwrite them with its captured values. Session, metric, and biomechanics writes before this line make that reordering possible, so consecutive sets using different modes can leave the profile reverted to the earlier set; skip this redundant write after successful synchronous persistence or add ordering/version protection.

Useful? React with 👍 / 👎.


val postSave = snapshot.postSaveInput
val hasPR = gamificationManager.processPostSaveEvents(
Expand Down Expand Up @@ -11223,6 +11261,38 @@ class ActiveSessionEngine(

Logger.d("handleSetCompletion: summaryCountdownSeconds=$summaryCountdownSeconds, skipSummary=$skipSummary, wasBodyweight=$wasBodyweight, effectiveSkipSummary=$effectiveSkipSummary, isJustLift=$isJustLift, isAMRAP=${params.isAMRAP}")

// Issue #714 (Codex P1 follow-up): write synchronously so
// JustLiftScreen's reload sees fresh defaults before any navigation
// fires; try/catch so a transient prefs failure doesn't strand the
// user mid-teardown.
// reads persisted defaults on `LaunchedEffect(readyProfileId)` — once per

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: You applied the suggestion from the previous review by adding the 4-line summary on top of the existing 16-line block instead of replacing the block. The result is 19 lines of comment saying roughly the same thing twice — same over-explained-comment pattern, but now wearing the same costume in two consecutive scenes. The 4-line summary at L11264-11267 is the entire story; this 15-line block is now a redundant director's commentary playing on mute.

🩹 The Fix: Delete the entire old verbose block (lines 11268-11282). The 4-line summary above already carries the sync-before-flip rationale and the try/catch resilience note; the helper's KDoc at ActiveSessionEngine.kt:9036 carries the architectural story. After deletion, the if (isJustLift && terminalSnapshot?.justLiftDefaults != null) { block sits directly under the new summary.

Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// profile-id change — and the async `persistSnapshot` coroutine does not
// finish writing the captured Just Lift defaults until AFTER this
// completion job has published `SetSummary` or flipped `WorkoutState` to
// `Idle`. The ActiveWorkoutScreen observer then pops back to
// JustLiftScreen and JustLiftScreen recomposes against the stale TUT value.
// Write the captured defaults synchronously here, BEFORE any state flip or
// summary publish that could trigger the navigation observer, so the
// JustLiftScreen reload sees the persisted Old School (or whatever the user
// picked). The write is wrapped in try/catch so a transient
// preferences-store failure cannot leave the user stuck after machine
// teardown — the retained-snapshot retry path is the durable backstop.
// The async `persistSnapshot` path still calls the same helper for retained
// snapshot recovery and process recovability (idempotent re-write of the
// same value).
if (isJustLift && terminalSnapshot?.justLiftDefaults != null) {
try {
persistCapturedJustLiftDefaultsSnapshot(terminalSnapshot)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Logger.w(e) {
"Issue #714: synchronous Just Lift defaults write failed; " +
"retained-snapshot retry will recover"
}
}
}

if (!effectiveSkipSummary && !preservePlanOwnedResting) {
Logger.d("handleSetCompletion: Setting state to SetSummary (effectiveSkipSummary=false)")
val summaryPublished = executionGuard.commitIfCurrent(lease) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.devil.phoenixproject.domain.model.BiomechanicsRepResult
import com.devil.phoenixproject.domain.model.BiomechanicsSetSummary
import com.devil.phoenixproject.domain.model.CompletedSet
import com.devil.phoenixproject.domain.model.ForceCurveResult
import com.devil.phoenixproject.domain.model.JustLiftDefaultsDocument
import com.devil.phoenixproject.domain.model.ProgramMode
import com.devil.phoenixproject.domain.model.RepMetricData
import com.devil.phoenixproject.domain.model.RoutineExecutionIdentity
Expand Down Expand Up @@ -357,6 +358,17 @@ internal data class WorkoutExitSnapshot(
val repMetrics: List<RepMetricData>,
val biomechanicsRepResults: List<BiomechanicsRepResult>,
val singleExerciseDefaults: SingleExerciseDefaultsDocument? = null,
/**
* Just Lift defaults captured from the pre-teardown Just Lift WorkoutParameters.
* Populated only when [completion] is a Just Lift completion; persisted in the same
* `settingsManager.mutateWorkout(snapshot.lease.profileId)` block as
* [singleExerciseDefaults] so the automatic-completion path cannot fall behind the
* legacy manual `saveJustLiftDefaultsFromWorkout()` path.
*
* See issue #714 (Just Lift mode resets to TUT at end of every set instead of
* preserving the user's selected mode).
*/
val justLiftDefaults: JustLiftDefaultsDocument? = null,
val presentationSummary: WorkoutState.SetSummary,
val exerciseIndex: Int,
val setIndex: Int,
Expand Down
Loading
Loading