Skip to content

fix: exempt Just Lift from rep freshness gate target mismatch (#698) - #699

Open
9thLevelSoftware wants to merge 6 commits into
mainfrom
fix/issue-698-just-lift-rep-freshness-gate
Open

fix: exempt Just Lift from rep freshness gate target mismatch (#698)#699
9thLevelSoftware wants to merge 6 commits into
mainfrom
fix/issue-698-just-lift-rep-freshness-gate

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Summary

Fixes the rep freshness gate dropping all Just Lift packets as TARGET_MISMATCH, which prevented warmup rep counting, audio feedback, and auto-stop on Just Lift sessions.

Root Cause

Echo Just Lift commands use unlimited target semantics — the firmware encodes the target as 0xFF, which the parser reports as repsSetTotal=252. However, RepNotificationFreshnessGate.evaluate() required the device-reported target to equal the finite UI lease.workingRepTarget (e.g. 10). Every valid Just Lift packet was dropped at lines 59-61 before rep counting, warmup, audio, or auto-stop could fire.

Fix

Gate the target-equality and finite-terminal checks on !lease.isJustLift using the existing isJustLift field on ExecutionLease:

  • Target check (line 59): Just Lift leases skip the repsSetTotal == workingRepTarget comparison
  • Terminal check (line 64): Just Lift leases skip the repsSetCount >= workingRepTarget terminal check

No parser conversion, BLE command change, or broad mode refactor. Minimal scope — only RepNotificationFreshnessGate.evaluate() is affected.

Acceptance Criteria

  • Post-cutover Just Lift modern packets with repsSetTotal=252 pass the freshness gate
  • Just Lift repsSetCount is not treated as terminal
  • Finite-target executions still reject nonzero mismatched targets
  • Pre-cutover, invalidated, and non-current execution packets remain rejected
  • No parser conversion, BLE command change, or broad mode refactor included

Tests

Three new test cases in RepNotificationFreshnessGateTest:

  1. Just Lift lease accepts repsSetTotal=252 despite finite UI target
  2. Just Lift lease does not treat repsSetCount as terminal
  3. Finite lease still rejects mismatched repsSetTotal after fix

Fixes #698

… in RepNotificationFreshnessGate

Issue #698: Echo Just Lift commands use unlimited target semantics (0xFF/252),
but the modern rep freshness gate required the device-reported repsSetTotal to
equal the finite UI lease workingRepTarget. This caused every valid Just Lift
packet to be dropped as TARGET_MISMATCH before rep counting, warmup, audio
feedback, or auto-stop could fire.

Fix: gate the target-equality and finite-terminal checks on !lease.isJustLift
using the existing isJustLift field on ExecutionLease.

Acceptance criteria:
- Just Lift modern packets with repsSetTotal=252 pass the freshness gate
- Just Lift repsSetCount is not treated as terminal
- Finite-target executions still reject nonzero mismatched targets
- Pre-cutover, invalidated, and non-current packets remain rejected

Fixes #698
Copilot AI lite review requested due to automatic review settings August 16, 2026 04:22

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@kilo-code-bot

kilo-code-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: No Issues Found | Recommendation: Merge

Verdict

Approve. The previous warning (auto-start grab predating lease activation) is resolved by reading bleRepository.handleState.value synchronously after activate() and before startActiveWorkoutPolling(), carrying an already-confirmed Grabbed across the activation boundary into repFreshnessGate.observeMovement(activeLease).

Correctness / Safety Findings

No correctness or safety findings.

Ponytail Review

Ponytail: Lean already. Ship.

Ponytail net: 0 lines.

Suggested Minimal Patch

No patch needed.

Final Merge Guidance

Can merge as-is.

Notes

  • The new if (activeLease.isJustLift && bleRepository.handleState.value == HandleState.Grabbed) branch is scoped to Just Lift only — finite-target leases still arm via the zero-baseline path (line 91-94 of RepNotificationFreshnessGate.kt), so they don't need this synchronous carry-over. The previous commit's PROGRESS_BEFORE_EVIDENCE drop (line 95-97) is precisely what makes Just Lift sensitive to this race; finite leases accept the first baseline packet regardless.
  • The new regression test (Just Lift warmup accepts unlimited progress when the confirmed grab predates lease activation) sets HandleState.Grabbed before startWorkout(), which is the exact shape of the auto-start flow the previous warning described.
  • The existing test at line 1700 still covers the Released → Grabbed post-activation path; together with the new test, both pre- and post-activation grab scenarios are pinned.
  • observeMovement is idempotent (state set to Armed regardless of prior value), so any redundant call from the existing handle-state collector (line 553-560) is harmless.

🏆 Best part: The fix uses the existing observeMovement API and lives next to resetFor + startActiveWorkoutPolling — the activation ceremony is now self-contained, and a future reader can see all three lifecycle steps in one screen. Disciplined.

💀 Worst part: The comment says "The polling restart preserves that confirmed detector state," which is slightly back-to-front — polling restart doesn't preserve detector state; the handleState StateFlow already holds the value and won't re-emit without a transition. But the intent reads correctly on a second pass, so I'll stop being pedantic.

📊 Overall: Like the third sequel that actually fixes the plot hole from the second movie. Five Codex P1 follow-ups, five clean increments. Whoever's writing this is either very good or very lucky, and I'm running out of material either way.

Files Reviewed (2 files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt - 0 issues
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/DWSMWorkoutLifecycleTest.kt - 0 issues
Previous Review Summaries (5 snapshots, latest commit 80b94a9)

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

Previous review (commit 80b94a9)

Verdict: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 0
⚠️ warning 1
💡 suggestion 0
🤏 nitpick 0
Issue Details (click to expand)
File Line Roast
shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt 555 The confirmed grab that starts auto-start occurs before activation, so the new gate can miss its only Grabbed event and count zero reps forever.

🏆 Best part: The regression test is focused and the production change is a single small movement-evidence branch. Oh wait, this part is actually disciplined; somebody hide the confetti.

💀 Worst part: The test simulates Released → Grabbed after activation, while the real auto-start path can already be Grabbed; the test and the production event order are living in different universes.

📊 Overall: Like adding a doorbell and discovering the lock is on the other side — the intent is right, but the first real rep can still walk straight past it.

Fix these issues in Kilo Cloud

Files Reviewed (2 files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt - 1 issue
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/DWSMWorkoutLifecycleTest.kt - 0 issues

Previous review (commit e3bb615)

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this increment lands the third Codex P1 clean and even has tests that read like a bouncer checking IDs at the door: "no zero baseline? no movement? no entry." I had my flame-thrower idling and you're out here shipping a guard clause.

Overview

Severity Count
🚨 critical 0
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 0

The new PROGRESS_BEFORE_EVIDENCE drop sits exactly where it belongs — after the target check passes, after an all-zero baseline would arm, but before the catch-all hasNonTerminalProgress → Arm; Process line that previously made Just Lift sessions import the previous set's count. Ordering verified:

  • Just Lift + repsSetTotal=252 + non-zero progress + AwaitingEvidence → ✅ Drop PROGRESS_BEFORE_EVIDENCE
  • Just Lift + all-zero baseline → ✅ BaselineOnly; arm
  • Just Lift + any non-zero after Arm → ✅ Process (line 73 short-circuit, unaffected)
  • Finite + terminal + non-zero → ✅ Drop TERMINAL_BEFORE_EVIDENCE (existing path intact)

The two new regression tests are properly minimal and the DWSM lifecycle helper now takes repsSetTotal + includeZeroBaseline so the existing test exercises a real Just Lift BLE shape end-to-end. observeMovement continues to arm the gate, but this PR doesn't tighten or loosen that, so it remains outside its scope.

🏆 Best part: The new drop reason and its single-line guard sit next to a pre-existing if (allZero) ... BaselineOnly arming branch — the two together are the whole fix, which is the minimum mental surface area this could possibly have.

💀 Worst part: Nothing to roast. The PR author has, against all odds, shipped an increment I cannot embarrass.

📊 Overall: Like the third season of a show that started shaky and is now quietly out-writing prestige dramas. Three P1 follow-ups, three clean increments. Suspiciously competent.

Files Reviewed (2 files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGate.kt - 0 issues
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGateTest.kt - 0 issues
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/DWSMWorkoutLifecycleTest.kt - 0 issues

Previous review (commit e834407)

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this increment actually lands the P1 fix and retires the previous nitpick in the same stroke. I had my soapbox warmed up for a long speech about operator precedence and now I'm out of material.

Overview

Severity Count
🚨 critical 0
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 0

The previous review's only finding (shrink the redundant inner repsSetTotal == 0 clause on the Just Lift branch) is resolved by collapsing the chain into an explicit if (lease.isJustLift) { ... } else { ... } — no more shadowed zero disjunct, and the two semantic branches (unlimited exemption vs. finite target match) now read in parallel. The companion test now drives the exact regression: a stale packet whose repsSetTotal happens to equal the Just Lift UI lease's workingRepTarget (10 == 10) is still rejected with TARGET_MISMATCH. The zero clause moved cleanly out of the Just Lift branch and stayed in the finite branch where it belongs.

Trace verification against the new code:

  • Just Lift + UNLIMITED → match ✓
  • Just Lift + 0 → match ✓
  • Just Lift + finite non-zero (including one equal to workingRepTarget) → drop ✓
  • Finite + 0 → match ✓
  • Finite + workingRepTarget → match ✓
  • Finite + finite non-zero ≠ workingRepTarget → drop ✓

All branches preserved, all comments updated to mention the new "including one whose target happens to equal the UI lease target" case.

🏆 Best part: The diff is purely subtractive in spirit — it splits one gnarly boolean into two clean branches, deletes a shadowed clause, and rewrites one test assertion. That's what a P1 follow-up should look like.

💀 Worst part: There's literally nothing left to roast, which is the most offensive thing a PR can do to a reviewer with a flamethrower.

📊 Overall: Like a director's cut that fixes the original plot hole and patches the deleted scene's continuity error — and does it in fewer shots than the original take.

Files Reviewed (2 files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGate.kt - previous nitpick resolved, 0 new issues
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGateTest.kt - regression test updated to target the actual bug shape, 0 issues

Previous review (commit b7fee41)

Verdict: 1 Optional Nitpick | Recommendation: Merge

Verdict

Comment only. The Codex P1 follow-up correctly restricts the Just Lift exemption to the documented unlimited encodings; one redundant clause snuck in alongside the fix.

Correctness / Safety Findings

No correctness or safety findings.

Ponytail Review

  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGate.kt:L65: shrink — the new inner notification.repsSetTotal == 0 inside (lease.isJustLift && (... || repsSetTotal == 0)) duplicates the outer || notification.repsSetTotal == 0 two lines below. Collapse to three disjuncts:
val targetMatches = lease.isJustLift && notification.repsSetTotal == UNLIMITED_REPS_SET_TOTAL ||
    notification.repsSetTotal == 0 ||
    notification.repsSetTotal == lease.workingRepTarget

Ponytail net: -1 line.

Suggested Minimal Patch

Drop the inner notification.repsSetTotal == 0 on line 65 of RepNotificationFreshnessGate.kt and tighten the comment on line 61 (drop "or zero" since the outer clause still admits zero for both Just Lift and finite leases).

Final Merge Guidance

Can merge as-is; the Ponytail nit is optional cleanup.


🏆 Best part: The P1 fix lands exactly where the previous review predicted — gating the Just Lift exemption to UNLIMITED_REPS_SET_TOTAL (or the legacy zero form), introducing a named companion constant, and adding a regression test (just lift rejects stale finite repsSetTotal from prior execution) that pins the exact failure mode the previous review warned about. Surgical, named, tested. Boring in the best way.

💀 Worst part: One redundant repsSetTotal == 0 clause was wedged into the new Just Lift branch and now shadows the outer zero disjunct with the same value — the kind of thing that's easy to miss when you're rushing to close a P1.

📊 Overall: Like a director's cut that fixes the original plot hole — but accidentally leaves one continuity error on the editing-room floor.

Files Reviewed (2 files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGate.kt - 1 nitpick
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGateTest.kt - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit b88dc44)

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.

Verdict

Approve. Minimal, surgical fix with regression coverage and zero scope creep.

Correctness / Safety Findings

No correctness or safety findings.

Ponytail Review

Ponytail: Lean already. Ship.

Ponytail net: 0 lines.

Suggested Minimal Patch

No patch needed.

Final Merge Guidance

Can merge as-is.

Notes

  • The isJustLift short-circuit on targetMatches and the !isJustLift guard on terminal are the minimum possible diff to fix the regression. No parser conversion, no BLE changes, no mode refactor — exactly as the PR promises.
  • The three new tests pin both the new Just Lift path and the existing finite path, so a future refactor that flips the semantics will fail loudly.
  • Comments above the two isJustLift branches explain why (firmware encodes unlimited target as 0xFF/252) — non-obvious enough to keep, not boilerplate.

🏆 Best part: .copy(isJustLift = true) in the tests — keeps the helper neutral and makes the variant under test explicit on a single line. Tasteful.

💀 Worst part: Honestly? Nothing. This is the kind of fix that makes code review boring, which is the highest compliment I can give a bugfix.

📊 Overall: Like a surgeon with a scalpel — one precise cut, three stitches, patient walks out fine.

Files Reviewed (2 files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGate.kt - 0 issues
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/RepNotificationFreshnessGateTest.kt - 0 issues

Reviewed by minimax-m3 · Input: 46.6K · Output: 6.9K · Cached: 463.4K

Review guidance: REVIEW.md from base branch main

@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: b88dc44e86

ℹ️ 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".

Address Codex review: accept only the known unlimited representation
(252) or zero for Just Lift leases, rejecting stale packets from prior
finite-target sets that could corrupt reps and feedback.

- Add UNLIMITED_REPS_SET_TOTAL constant (252)
- Gate targetMatches on specific unlimited value, not blanket isJustLift
- Add test: just lift rejects stale finite repsSetTotal from prior execution

@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: b7fee41271

ℹ️ 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".

(notification.repsSetTotal == UNLIMITED_REPS_SET_TOTAL ||
notification.repsSetTotal == 0)) ||
notification.repsSetTotal == 0 ||
notification.repsSetTotal == lease.workingRepTarget

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 Reject matching finite targets for Just Lift

When the preceding finite set has the same target as the Just Lift UI target (for example, both are 10), this final equality branch still accepts its delayed packet even though a Just Lift packet should report only 252 or zero. KableBleRepository.parseRepNotification() timestamps packets at receipt, and the new terminal exemption then classifies the stale nonzero count as progress, so RepCounterFromMachine imports it into the new session. Fresh evidence in this revision is that the narrowed Just Lift condition is still followed by the mode-agnostic repsSetTotal == lease.workingRepTarget fallback; exclude that fallback when lease.isJustLift is true.

Useful? React with 👍 / 👎.

Address Codex P1 on #699: a delayed finite-set packet whose repsSetTotal happened to equal the Just Lift UI target still passed the generic equality fallback. Restrict Just Lift to only the documented unlimited target (252) or zero, and add a matching-target stale-packet regression test.

@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: e834407ac7

ℹ️ 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".

Address Codex P1 on #699 by holding nonzero unlimited Just Lift notifications until a zero baseline or observed movement arms the new execution. Update the Issue 267 lifecycle fixture to emit the documented unlimited target (252) and baseline so CI models the device protocol.

@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: e3bb615565

ℹ️ 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 +95 to +96
if (lease.isJustLift && hasNonTerminalProgress) {
return RepFreshnessDecision.Drop(RepDropReason.PROGRESS_BEFORE_EVIDENCE)

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 Arm the gate on confirmed handle movement

When a Just Lift set receives no all-zero rep packet and the user pulls continuously above the velocity threshold, HandleStateDetector transitions directly from Released to Grabbed, while ActiveSessionEngine calls observeMovement() only for HandleState.Moving (the low-velocity state). The lease therefore remains AwaitingEvidence, and this branch drops the first progress packet and every subsequent cumulative packet, leaving the entire set at zero reps. Treat the confirmed Grabbed transition as movement evidence, or otherwise ensure ordinary first-rep progress can arm the gate without requiring an optional zero packet.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in commit 80b94a9: a confirmed Released → Grabbed transition now arms the Just Lift freshness gate, so a normal zero-less first-rep path is accepted. Added a lifecycle regression that starts from that direct transition and verifies unlimited (252) warmup progress reaches completion.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

// Moving would leave the freshness gate unarmed without a zero packet.
val hasFreshJustLiftGrab = activityState == HandleState.Grabbed &&
currentLease?.isJustLift == true
if ((activityState == HandleState.Moving || hasFreshJustLiftGrab) &&

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: This only arms on a new Grabbed emission, but the normal Just Lift auto-start path can already be in Grabbed before the new lease is activated. The grab that starts the auto-start countdown is emitted before activation; startActiveWorkoutPolling() restarts polling with forAutoStart=false, and the polling engine does not reset the detector, so there is no second Grabbed event after repFreshnessGate.resetFor(). The first unlimited rep packet is then dropped with PROGRESS_BEFORE_EVIDENCE, and the rest of the set stays at zero reps — a very confident way to count absolutely nothing.

🩹 The Fix: After activating the Just Lift lease, inspect the current handle state and call repFreshnessGate.observeMovement(activeLease) when it is Grabbed (or carry the already-confirmed grab evidence across the auto-start boundary). Add a regression test where Grabbed is set before workout activation and no post-activation state transition occurs.

📏 Severity: warning


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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in commit 1fafc9d: immediately after activating a Just Lift lease, the engine now carries an already-confirmed Grabbed handle state into the freshness gate before polling restarts. Added a zero-less unlimited-warmup lifecycle regression where the confirmed grab predates lease activation.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

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.

[Bug]: App not counting warmup reps

2 participants