Fix the 3am notification avalanche and the failing morning digest

Three bugs from the first day of real use.

1. Quiet hours were evaluated in UTC. The router is handed a UTC `now` and
   compared its hour against "22:00-07:00" directly, so notifications were
   silenced from 18:00 local -- prime reading time -- and the whole day's budget
   was released at 03:00 local. The reported avalanche landed at 03:39 and 03:55.
   Quiet hours are now converted to the reader's wall clock first.

2. The timezone never resolved anyway. TZ was "America/Louisville", which exists
   on this Arch host but NOT in a Debian container, which ships only the
   canonical "America/Kentucky/Louisville". The container fell back to UTC and
   `date` printed "America" as the zone abbreviation. The zone is now read from
   config rather than the environment, so a bad TZ cannot silently change
   behaviour, and the compose entry uses the canonical name. NOTE: ~20 other
   services in compose.yml carry the same bad string; they were left alone since
   it only affects their log timestamps.

3. Budgets are not a rate. Six notifications fired in a single minute because
   the daily allowance could all be spent in one cycle. Each lane now has a
   minimum gap (interest 75min, importance 30min) and sends at most one per
   cycle, which turns the budget into a rate spread across the day.

Separately, the morning digest had silently fallen back to the templated list.
The real 28-story payload takes 70.3s to generate against a 60s timeout. Raised
to 180s -- and write_prose now LOGS why it failed instead of returning None in
silence, which is what made a one-line timeout cost a debugging session.
This commit is contained in:
connor committed 2026-09-09 09:55:49 -04:00
1 parent 161c518aa7
commit 98bba62976
8 files changed
+151 -4

No files matched your search

+9 -1
View File
@@ -88,6 +88,12 @@ important_per_day = 2
# Hard cap on waking the reader during quiet hours, inside important_per_day.
important_per_night = 1
# Minimum spacing between sends on a lane. Budgets alone are not a rate: without
# these the whole day's allowance fires in one cycle, and six notifications
# arrived in the same minute.
interest_min_gap_minutes = 75
important_min_gap_minutes = 30
# Thresholds are in z-score units -- "how many standard deviations above normal
# for this block", not raw cosine. Calibrate with `news simulate`, which reports
# the notifications-per-day these values would actually have produced.
@@ -141,4 +147,6 @@ enabled = true
base_url = "http://litellm:4000"
model = "news/glm-5.3-flash"
api_key_env = "NEWS_LITELLM_KEY"
timeout_secs = 60
# 180, not 60: the real digest payload (28 stories) took 70.3s to generate, so a
# 60s timeout failed every morning and silently fell back to the templated list.
timeout_secs = 180