Skip to content

fix: hold DAYLIGHT through the morning instead of cascading to NIGHT#7

Merged
cnewkirk merged 1 commit into
mainfrom
fix/rhythm-morning-phase-cascade
Jul 6, 2026
Merged

fix: hold DAYLIGHT through the morning instead of cascading to NIGHT#7
cnewkirk merged 1 commit into
mainfrom
fix/rhythm-morning-phase-cascade

Conversation

@cnewkirk

@cnewkirk cnewkirk commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

The rhythm phase engine's DAYLIGHT branch cascaded to NIGHT whenever the day reached DAYLIGHT before noon — every morning under normal operation, and permanently after a mid-morning daemon restart.

Root cause

_noon_shifted(minute) = (minute - 720) % 1440 maps morning minutes (00:00–11:59) onto 720–1439, which is above every evening threshold. The DAYLIGHT branch compared m_shift against wind_down_start / sunset with no morning guard, so the moment MORNING elapsed to DAYLIGHT at ~08:00 (or a restart seeded DAYLIGHT mid-morning), the next tick fell DAYLIGHT → WIND_DOWN → NIGHT.

Two manifestations, both confirmed by tracing the transition table:

  • Continuous operation: the NIGHT morning-escape bounced it back to MORNING, 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).
  • Restart with no wake evidence: with no human-judged presence to trigger the escape, it simply stuck in NIGHT for the entire day (this is what a fresh mid-morning daemon start produced in the field).

Fix

Gate the DAYLIGHT branch's evening-ward transitions on minute >= 720 — the same noon boundary the seed branch already uses (if minute < 720: return DAYLIGHT).

EVENING is deliberately not guarded: 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 (minute < 720). Guarding EVENING on minute >= 720 would make its EVENING → WIND_DOWN transition unsatisfiable and strand that config in EVENING forever.

Testing

  • 3 new regression tests reproduce the cascade and fail on the unfixed code — the MORNING → DAYLIGHT path, the restart-seed path, and a full-day walk that also proves the normal evening progression still fires.
  • 1 new test guards the post-midnight bed_target=02:00 path (EVENING → WIND_DOWN at 00:30 → NIGHT at 02:00) so the fix is not over-applied to EVENING.
  • Full suite: 360 passed; strict mypy clean.

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_min accepts 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 (no next_alarm signal is delivered), real once that signal is wired.
  • Bedroom-pet motion can satisfy the wake gate, bypassing the pet-discount logic.
  • SLEEP and normally-reached NIGHT lack the clock-based missed-wake failsafe that DAWN has.

🤖 Generated with Claude Code

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>
@cnewkirk
cnewkirk merged commit 3bf78cf into main Jul 6, 2026
1 check passed
@cnewkirk
cnewkirk deleted the fix/rhythm-morning-phase-cascade branch July 6, 2026 15:11
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.

1 participant