fix: hold DAYLIGHT through the morning instead of cascading to NIGHT#7
Merged
Conversation
The DAYLIGHT branch of the rhythm phase engine compared the noon-shifted clock (`_noon_shifted(minute) = (minute - 720) % 1440`) against the evening thresholds with no morning guard. `_noon_shifted` maps morning minutes (00:00-11:59) to 720-1439, above every evening threshold, so the moment the phase reached DAYLIGHT before noon -- every morning when MORNING elapses at ~08:00, and on any daemon restart that seeds DAYLIGHT mid-morning -- the next tick fell DAYLIGHT -> WIND_DOWN -> NIGHT. In continuous operation the NIGHT morning-escape then bounced it back to MORNING, so the engine flapped hourly until ~noon and re-recorded a bogus ~11:00 wake anchor on each loop, corrupting the learned wake time. With no wake evidence -- e.g. a restart while presence has never been judged human -- it instead stuck in NIGHT for the whole day. Gate the DAYLIGHT branch's evening-ward transitions on `minute >= 720`, the same noon boundary the seed branch already uses. EVENING is left unguarded on purpose: it is never entered before noon in normal forward progression, and a post-midnight `bed_target` legitimately winds down from EVENING in the morning half, so guarding it would strand that config in EVENING forever. Tests: three regression tests reproduce the cascade on the MORNING->DAYLIGHT path, the restart-seed path, and a full-day walk; one guards the post-midnight `bed_target=02:00` path so the fix is not over-applied to EVENING. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
The rhythm phase engine's
DAYLIGHTbranch cascaded toNIGHTwhenever the day reachedDAYLIGHTbefore noon — every morning under normal operation, and permanently after a mid-morning daemon restart.Root cause
_noon_shifted(minute) = (minute - 720) % 1440maps morning minutes (00:00–11:59) onto 720–1439, which is above every evening threshold. TheDAYLIGHTbranch comparedm_shiftagainstwind_down_start/ sunset with no morning guard, so the momentMORNINGelapsed toDAYLIGHTat ~08:00 (or a restart seededDAYLIGHTmid-morning), the next tick fellDAYLIGHT → WIND_DOWN → NIGHT.Two manifestations, both confirmed by tracing the transition table:
NIGHTmorning-escape bounced it back toMORNING, so the engine flapped ~hourly until ~noon then self-healed — but each escape called_record_wake, dragging the learned wake anchor to ~11:00, which corrupts the observe stage's whole point (learning the real wake time).NIGHTfor the entire day (this is what a fresh mid-morning daemon start produced in the field).Fix
Gate the
DAYLIGHTbranch's evening-ward transitions onminute >= 720— the same noon boundary the seed branch already uses (if minute < 720: return DAYLIGHT).EVENINGis deliberately not guarded: it is never entered before noon in normal forward progression, and a post-midnightbed_targetlegitimately winds down fromEVENINGin the morning half (minute < 720). GuardingEVENINGonminute >= 720would make itsEVENING → WIND_DOWNtransition unsatisfiable and strand that config inEVENINGforever.Testing
MORNING → DAYLIGHTpath, the restart-seed path, and a full-day walk that also proves the normal evening progression still fires.bed_target=02:00path (EVENING → WIND_DOWNat 00:30 →NIGHTat 02:00) so the fix is not over-applied toEVENING.mypyclean.Related findings (not fixed here)
An independent audit of the same module surfaced separate latent issues, all out of scope for this focused fix and worth tracking separately:
_wake_anchor_minaccepts any phone alarm within the next 18h as the wake anchor with no morning-plausibility clamp — a non-morning alarm would blank out wake detection for the day. Dormant today (nonext_alarmsignal is delivered), real once that signal is wired.SLEEPand normally-reachedNIGHTlack the clock-based missed-wake failsafe thatDAWNhas.🤖 Generated with Claude Code