Repository navigation
fix(alarms): time the wind-down nudge from the sleep need the Sleep tab states - #2906
Open
UtkuDenizAltiok wants to merge 1 commit into
Open
UtkuDenizAltiok wants to merge 1 commit into
UtkuDenizAltiok wants to merge 1 commit into
Conversation
…ab states
The wind-down nudge fires at wake time minus sleep need minus a 30-minute lead, and both platforms'
copy says it uses "your usual sleep need". It used a stored `windDown.sleepNeedMinutes` that defaults to
8 h and that nothing on either platform ever writes. Every other surface (the Sleep tab's debt card,
ledger and trend need line, and the Rest score) uses the personal need: the upper-quartile night,
never below the 8 h adult target, capped at 9.5 h. So a long sleeper whose Sleep tab said "8h 40m
needed" was reminded as if they needed 8 h, and the Apple Alarms copy named a need no other screen
stated.
The stored value stays (the scheduler and Android's boot receiver have no database), but the app now
keeps it equal to the Sleep tab's need:
- Apple: `WindDownNudge.adoptSleepNeed(normativeNeedMin:)`, called from AppModel's `repo.$days` sink with
`SleepModel.debtNeedMin(days:)`.
- Android: `WindDownStore.adoptSleepNeed(normativeNeedMin)`, called from the recentDays collector with
the new `debtNeedMin(days)`, which SleepModelLogic now also uses (same expression, extracted).
It writes only when the rounded need changes (it moves over weeks) and reschedules only then and only
while the nudge is on; an empty history emission is skipped so a launch cannot reset a learned need to
8 h and back. Rounding is half away from zero, clamped to the store's 5-11 h before converting, and
byte-identical on both platforms: the Kotlin test's expected values are the stdout of the Swift body
compiled standalone over 28 inputs including ties, bounds and non-finite values. The need never drops
below 8 h, so the nudge only ever moves earlier, and only for people whose longer nights run past 8 h.
The Apple Alarms copy ("A calm nudge %lldh %lldm before your usual wake time", "The nudge fires
%lldh %lldm before this") printed the need's whole hours beside the lead's minutes, which only added
up for a whole-hour need; it now states need + lead (same strings, no new translations).
Tests: WindDownSleepNeedTests (Swift) and WindDownSleepNeedTest (Kotlin): the rounding oracle, adopt
only on change, the 07:00 nudge moving from 22:30 to 21:50 for an 8h 40m Sleep-tab need, the copy sum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UtkuDenizAltiok
added a commit
to UtkuDenizAltiok/noop
that referenced
this pull request
Nov 6, 2026
…e Sleep tab's sleep need
UtkuDenizAltiok
added a commit
to UtkuDenizAltiok/noop
that referenced
this pull request
Nov 6, 2026
UtkuDenizAltiok
added a commit
to UtkuDenizAltiok/noop
that referenced
this pull request
Nov 6, 2026
Today's sleep card gains a "Tonight" row while the wind-down reminder is on: "Aim to be asleep by 22:20", then "That gives you 8h 40m of sleep before your 07:00 wake. Your wind-down reminder comes at 21:50." It answers the evening question on the screen people open, from the reminder's own inputs. - SimpleDayPlan.tonightPlan reads exactly what WindDownNudge schedules from (the wake time for the weekday being woken on, the sleep need, the lead) with the reminder's own wall-clock arithmetic, so "wind down at" is the minute the notification fires (tested over a spread of wakes and needs, per-day wake times, and ryanbr#1863's early-wake case landing on the evening before). - Shown only while the reminder is on (its wake time is otherwise an unused setting, ryanbr#2353) and only while the fall-asleep time is still ahead: not late in the evening, not after midnight. - The card's last-night line can state WHOOP's imported need for that night while tonight uses NOOP's; when the two differ, tonight's row leaves out its duration rather than print a second need. - Today observes the reminder's settings (SimpleDayPlan.ReminderKeys, pinned to WindDownNudge's keys by WindDownMirrorKeyTests), so switching it off on the Alarms screen removes the row at once. With upstream ryanbr#2906 (in the testing stack) the reminder's need is the Sleep tab's need, so the row and the Sleep tab agree. Presentation only; the full view is unchanged. 3 new strings, nine locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
|
The |
UtkuDenizAltiok
added a commit
to UtkuDenizAltiok/noop
that referenced
this pull request
Nov 6, 2026
Today's sleep card gains a "Tonight" row while the wind-down reminder is on: "Aim to be asleep by 22:20", then "That gives you 8h 40m of sleep before your 07:00 wake. Your wind-down reminder comes at 21:50." It answers the evening question on the screen people open, from the reminder's own inputs. - SimpleDayPlan.tonightPlan reads exactly what WindDownNudge schedules from (the wake time for the weekday being woken on, the sleep need, the lead) with the reminder's own wall-clock arithmetic, so "wind down at" is the minute the notification fires (tested over a spread of wakes and needs, per-day wake times, and ryanbr#1863's early-wake case landing on the evening before). - Shown only while the reminder is on (its wake time is otherwise an unused setting, ryanbr#2353) and only while the fall-asleep time is still ahead: not late in the evening, not after midnight. - The card's last-night line can state WHOOP's imported need for that night while tonight uses NOOP's; when the two differ, tonight's row leaves out its duration rather than print a second need. - Today observes the reminder's settings (SimpleDayPlan.ReminderKeys, pinned to WindDownNudge's keys by WindDownMirrorKeyTests), so switching it off on the Alarms screen removes the row at once. With upstream ryanbr#2906 (in the testing stack) the reminder's need is the Sleep tab's need, so the row and the Sleep tab agree. Presentation only; the full view is unchanged. 3 new strings, nine locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UtkuDenizAltiok
added a commit
to UtkuDenizAltiok/noop
that referenced
this pull request
Nov 6, 2026
…rms wording; phone build 281239a verified; ryanbr#2906 CI note
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Nov 8, 2026
…ab states Adopted from ryanbr#2906 (by @UtkuDenizAltiok). The wind-down nudge, and with it Forge's planned bedtime and evening start, counted back from `windDown.sleepNeedMinutes`, a fixed 8 h that nothing ever wrote. The Sleep tab, the debt ledger and the Rest score use the personal need (`SleepModel.debtNeedMin`, upper-quartile night, at least 8 h). AppModel now adopts that need whenever the daily history changes (rounded, clamped to 5-11 h, written and rescheduled only when it moves; an empty history is skipped). Before seven nights it stays 8 h, and the need is capped at 9.5 h, so the nudge cannot land at an odd time. The Alarms copy adds need and lead correctly when the need is not a whole number of hours. Fork adaptation: - the Forge Sleep card no longer names a second need ("Bedtime counts X h, as set in Alarms"), since bedtime and card now use the same one - the fork's own "You'll be reminded around ..." line is kept; the upstream wake-time picker hunk does not apply here - the Android (Kotlin) hunks are left out: Android logic belongs in the shared core, so the Android wind-down still uses 8 h until that move Not compiled here (no Swift toolchain in this environment). Analysis migration required: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AWc14WNvMmSF4RHP6ww4d
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The wind-down nudge fires at wake time − sleep need − 30 min lead. Both platforms describe it as timed from the
user's usual sleep need (Apple: "A calm nudge 8h 30m before your usual wake time"; Android: "timed from your wake
time and usual sleep need"). The need it actually used is
windDown.sleepNeedMinutes, which defaults to 8 h andwhich nothing on either platform ever writes (no UI, no backup key, no caller of the setter).
Every other surface uses the personal need: the Sleep tab's debt card, ledger and trend need line
(
SleepModel.debtNeedMin/ KotlinSleepModelLogic), and the Rest score, all throughAnalyticsEngine.Rest.personalizedNeedHours(upper-quartile night, floored at the 8 h adult target, capped at9.5 h). So a long sleeper whose Sleep tab says "8h 40m needed" is reminded as if they need 8 h, and the Apple
Alarms copy states a need no other screen states.
A second, smaller Apple bug in the same copy: it printed
sleepNeedMinutes / 60hours besideleadMinutesminutes, which only adds up while the need is a whole number of hours (an 8h 40m need + 30 min lead read
"8h 30m").
The change
The stored value stays (the scheduler, and Android's boot receiver, run without the database), but the app keeps
it equal to the Sleep tab's need:
WindDownNudge.adoptSleepNeed(normativeNeedMin:), called fromAppModel'srepo.$dayssink withSleepModel.debtNeedMin(days:).WindDownStore.adoptSleepNeed(normativeNeedMin), called from therecentDayscollector with a newdebtNeedMin(days)inSleepModelLogic.kt, whichbuildSleepModelnow also uses (the same expression,extracted, so the two cannot drift).
Both write only when the rounded need changes (it moves over weeks) and reschedule only then, and only while the
nudge is on. An empty history emission is skipped so a launch cannot reset a learned need to 8 h and back.
Behaviour change, stated plainly: because the personal need never drops below 8 h, the nudge only ever moves
earlier, and only for people whose longer nights run past 8 h. Anyone with fewer than 7 nights, or whose
upper-quartile night is at or below 8 h, sees no change.
The Apple copy now states need + lead (
WindDownNudge.offsetHoursMinutes); same strings, no new translations.Android's existing copy ("timed from your wake time and usual sleep need") is now accurate as written.
Parity
The rounding helper (
WindDownNudge.needMinutes(fromNormativeNeedMin:)/WindDownStore.needMinutes) rounds halfaway from zero after clamping to the store's 5–11 h (clamping first so neither
Int(_:)traps norMath.roundsaturates). Verified by oracle, not by reading: the Swift body was compiled standalone over 28 inputs (ties,
bounds, negative, huge, NaN, ±∞) and its stdout is pasted verbatim as the expected string in the Kotlin test; the
Swift test asserts the same list.
Tests
StrandTests/WindDownSleepNeedTests.swift: the oracle list; adopt only when the rounded need moves; ten nightswith an 8h 40m upper quartile give the Sleep tab's 520-minute need and move a 07:00 nudge from 22:30 to 21:50;
the copy sum (8h 40m + 30m → 9h 10m).
android/.../alarm/WindDownSleepNeedTest.kt: the same oracle, adopt-on-change, and the same 22:30 → 21:50 casethrough
debtNeedMin.Verification
NOOPiOSsimulator build;StrandmacOS build +StrandTests(2306, 0 failures, 2 skipped as before).assembleFullDebug+testFullDebugUnitTest) green on this head: https://github.com/UtkuDenizAltiok/noop/actions/runs/42258465615Tools/i18n_audit.py --ci upstream/mainexit 0,Tools/doc_comment_lint.pyclean, Tools unit tests 338 OK,parity governance acceptance 50 OK.
Related
only the narrow piece those build on: the existing nudge uses the existing need. No planner, no new setting.
nudgeMinuteOfDay; the hunks donot overlap except that one copy line, so whichever lands second needs a one-line rebase.
🤖 Generated with Claude Code