# Quiet Hours and Timezone-Aware Delivery ## Goal Hold all notifications during sleep hours, merge held messages into the morning briefing, and adjust automatically when the user travels. ## What the User Gets Without this: 3 AM pings from cron jobs. One bad notification and the user disables the entire system. With this: the brain works overnight (dream cycle, collectors, enrichment) but notifications are held until morning. Travel to Tokyo? The system adjusts automatically from your calendar, no config change needed. ## Implementation ### Quiet Hours Gate Every cron job that sends notifications must check quiet hours FIRST. ``` QUIET_START = 23 // 11 PM local time QUIET_END = 8 // 8 AM local time is_quiet(local_hour): return local_hour >= QUIET_START OR local_hour < QUIET_END ``` **Before sending any notification:** 1. Determine user's current timezone (from config or heartbeat state) 2. Convert current UTC time to local time 3. If quiet hours: hold the message, don't send ### Held Messages During quiet hours, output goes to a held directory instead of being sent: ``` if is_quiet(): mkdir -p /tmp/cron-held/ write("/tmp/cron-held/{job-name}.md", output) exit // don't send else: send(output) ``` The morning briefing picks up held messages: ``` morning_briefing(): held_files = list("/tmp/cron-held/*.md") if held_files: briefing += "## Overnight Updates\n\n" for file in held_files: briefing += read(file) delete(file) ``` This way nothing is lost. Overnight cron results get folded into the first thing the user sees in the morning. ### Timezone Awareness The agent should know what timezone the user is in. Store it in the agent's operational state: ```json { "currentLocation": { "timezone": "US/Pacific", "city": "San Francisco" } } ``` **Update the timezone when:** - Calendar shows the user flying somewhere (check for airline/hotel events) - User mentions being in a different city - User's active hours shift (they're responding at 3 AM PT = they're probably traveling) **All times shown to the user should be in their LOCAL timezone.** Never show UTC or a timezone the user isn't in. ### Shell Implementation ```bash #!/bin/bash # quiet-hours-gate.sh — run before any notification TIMEZONE="${USER_TIMEZONE:-US/Pacific}" LOCAL_HOUR=$(TZ="$TIMEZONE" date +%H) if [ "$LOCAL_HOUR" -ge 23 ] || [ "$LOCAL_HOUR" -lt 8 ]; then echo "QUIET_HOURS=true" exit 1 # don't send fi echo "QUIET_HOURS=false" exit 0 # ok to send ``` **In cron job scripts:** ```bash # Check quiet hours first if ! bash scripts/quiet-hours-gate.sh; then mkdir -p /tmp/cron-held echo "$OUTPUT" > /tmp/cron-held/$(basename "$0" .sh).md exit 0 fi # Not quiet hours — send normally send_notification "$OUTPUT" ``` ### Configurable Hours Some users want different quiet hours. Store the config: ```json { "quiet_hours": { "start": 23, "end": 8, "enabled": true } } ``` Set `enabled: false` to disable quiet hours entirely (e.g., for 24/7 monitoring). ## Tricky Spots 1. **Gate on EVERY job.** The quiet hours check must run before every single cron job that produces notifications. If even one job skips the gate, the user gets a 3 AM ping and loses trust in the entire system. No exceptions. 2. **Held messages MUST be picked up.** If the morning briefing doesn't read `/tmp/cron-held/`, overnight results vanish silently. Verify the briefing skill reads and clears the held directory. Orphaned held files mean the pickup integration is broken. 3. **Timezone auto-detection is fragile.** Calendar-based timezone detection relies on the user having airline/hotel events with location data. If the user books travel without calendar entries, the system won't detect the move. Fall back to activity-hour analysis (responding at 3 AM PT = probably not in PT anymore) and ask the user if uncertain. ## How to Verify 1. **Set quiet hours to the current hour.** Temporarily set `QUIET_START` to one hour before now and `QUIET_END` to one hour after. Trigger a cron job. Verify the output goes to `/tmp/cron-held/` instead of being sent. 2. **Check held message pickup.** After step 1, run or simulate the morning briefing. Verify the held message appears in the "Overnight Updates" section and the file is deleted from `/tmp/cron-held/`. 3. **Verify timezone adjustment.** Change the timezone config to a zone where it's currently quiet hours. Trigger a notification. Verify it's held. Change back to your real timezone during active hours. Trigger again. Verify it sends. --- *Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*