Skip to content

fix: defer AMRAP/stall auto-stop after verbal VBT cue when autoEndOnVelocityLoss=false (#649)#650

Merged
9thLevelSoftware merged 5 commits into
mainfrom
fix/issue-649-vbt-verbal-defer
Jul 10, 2026
Merged

fix: defer AMRAP/stall auto-stop after verbal VBT cue when autoEndOnVelocityLoss=false (#649)#650
9thLevelSoftware merged 5 commits into
mainfrom
fix/issue-649-vbt-verbal-defer

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Summary

When the user has VBT Auto-End on Velocity Loss disabled but verbal encouragement enabled, the AMRAP position-based (2.5s) and velocity-stall (5s) auto-stop timers remained independent of the verbal VBT cue. After VBT triggered the verbal cue, the same cue's low-velocity window could start either timer; ~2.5–5s later the engine called handleSetCompletion(), cutting the verbal cue short and emitting WORKOUT_END. The user's AMRAP set ended mid-rep, with the set-completion audio cue firing right after the truncated encouragement.

This fix adds a per-set defer guard on the active session engine:

  • When checkVelocityThreshold() emits VERBAL_ENCOURAGEMENT while autoEndOnVelocityLoss=false, set deferAutoStopUntilNextWorkingRep = true and reset both timers.
  • checkAutoStop() early-returns (resetting both timers defensively) while the defer is active.
  • Cleared by the next completed working rep and at the existing per-set reset boundaries.
  • Enabled VBT auto-end (autoEndOnVelocityLoss=true) is untouched.

Root cause

ActiveSessionEngine.checkVelocityThreshold() correctly gates the two-consecutive-rep auto-end on coordinator.autoEndOnVelocityLoss. AMRAP position auto-stop (shouldRunPositionBasedAutoStop(params) → handlesCompletelyAtRest → requestAutoStop() after 2.5s) and velocity-stall (maxVelocity < STALL_VELOCITY_LOW → requestAutoStop() after 5s when enabled) did not consult that preference or the verbal-cue one-shot, so any "verbal cue in flight + user pauses" window could end the set early. handleSetCompletion() then stops BLE and emits WORKOUT_END, producing the overlapping end cue the reporter observed.

Fix direction (from GPT-5.5 RCA)

When checkVelocityThreshold() emits VERBAL_ENCOURAGEMENT while autoEndOnVelocityLoss is false, mark the current set as auto-stop-deferred-until-next-completed-working-rep. While that marker is set, checkAutoStop() must reset both position and velocity-stall countdown state and return before either can call requestAutoStop(). Clear the marker only after a later completed working rep, and reset it with the existing per-set reset path.

ponytail: single boolean + one early-return guard + three reset sites. Skipped: a tunable "encouragement duration" timeout (the next-rep boundary avoids guessing the cue's actual duration). Add when users report legitimate pauses being ended by this defer.

Acceptance criteria coverage

  • With isAMRAP=true, warmup complete, autoEndOnVelocityLoss=false, and a verbal VBT event, stationary metrics past both 2.5s and 5s stay Active with no BLE stop or WORKOUT_END.
  • After the next completed working rep, normal AMRAP auto-stop can complete again.
  • Enabled VBT auto-end (autoEndOnVelocityLoss=true) retains the two-consecutive-threshold-rep behavior (defer never arms).
  • Both AMRAP position and enabled velocity-stall paths respect the defer guard.

Tests

./gradlew :shared:testAndroidHostTest -Pskip.supabase.check=true2,424 tests pass, 0 failures, 0 errors across 230 test classes.

New: VerbalEncouragementAutoStopDeferTest (4 tests) covers the guard logic in the same tracker-mirror style as VbtAutoEndTest and VerbalEncouragementGateTest. Existing DWSMWorkoutLifecycleTest (63), VbtAutoEndTest (9), and VerbalEncouragementGateTest (7) all still pass.

Notes

  • Coordinated with GPT-5.5 RCA at Echo Hardest AMRAP: VBT verbal encouragement cut off, set ends prematurely on 3rd of 3 #649 (comment).
  • No public-surface changes — keeps the existing autoEndOnVelocityLoss setting semantically intact (still the user-controlled VBT switch) and adds no new preference.
  • Maintainer hypothesis in the original Discord thread (stall detection and VBT may be conflicting) substantially confirmed: this fix is the coordination layer that was missing. Stall detection itself is preserved.

Fixes #649

…elocityLoss=false (#649)

When the user has VBT Auto-End on Velocity Loss disabled but verbal
encouragement enabled, AMRAP position-based (2.5s) and velocity-stall
(5s) auto-stop timers remained independent of the verbal cue and could
call handleSetCompletion() mid-playback, cutting off the cue and ending
the AMRAP set prematurely.

Add a single-set guard: when VERBAL_ENCOURAGEMENT is emitted with
autoEndOnVelocityLoss=false, defer both AMRAP position and velocity-stall
auto-stop timers until the next completed working rep. Cleared by the
next completed working rep or at the per-set reset boundary. The VBT
two-consecutive-rep auto-end branch (gated on autoEndOnVelocityLoss) is
untouched.

RCA: #649 (comment)

Fixes #649

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request addresses Issue #649 by introducing a mechanism to defer position and stall auto-stops after a verbal VBT cue is emitted with autoEndOnVelocityLoss disabled, resetting the deferral once the next working rep is completed. It also adds a comprehensive suite of unit tests to verify this state machine logic. The review feedback highlights a potential concurrency issue where the newly introduced deferAutoStopUntilNextWorkingRep flag is accessed across different threads, recommending the addition of the @volatile annotation to ensure proper cross-thread visibility.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +246 to +249
// Issue #649: defer position/stall auto-stop until the next completed working
// rep after a verbal VBT cue was emitted with autoEndOnVelocityLoss=false.
// ponytail: single boolean; cleared on next completed working rep + per-set reset.
private var deferAutoStopUntilNextWorkingRep = false

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.

high

Since deferAutoStopUntilNextWorkingRep is written on Dispatchers.Default (inside checkVelocityThreshold) and read on the metrics collection thread (inside checkAutoStop), it is accessed concurrently across different threads. Without the @Volatile annotation, thread-local caching can prevent the metrics collection thread from immediately seeing the updated value, potentially causing premature auto-stops during verbal VBT cues. Marking the field as @Volatile ensures proper cross-thread visibility and avoids data races.

Suggested change
// Issue #649: defer position/stall auto-stop until the next completed working
// rep after a verbal VBT cue was emitted with autoEndOnVelocityLoss=false.
// ponytail: single boolean; cleared on next completed working rep + per-set reset.
private var deferAutoStopUntilNextWorkingRep = false
// Issue #649: defer position/stall auto-stop until the next completed working
// rep after a verbal VBT cue was emitted with autoEndOnVelocityLoss=false.
// ponytail: single boolean; cleared on next completed working rep + per-set reset.
@kotlin.concurrent.Volatile
private var deferAutoStopUntilNextWorkingRep = false
References
  1. Prefer using a @Volatile class field directly instead of introducing a wrapper holder class (like VolatileRef) to address multi-threaded visibility/data race concerns. This avoids unnecessary indirection while providing the same happens-before guarantee and maintaining consistency with existing field patterns.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72b8952353

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1431 to +1434
if (deferAutoStopUntilNextWorkingRep) {
resetStallTimer()
resetAutoStopTimer()
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow auto-stop if the user stops after the cue

When autoEndOnVelocityLoss is false and the verbal encouragement fires, this flag remains set until another working rep is completed; if the athlete instead racks the handles or fails immediately after the cue, every subsequent metric takes this branch and resets both timers, so AMRAP/timed/Just Lift position and stall auto-stop can never finish the set. Consider expiring the defer after the cue/grace window or allowing release/stall auto-stop after a short delay rather than requiring another completed rep.

Useful? React with 👍 / 👎.

@kilo-code-bot

kilo-code-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: Comment only | Recommendation: Prior warning + 4 of 5 Codex P2s resolved; acknowledged race at the arming site stands and is already commented inline; optional test cleanup welcome.

Overview

Severity Count
🚨 critical 0
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 0
Issue Details (click to expand)
File Line Roast
none new in changed code

🏆 Best part: The single-source-of-truth refactor is exactly the move. Collapsing flag+deadline into one @Volatile Long, zeroing it on the next completed working rep, and deleting the re-derivation in checkAutoStop() makes every defect I called out last round — plus three of the four Codex P2s — disappear in the same diff. The four reset sites (resetAutoStopState, resetForNewWorkout, startWorkout, the set-completion path) all reach the deadline correctly now. That's the sound of architectural debt getting paid down, finally.

💀 Worst part: There's nothing mean to roast in this incremental — the worst part is having nothing mean to roast. The acknowledged race at the arming site (line 1370) remains the only standing concern, and it's already pinned by Codex P2 inline.

📊 Overall: Like a kernel bisect that lands on the commit you already trusted — quieter than the prior round, and that's a compliment.

Prior review findings — re-verified
Source Location Status Notes
Kilo previous warning line 1463 ✅ Resolved deferAutoStopUntilNextWorkingRep = true re-assertion deleted; deadline is now the sole source of truth
Codex P2 "publish deadline before arming" line 1370 ✅ Resolved Single field — no publish order to race on
Codex P2 "allow auto-stop if user stops after cue" line 1459 ✅ Resolved 30s deadline window in checkAutoStop releases the defer even without a working rep
Codex P2 "clear defer in resetAutoStopState" line 973 ✅ Resolved Deadline zeroed in resetAutoStopState()
Codex P2 "zero deadline on completed working rep" line 1102 ✅ Resolved deferAutoStopDeadlineMs = 0L on the next working rep
Codex P2 "clear deadline in resetForNewWorkout" line 2071 ✅ Resolved deferAutoStopUntilNextWorkingRep = falsedeferAutoStopDeadlineMs = 0L
Codex P2 "guard late VBT callbacks" line 1370 ⚠️ Partially resolved _workoutState.value is WorkoutState.Active narrows the window; a callback that fires after the next set goes Active would still arm a phantom defer. Acknowledged in-code as a partial fix. Full fix needs a set/session generation token
Gemini P1 @Volatile line 256 ✅ Resolved Only the deadline field remains, marked @Volatile; comment explains why
Ponytail pass
  • VerbalEncouragementAutoStopDeferTest.kt:163-171: test-shrink redundant with line 67-74. Both verify that onRepCompleted(shouldStopSet=true) at nowMs arms the deadline to nowMs + 30_000L. Delete the second one — keep the original.
  • VerbalEncouragementAutoStopDeferTest.kt:163: test-name nit still standing after the rename. The title claims "metrics-thread reader sees immediately" but the tracker is single-threaded and the assertion is just nowMs + window arithmetic. Either strip the cross-thread framing from the name (e.g. "arm publishes a future deadline") or delete alongside the prior point.
  • ActiveSessionEngine.kt:1352-1373: comment is verbose. The 7-line "Acknowledged race-window" paragraph could collapse to "Narrow the window with an Active-state re-check; full fix needs a generation token." Optional.

Ponytail net: -10 lines (delete the redundant test at 163-171).

Correctness / Safety Findings

No new correctness, safety, security, regression, or test issues introduced by this incremental diff.

The acknowledged race at ActiveSessionEngine.kt:1370 (Codex P2 from the prior review) remains the only outstanding concern and is already commented inline. Full closure requires a set/session generation token captured at arming and checked at write — out of scope for this incremental, but worth filing as a follow-up before this path ships to users with rapid set transitions.

Files Reviewed (2 files, incremental diff only)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt — 47 lines changed (head 0d0f4ad7 vs d59b44e0): field refactor + Active-state re-check + reset-path migration
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/VerbalEncouragementAutoStopDeferTest.kt — tracker refactor + 1 new test seam (armEligible) + 2 test renames
Previous Review Summaries (3 snapshots, latest commit d59b44e)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit d59b44e)

Verdict: Request changes | Recommendation: Drop line 1463's flag re-assertion and zero the deadline at line 1106 before merge.

Overview

Severity Count
🚨 critical 0
⚠️ warning 1
💡 suggestion 0
🤏 nitpick 1
Issue Details (click to expand)
File Line Roast
ActiveSessionEngine.kt 1463 New flag re-assertion contradicts the "deadline-as-source-of-truth" design and undoes the line-1106 working-rep clear
VerbalEncouragementAutoStopDeferTest.kt 178 Test name implies cross-thread ordering verification but tracker is single-threaded

🏆 Best part: The source-of-truth refactor (val deferDeadline = deferAutoStopDeadlineMs; if (deferDeadline != 0L)) and the reorder at lines 1365-1366 (deadline-write before flag-write) are exactly the right architectural fixes for the cross-thread race Codex flagged. The resetAutoStopState() addition at lines 977-978 also closes the prior Codex P2 about reset paths. Two clean wins.

💀 Worst part: The new re-assertion at line 1463 is the architectural equivalent of installing a deadlock-prevention mutex that deadlocks. It claims the deadline is the gate, then re-derives the flag every metric — and that re-derivation undoes the only line (1106) that's supposed to release the defer on a completed working rep. The deadline is also never zeroed at line 1106 (still flagged by Codex P2), so even removing the re-assertion wouldn't fully restore the "clear on next working rep" contract until that pair is fixed together.

📊 Overall: Like buying a fancy new deadbolt and then taping the key to the doorframe. The intent is right; the wiring is fighting itself.

Prior review findings — re-verified
Source Location Status Notes
Gemini P1 @Volatile line 256 ✅ Resolved (1e4c0c3) Both fields annotated; comment explains why the deadline gets defensive @Volatile too
Codex P2 "publish deadline before arming" line:null ✅ Resolved Reorder at lines 1365-1366 publishes deadline first
Codex P2 "allow auto-stop if user stops after cue" line:null ✅ Resolved 30s deadline window in checkAutoStop releases the defer even without a working rep
Codex P2 "clear defer in resetAutoStopState" line 2073 ⚠️ Partially resolved resetAutoStopState() now clears both fields; however line 2073 in resetForNewWorkout() itself still only clears the flag, leaving a stale deadline. New workouts inherit a phantom defer until natural expiry
Codex P2 "zero deadline on completed working rep" line 1106 ❌ Still open Unchanged. With the new source-of-truth predicate, this is now load-bearing: without zeroing the deadline, the working-rep clear is observably a no-op. The new line 1463 re-assertion amplifies it
Ponytail pass
  • ActiveSessionEngine.kt:1463: delete re-asserting deferAutoStopUntilNextWorkingRep = true inside the else branch. The deadline is the source of truth; the flag doesn't need re-derivation.
  • ActiveSessionEngine.kt:1460-1461: the natural-expiry clear path clears flag before the deadline. Swap to deferAutoStopDeadlineMs = 0L first, then deferAutoStopUntilNextWorkingRep = false, to match the arming order at 1365-1366 (deadline first, flag second). Cosmetic but consistent.
  • VerbalEncouragementAutoStopDeferTest.kt:178: rename to reflect what it actually verifies — it pins the deadline value after arming on a single-threaded tracker; it does not (and cannot) verify cross-thread publish ordering.

Ponytail net: -3 lines (delete line 1463; net zero elsewhere).

Correctness / Safety Findings
  • ActiveSessionEngine.kt:1463 (warning): deferAutoStopUntilNextWorkingRep = true re-asserts the flag every metric while defer is active. Combined with the unchanged line 1106 (which clears only the flag, not the deadline), the working-rep release is reverted on the next metric. Fix: delete line 1463 AND add deferAutoStopDeadlineMs = 0L at line 1106.
  • ActiveSessionEngine.kt:2073 (important, pre-existing, unchanged line but caused by the source-of-truth refactor): resetForNewWorkout() does not call resetAutoStopState() and only clears the flag. A defer armed on the previous workout survives into the new workout as deadline != 0L. Fix: add deferAutoStopDeadlineMs = 0L here, or call resetAutoStopState() from this function.
  • VerbalEncouragementAutoStopDeferTest.kt:178 (nitpick): test name promises cross-thread ordering verification, but the tracker is single-threaded — the test only pins the deadline value. Rename for accuracy.
Files Reviewed (2 files, incremental diff only)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt — 19 added lines (lines 975-978, 1361-1369, 1447-1468)
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/VerbalEncouragementAutoStopDeferTest.kt — 24 added lines (tracker isDeferActive rewrite + 2 new methods + 2 new tests)

Fix these issues in Kilo Cloud

Previous review (commit 1e4c0c3)

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this incremental update is still clean. I had to put the flamethrower back in storage. Twice.

🏆 Best part: Both prior review findings (Gemini P1 @Volatile, Codex P2 deadline) are addressed surgically — one annotation, one companion constant, one currentTimeMillis() check, two new tests. That's 6 changed lines of behavior for two cross-thread concerns.

💀 Worst part: I went looking for trouble in the deadline-expiry branch (if (currentTimeMillis() >= deferAutoStopDeadlineMs) { flag = false; fallthrough }) and the only thing I can flag is the deadline field isn't zeroed at the per-set reset sites. That's "stale value behind a guard flag" — cosmetically itchy, behaviorally a no-op. The flag guard makes it inert. Skipping per the nitpick-isn't-worth-a-thread rule.

📊 Overall: Like a sequel that's better than the original — the original was already a haiku, and now it's a haiku with a safety net.

Files Reviewed (2 files, incremental diff only)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt — 23 added lines (incl. @Volatile x2, deadline field, deadline check, companion constant)
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/VerbalEncouragementAutoStopDeferTest.kt — 51 added lines (deadline mirrored + 2 new tests)

Prior review findings — re-verified on incremental diff

Source Severity Status Notes
Gemini (line 256) P1 @Volatile ✅ Resolved Applied to both deferAutoStopUntilNextWorkingRep and deferAutoStopDeadlineMs; comment explains which one actually needs it and why @Volatile on the second is defensive consistency.
Codex (line null, outdated) P2 defer expiring on racked handles ✅ Resolved 30s VERBAL_ENCOURAGEMENT_DEFER_WINDOW_MS ceiling + auto-clear in checkAutoStop(). 29_999L / 30_001L boundary tests cover it. Codex comment went line:null after commits — superseded.
Ponytail pass

Ponytail: Lean already. Ship.

Ponytail net: 0 lines.

Correctness / Safety Findings

No correctness or safety findings.

Deadline-expiry logic at L1446-1454 is correct: timers reset every metric during defer, so when the flag clears the auto-stop path enters with a fresh timer baseline — no stale-timestamp surprise. The >= comparison with an initial deadline of 0L is harmless because the flag guard short-circuits before the comparison runs.

Previous review (commit 72b8952)

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this PR is actually clean. I need to sit down. I had my flamethrower warmed up and everything.

🏆 Best part: The state machine is a single boolean, one early-return guard, and three reset sites — that's not a fix, that's a haiku written in Kotlin. Even the comment budgets its own line count.

💀 Worst part: I went looking for trouble and found none. Refusing to manufacture some just to feel useful.

📊 Overall: Like finding a unicorn in production — I didn't think clean PRs existed anymore, but here we are.

Files Reviewed (2 files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/VerbalEncouragementAutoStopDeferTest.kt

Reviewed by minimax-m3 · Input: 47K · Output: 13K · Cached: 226.6K

- Gemini review: deferAutoStopUntilNextWorkingRep is written on
  Dispatchers.Default (checkVelocityThreshold) and read on the metrics
  collection thread (checkAutoStop). Marked @volatile so the metrics
  thread immediately sees the armed flag and avoids a premature
  auto-stop window starting before the cue's audio finishes.
- Codex review (P2): if the athlete racks mid-set after the cue and
  never completes another working rep, the original fix would have
  suppressed AMRAP/stall auto-stop forever. Added a 30s deadline
  (VERBAL_ENCOURAGEMENT_DEFER_WINDOW_MS) — cue + short post-cue
  transition window — that auto-clears the defer so a legitimately
  racked mid-set handle still ends the set normally. Per-set reset
  sites and the next completed working rep continue to clear it.

Tests: VerbalEncouragementAutoStopDeferTest now 6/6 (added
within-window active + deadline expiry). ./gradlew
:shared:testAndroidHostTest -Pskip.supabase.check=true -> 2,426
tests pass, 0 failures, 0 errors.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e4c0c30db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1359 to +1360
deferAutoStopUntilNextWorkingRep = true
deferAutoStopDeadlineMs = currentTimeMillis() + VERBAL_ENCOURAGEMENT_DEFER_WINDOW_MS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Publish the deadline before arming the defer

When the verbal cue arms the defer, checkAutoStop() can run on the metrics thread between these two volatile writes. In that interleaving it observes deferAutoStopUntilNextWorkingRep == true while deferAutoStopDeadlineMs is still the old 0, treats the defer as already expired, and clears the flag, so the AMRAP/stall timers are not deferred after the cue. Set the deadline before publishing the flag, or update them atomically.

Useful? React with 👍 / 👎.

coordinator.biomechanicsEngine.reset()
velocityThresholdAlertEmitted = false
consecutiveThresholdReps = 0
deferAutoStopUntilNextWorkingRep = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the defer in the shared auto-stop reset

Clearing this flag only in a few higher-level paths misses reset paths that call resetAutoStopState() directly, such as stopAndReturnToSetReady() / skip flows and the startWorkout() reset. If a user stops or skips shortly after the verbal cue and starts another set before the 30s deadline, the stale flag carries into that new active set and checkAutoStop() suppresses AMRAP/stall auto-stop there. The defer state should be reset inside resetAutoStopState() so every new-set reset clears it consistently.

Useful? React with 👍 / 👎.

Two Codex P2 review follow-ups:

1) Cross-thread publish ordering — two @volatile fields (flag, deadline)
   don't chain happens-before across the JMM, so a metrics-thread reader
   could observe flag=true with a stale 0L deadline and clear the defer.
   Refactored to a single source of truth: deferAutoStopDeadlineMs == 0L
   means no defer. Reader derives the flag from the deadline and never
   sees 'flag=true && deadline=0L'. Writer still publishes deadline
   first when arming for clarity.

2) Reset path coverage — the defer flag was cleared only in per-set
   reset paths; resetAutoStopState() is the canonical reset hub and
   is called by stopAndReturnToSetReady() / startWorkout() / skip flows.
   If a user stops or skips shortly after the cue, the stale flag could
   carry into the next active set and suppress AMRAP/stall auto-stop
   there. Moved the defer-clear into resetAutoStopState() so every
   reset path covers it; existing per-set clears left as defensive
   belt-and-suspenders.

Tests: VerbalEncouragementAutoStopDeferTest 8/8 (added reset-paths
and arm-publishes-deadline). ./gradlew :shared:testAndroidHostTest
-Pskip.supabase.check=true -> 2,428 tests pass, 0 failures, 0 errors.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d59b44e0cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (repCountAfter > repCountBefore) {
// Issue #649: a completed working rep proves the user is back in motion;
// let normal AMRAP / stall auto-stop resume.
deferAutoStopUntilNextWorkingRep = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the defer deadline when the next rep completes

When a verbal cue is armed and the athlete completes another working rep before the 30s window expires, this only clears deferAutoStopUntilNextWorkingRep; however checkAutoStop() now treats deferAutoStopDeadlineMs != 0L as the source of truth and returns until that deadline. In that scenario AMRAP/stall auto-stop remains suppressed for the rest of the cue window despite the intended “next completed working rep” release, so the deadline should be zeroed here as well.

Useful? React with 👍 / 👎.

deferAutoStopUntilNextWorkingRep = false
deferAutoStopDeadlineMs = 0L
} else {
deferAutoStopUntilNextWorkingRep = true

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: The new checkAutoStop comment above literally says "Source of truth is the deadline field — flag is derived." — and then on the very next metric while defer is active, it helpfully re-asserts the flag back to true. That's not defensive programming, that's the flag and the deadline arguing over who's driving. Worse: this re-assertion actively undoes the working-rep clear at line 1106 (which the existing Codex P2 still flags as unfixed because the deadline isn't zeroed there). Net effect — the user completes a working rep at t=10s, line 1106 sets flag=false, the next metric at t=10.05s reads deadline=35_000L, drops into this else branch, sets flag=true again, and the defer is back. The "cleared on next completed working rep" contract advertised in the comment is a lie until either line 1106 zeros the deadline or this re-assertion goes away.

🩹 The Fix: Delete line 1463. The deadline is the gate; the flag doesn't need re-derivation every metric. Pair with zeroing deferAutoStopDeadlineMs = 0L at line 1106 (the existing Codex P2 there) so the working-rep clear is actually observable to checkAutoStop's if (deferDeadline != 0L) predicate.

📏 Severity: warning


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

Codex P2 + Kilo review both flagged the same bug: the working-rep
clear at line 1106 only zeroed the flag but not the deadline field.
The metrics-thread read site then re-derived 'flag=true' from the
still-nonzero deadline and silently re-armed the defer, so the
'cleared on next completed working rep' contract was a lie.

Removed the derived flag entirely. The @volatile Long deadline is
the only source of truth; checkAutoStop() gates on
'deadline != 0L' so there is nothing to re-derive. Every reset
path (next completed working rep, resetAutoStopState,
per-set boundaries, deadline expiry) zeros the same field.

Net effect in production: -29 lines, one @volatile Long, one
const ceiling, no flag/deadline synchronization to maintain.

Tests: VerbalEncouragementAutoStopDeferTest 8/8 (rewritten to
mirror the single-source-of-truth pattern; explicit tests pin
arm-with-future-deadline, working-rep-clears-regardless-of-deadline,
expiry, resetAutoStopState, non-arm cases). ./gradlew
:shared:testAndroidHostTest -Pskip.supabase.check=true -> 2,428
tests pass, 0 failures, 0 errors.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34897f5db0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// alone is the source of truth — no derived flag to keep in
// sync.
if (!coordinator.autoEndOnVelocityLoss) {
deferAutoStopDeadlineMs = currentTimeMillis() + VERBAL_ENCOURAGEMENT_DEFER_WINDOW_MS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard late VBT callbacks before arming a defer

When biomechanics work from a completed set runs late, this assignment can publish a fresh 30s defer after startWorkout() has already reset state for the next set. processBiomechanicsForRep() launches on the shared scope and checkVelocityThreshold() does not verify that the analyzed rep still belongs to the current set, so in a quick set transition/autostart path an old verbal-cue callback can make the new active set's checkAutoStop() suppress AMRAP/stall auto-stop until the timeout or another rep clears it. Capture and validate a set/session generation before arming the deadline; a simple Active-state check is not enough once the next set has started.

Useful? React with 👍 / 👎.

…t transition

Codex P2 (22:28): a stale processBiomechanicsForRep() callback resumed
from Dispatchers.Default after startWorkout() reset state for the next
set could write a fresh 30s defer into the new active set's checkAutoStop
predicate. Note the underlying race also affects the existing
velocityThresholdAlertEmitted flag (same call site); a full fix needs
processBiomechanicsForRep() to verify the analyzed rep belongs to the
current set, which is out of scope for the #649 fix.

Narrow the window for the defer specifically: re-read
coordinator._workoutState.value inside the arm guard and skip the
arm if the workout has already left Active (manual stop / set
transition). If the arm happens to win the race, the next completed
working rep will clear the defer within a rep or two; or it expires
in 30s. Either way the worst case is a bounded grace window on the
next set.

Tests: ./gradlew :shared:testAndroidHostTest -Pskip.supabase.check=true
-> 2,428 tests pass, 0 failures, 0 errors.
@9thLevelSoftware 9thLevelSoftware merged commit 0f00f4b into main Jul 10, 2026
10 checks passed
@9thLevelSoftware 9thLevelSoftware deleted the fix/issue-649-vbt-verbal-defer branch July 11, 2026 00:07
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.

Echo Hardest AMRAP: VBT verbal encouragement cut off, set ends prematurely on 3rd of 3

1 participant