{"record":{"id":"6a1ce4055a42df86","repo":"Hmbown/CodeWhale","slug":"unable-to-construct-hourly-anchor","errorCode":null,"errorMessage":"Unable to construct HOURLY anchor","messagePattern":"Unable to construct HOURLY anchor","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/tui/src/automation_manager.rs","lineNumber":439,"sourceCode":"                        \"Once schedule has no future run after {}\",\n                        after.to_rfc3339()\n                    )\n                }\n            }\n            Self::Hourly {\n                interval_hours,\n                byday,\n                anchor_hour,\n                anchor_minute,\n            } => {\n                if anchor_hour.is_some() || anchor_minute.is_some() {\n                    let local_anchor_reference = anchor_reference.with_timezone(timezone);\n                    let hour = anchor_hour.unwrap_or(local_anchor_reference.hour());\n                    let minute = anchor_minute.unwrap_or(0);\n                    let anchor_naive = local_anchor_reference\n                        .date_naive()\n                        .and_hms_opt(hour, minute, 0)\n                        .ok_or_else(|| anyhow::anyhow!(\"Unable to construct HOURLY anchor\"))?;\n                    let interval_seconds = i64::from(*interval_hours) * 60 * 60;\n                    let elapsed_seconds = local_after\n                        .naive_local()\n                        .signed_duration_since(anchor_naive)\n                        .num_seconds();\n                    let mut steps = if elapsed_seconds < 0 {\n                        0\n                    } else {\n                        elapsed_seconds / interval_seconds + 1\n                    };\n\n                    for _ in 0..MAX_HOURLY_SEARCH_STEPS {\n                        let hours = i64::from(*interval_hours)\n                            .checked_mul(steps)\n                            .ok_or_else(|| anyhow::anyhow!(\"HOURLY schedule exceeded its range\"))?;\n                        let delta = Duration::try_hours(hours)\n                            .ok_or_else(|| anyhow::anyhow!(\"HOURLY schedule exceeded its range\"))?;\n                        let candidate_naive = anchor_naive","sourceCodeStart":421,"sourceCodeEnd":457,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/automation_manager.rs#L421-L457","documentation":"When a HOURLY schedule carries BYHOUR/BYMINUTE, next_run builds a wall-clock anchor with NaiveDate::and_hms_opt(hour, minute, 0), which returns None only if hour > 23 or minute > 59. The RRULE parser already rejects those ranges for HOURLY, so in practice this is a defensive invariant branch: it means an out-of-range anchor reached the scheduler without going through parse-time validation.","triggerScenarios":"A stored automation record deserialized from an older version or hand-edited on disk with BYHOUR > 23 or BYMINUTE > 59, skipping parse_rrule's range checks; or future code that constructs AutomationSchedule::Hourly directly without validating.","commonSituations":"Automations persisted before the current validation existed; manual edits to the automation store; downgraded binaries scheduling records written by a newer schema.","solutions":["Keep BYHOUR in 0-23 and BYMINUTE in 0-59 in the RRULE","Update or re-create the automation through the supported tooling so parse_rrule re-validates the stored record","Audit the automation store for out-of-range anchor values if this error appears in logs"],"exampleFix":"// before (stored record that bypassed parse-time checks)\n\"FREQ=HOURLY;INTERVAL=24;BYHOUR=25;BYMINUTE=30\"\n// after\n\"FREQ=HOURLY;INTERVAL=24;BYHOUR=8;BYMINUTE=30\"","handlingStrategy":"validation","validationCode":"let (hour, minute) = (anchor_hour.unwrap_or(reference.hour()), anchor_minute.unwrap_or(0));\nensure!(hour <= 23 && minute <= 59, \"anchor time {hour:02}:{minute:02} is not a valid wall clock\");","typeGuard":"fn hourly_anchor_in_range(anchor_hour: Option<u32>, anchor_minute: Option<u32>) -> bool {\n    anchor_hour.is_none_or(|h| h <= 23) && anchor_minute.is_none_or(|m| m <= 59)\n}","tryCatchPattern":null,"preventionTips":["Never bypass parse_rrule when writing automation records — it enforces BYHOUR 0-23 and BYMINUTE 0-59","Re-validate stored records after upgrades that changed the RRULE grammar","Do not hand-edit the automation store; use update actions so validation re-runs","Treat this error as an invariant tripwire: its appearance means stale data, not user error"],"tags":["automation","rrule","scheduler","defensive"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}