feat(reminders): surface reminder scheduling failures loudly (Tier 1) - #1886
Merged
Conversation
Route reminder scheduling failures through the same alert, counter, and auto-disable path that execution failures use. Today the post-fire reschedule and the startup reconcile drop the failure, so a reminder can stay enabled and never fire without any alert. Artifacts: proposal, design, specs delta (netclaw-scheduling), tasks. The health-message-shape choice is parked in design.md as an open fork.
Reminder scheduling failures were silent. The post-fire reschedule and the startup reconcile only logged a failed schedule, so a reminder could stay enabled and never fire with no alert and no failure count. Route both unattended reschedule sites through a new ReportScheduleFailureAsync, a sibling of the execution-failure path. It reuses the shared ConsecutiveFailures count, the FailurePauseThreshold, the notification sink, and the channel-notice helper. A scheduling failure now emits a ReminderScheduleFailed (Warning) alert and counts toward auto-disable. Crossing the threshold disables the reminder and emits the ReminderAutoDisabled (Critical) alert plus a channel notice. An unresolvable schedule never falls back to a different time; it fails loud. The shared count resets on a successful execution, not on a reschedule, so cron execution-failure accumulation is preserved. - add AlertType.ReminderScheduleFailed (appended; ordinal-stable) - wire post-fire reschedule and reconcile restore loop - update netclaw-operations skill (2.46.0 -> 2.47.0) - tests: 4 actor-level cases; full Reminders suite 136/136 Refs PRD-008 SCHED-007. OpenSpec change: surface-reminder-schedule-failures.
Two cases now that CRON_TZ (#1789) is in dev: - skill_cron_tz_timezone (Skill Discovery): the model surfaces the CRON_TZ prefix for a local-time schedule, recovered from the netclaw-operations skill. - grounding_cron_tz_schedule (Grounding): the model calls set_reminder AND carries CRON_TZ into the schedule, instead of silently converting to UTC. Pairs with this branch's scheduling-failure surfacing: an unresolvable CRON_TZ zone now fails loud and counts toward auto-disable rather than dying silently.
Comment on lines
+1031
to
+1035
| catch (Exception ex) | ||
| { | ||
| EmitSettlementFailure(definition, ex.Message, ex); | ||
| return; | ||
| } |
| definition = definition with | ||
| { | ||
| ConsecutiveFailures = count, | ||
| Enabled = thresholdReached ? false : definition.Enabled, |
This was referenced Aug 13, 2026
Aaronontheweb
added a commit
that referenced
this pull request
Aug 20, 2026
* Sync delta specs for the last two completed changes Apply the redirect-shared-temp-to-session-scratch delta to session-cwd and tool-approval-gates. The OpenSpec CLI applied both files. Merge the surface-reminder-schedule-failures delta into netclaw-scheduling by hand. PR #2024 already corrected that requirement against the merged code, so a programmatic apply would undo those corrections. Keep the corrected text and add the scheduling-failure content: - An execution failure and a scheduling failure share one ConsecutiveFailures count. - A successful reschedule alone does not reset the count. - An unattended reschedule site emits ReminderScheduleFailed at Warning severity, and ReminderAutoDisabled at Critical severity on the threshold. - The set_reminder create path and update path return the error to the caller and emit no alert. Drop three stale delta claims. The merged code disables a reminder. It does not set a "paused" status and it does not write tasks.json. ReminderExecutionActor has no ExecutionTimeoutSeconds constant, so the one-hour absolute limit stands. * Archive the last two completed OpenSpec changes Move each change to openspec/changes/archive/2026-08-19-<name>/. The code of both changes is merged on dev. - surface-reminder-schedule-failures: PR #1886. - redirect-shared-temp-to-session-scratch: PRs #1890, #1925, #1926, #1945, and #1956. Tick task 4.3 of redirect-shared-temp-to-session-scratch. The full pr_validation suite ran green on Windows against current dev. Leave the other boxes unticked. The maintainer accepts tasks 4.5 and 4.6 of surface-reminder-schedule-failures as guaranteed by construction. The maintainer accepts task 6.4 of that change and task 4.4 of redirect-shared-temp-to-session-scratch as an eval-credentials gap.
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.
Why
Netclaw surfaces reminder execution failures loudly — it tracks consecutive
failures, auto-disables at a threshold, emits an operational alert, and posts a
channel notice. Reminder scheduling failures got none of that. When
ScheduleDefinitionAsynccould not compute the next fire — an unresolvableCRON_TZzone after tzdata/host drift, a cron with no future occurrence, anuninitialized client — the post-fire reschedule and the startup reconcile only
wrote a log line. The reminder stayed
Enabled, raised no alert, bumped nocounter, and never fired again.
That is a silent failure, which the constitution's "No silent fallbacks — fail
loudly" rule forbids, and it left PRD-008 SCHED-007 (consecutive-failure tracking
What changed
ReportScheduleFailureAsyncinReminderManagerActor, a sibling of theexecution-failure path. It reuses the existing seam — the persisted
ConsecutiveFailurescount,FailurePauseThreshold, the notification sink, andthe channel-notice helper. No new counter, no new threshold, no config knob.
reschedule and the startup reconcile restore loop (which previously
dropped the failure silently).
AlertType.ReminderScheduleFailed(Warning), appended so prior enumordinals stay stable. Crossing the threshold reuses
ReminderAutoDisabled(Critical) + a channel notice.
netclaw-operationsskill updated with scheduling-failure guidance (versionbump).
Key design decisions
is worse than a missed one. Verified by test (a failed schedule installs no
timer).
reschedule runs before that occurrence executes, so resetting there would
erase pending execution-failure accumulation. Recovery rides the existing
successful-execution reset.
reminder in the sink, so a persistent fault does not storm the channel on every
restart; only the terminal disable pings the channel.
count by one, so a transient boot fault raises alerts but disables nothing.
FailedCount— scheduling failures nowappear there for free. The richer "enabled-but-unscheduled" count is a parked
open question in
design.md(would need an async health handler + a newmessage field).
Cross-boundary check
AlertTypehas no exhaustiveswitch/map consumer anywhere — the Slack payloadbuilder and webhook service render generically off
Type/Summary/Severity(the only
switchis onAlertSeverity, with a safe default). So the new valueis handled with no silent default drop.
Tests
sleeps): surfacing + count, auto-disable + Critical alert, no-timer-installed
(anti-pattern guard), and health
FailedCountincludes scheduling failures.tasks.md): the post-fire "still executes"path and the recovery reset are covered by construction — both need a
fire-that-then-fails-to-reschedule, only reproducible under real tz drift.
Evals
Adds two local-timezone eval cases, now viable because
CRON_TZ(#1789 by@nixie-ai) is in
dev:skill_cron_tz_timezone(knowledge) andgrounding_cron_tz_schedule(action). They verify the model usesCRON_TZforlocal-time schedules instead of silently assuming UTC — and this branch gives
that capability a safety net when a zone will not resolve.
Note: the local eval model is weak at skill-activation decisions across the whole
suite (unrelated to this change); the scheduling/skill-discovery cases this
change touches hold at pass. Run the eval gate in a production-model environment
before merge.
Notes
dev(includes feat(reminders): support CRON_TZ prefix for timezone-aware cron schedules #1789CRON_TZ).scheduling.mdcarriesboth the
CRON_TZsection and the new scheduling-failure section; skill versionresolved to
2.49.0.surface-reminder-schedule-failuresOpenSpec change (proposal,design, specs delta on
netclaw-scheduling, tasks).