Skip to content

fw/services/activity: fire sleep summary notification once per sleep window#1767

Merged
jplexer merged 2 commits into
coredevices:mainfrom
jplexer:jp/firm-3101-sleep-report-from-previous-night-repeats-15-hours-later
Jul 21, 2026
Merged

fw/services/activity: fire sleep summary notification once per sleep window#1767
jplexer merged 2 commits into
coredevices:mainfrom
jplexer:jp/firm-3101-sleep-report-from-previous-night-repeats-15-hours-later

Conversation

@jplexer

@jplexer jplexer commented Jul 21, 2026

Copy link
Copy Markdown
Member

Problem

Users receive their "Good morning" sleep summary notification again hours after the real one — in the field trace, a duplicate arrived at 22:12 local, ~15 hours after the morning summary, and again the next day without any reboot in between (same symptom family as the mid-night summary complaints).

Root cause

Two layers:

  1. Detection accuracy (out of scope here, tracked in FIRM-1969): Kraepelin falsely detects ~40-60 min of "sleep" during sedentary-awake evening time. Sessions ending after 21:00 local (or starting before 12:00) escape nap reclassification and count as night sleep.

  2. Insights layer (this fix): prv_do_sleep_summary() treated any change of the day's sleep bounds as new-sleep-to-report:

    • a pin update for an extended sleep_exit_utc cleared s_sleep_pin_state.notified, re-arming the notification;
    • any shift of sleep_enter_utc was treated as "new day" and reset the whole pin state.

    Either path re-fires the full summary notification 30 minutes after the (false) exit, once the user is briefly active. The field's false 19:48-20:42Z session produced the duplicate at exactly exit+30min.

Fix design

  • Window-keyed dedup: the sleep pin state is now keyed to the sleep-day window (the 21:00-local cutoff already used by activity_sessions_prv_get_sleep_bounds_utc(), extracted as activity_sessions_prv_get_sleep_window_start_utc()). Within one window the notification fires at most once: pin updates no longer clear notified, and only a new window resets the state. Exact-timestamp matching is gone, so bounds/enter shifts (which fw/services/activity: keep short sleep cycles inside a fragmented night #1742 makes more common) and settings-state "time travel" resets can no longer re-arm it. The timeline pin still updates silently.
  • Wake-window gate (notification only): the summary notification only fires when sleep_exit_utc lands in [04:00, 18:00) local. Skipping outside that window does not consume the once-per-window notification, so a mid-night false exit stays silent and the real morning wake-up still notifies. The pin is unaffected.
  • Persistence: SleepPinState grows by a trailing window_utc field, persisted via the existing ActivitySettingsKeyInsightSleepSummaryState key. Older persisted blobs fail the settings read cleanly (E_RANGE zeroes the output — no partial misread of old-layout bytes), and a zeroed state is re-seeded at init as already-notified for the in-progress window, so the layout change cannot re-fire a summary the user already saw. Worst case is one missed summary right after the update.
  • Cleanup: AttributeIdIconTiny is added with attribute_list_add_resource_id() instead of attribute_list_add_uint32(), silencing the "Adding attribute with type uint32 for non-uint32_t attribute" warning seen in field logs.

Test plan

  • New unit tests in tests/fw/services/activity/test_activity_insights.c:
    • sleep_summary_no_renotify_same_window: night sleep → morning summary fires once → same-window evening session extension (analog of the field's false session) → pin updates in place, no second notification.
    • sleep_summary_notify_next_window: the next night past the 21:00 cutoff starts a new window with a new pin, and the notification fires again.
    • sleep_summary_notification_wake_window_gate: a 02:00-local exit does not notify and does not consume the window's notification; the later 07:00 wake still notifies.
  • Negative check: with the fix reverted (tests kept), all three new tests fail — the same-window repro fires 2 notifications, and the mid-night exit notifies at 02:35.
  • ./waf test -M '.*activity.*': 12/12 suites pass (includes test_activity, which compiles the refactored activity_sessions.c).
  • ./waf build (obelix@pvt, normal variant) succeeds.

Fixes FIRM-3101

🤖 Generated with Claude Code

jplexer and others added 2 commits July 21, 2026 20:36
…window

The "Good morning" sleep summary re-fired hours after the real morning
notification whenever the day's sleep bounds changed. Two paths re-armed
it: a pin update for an extended sleep exit cleared the notified flag,
and any shift of sleep_enter_utc was treated as a new day and reset the
whole pin state. A falsely detected evening sleep session (sedentary
awake time misclassified by the algorithm, tracked separately) was
enough to trigger either path, buzzing the user with last night's
summary in the evening or in the middle of the night.

Key the pin state to the sleep-day window (the 9pm local cutoff used by
activity_sessions_prv_get_sleep_bounds_utc, now exposed as
activity_sessions_prv_get_sleep_window_start_utc) instead of comparing
exact enter timestamps. Within one window the notification fires at
most once: pin updates no longer clear the notified flag, and only a
new window resets the state. Additionally, only notify when the sleep
exit lands at a plausible local wake-up time ([4am, 6pm)); skipping
outside that window does not consume the once-per-window notification,
so a mid-night false exit stays silent and the real morning wake-up
still notifies. The timeline pin keeps updating silently in all cases.

The persisted SleepPinState grows by a trailing window_utc field. Older
persisted blobs fail the settings read cleanly (E_RANGE zeroes the
output), and a zero window is re-seeded at init as already-notified so
the state layout change cannot re-fire a summary the user already saw;
the cost is at most one missed summary right after the update.

Fixes FIRM-3101

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Joshua Jun <lets@throw.rocks>
AttributeIdIconTiny is a ResourceId-typed attribute, but the insight
notification builder added it with attribute_list_add_uint32, producing
the "Adding attribute with type uint32 for non-uint32_t attribute"
warning in field logs on every insight notification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Joshua Jun <lets@throw.rocks>
@jplexer
jplexer requested a review from gmarull as a code owner July 21, 2026 18:38
@jplexer
jplexer merged commit d3d5297 into coredevices:main Jul 21, 2026
42 checks passed
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