Skip to content

feat(reminders): surface reminder scheduling failures loudly (Tier 1) - #1886

Merged
Aaronontheweb merged 3 commits into
devfrom
feat/surface-reminder-schedule-failures
Aug 11, 2026
Merged

feat(reminders): surface reminder scheduling failures loudly (Tier 1)#1886
Aaronontheweb merged 3 commits into
devfrom
feat/surface-reminder-schedule-failures

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

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
ScheduleDefinitionAsync could not compute the next fire — an unresolvable
CRON_TZ zone after tzdata/host drift, a cron with no future occurrence, an
uninitialized client — the post-fire reschedule and the startup reconcile only
wrote a log line. The reminder stayed Enabled, raised no alert, bumped no
counter, 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

  • operator notice) only half-implemented.

What changed

  • New ReportScheduleFailureAsync in ReminderManagerActor, a sibling of the
    execution-failure path. It reuses the existing seam — the persisted
    ConsecutiveFailures count, FailurePauseThreshold, the notification sink, and
    the channel-notice helper. No new counter, no new threshold, no config knob.
  • Both unattended reschedule sites now route failures through it: the post-fire
    reschedule
    and the startup reconcile restore loop (which previously
    dropped the failure silently).
  • New AlertType.ReminderScheduleFailed (Warning), appended so prior enum
    ordinals stay stable. Crossing the threshold reuses ReminderAutoDisabled
    (Critical) + a channel notice.
  • netclaw-operations skill updated with scheduling-failure guidance (version
    bump).

Key design decisions

  • No silent UTC fallback. An unresolvable zone fails loud — a wrong-time fire
    is worse than a missed one. Verified by test (a failed schedule installs no
    timer).
  • Reset only on successful execution, not on reschedule. The post-fire cron
    reschedule runs before that occurrence executes, so resetting there would
    erase pending execution-failure accumulation. Recovery rides the existing
    successful-execution reset.
  • Channel notice only on auto-disable. The Warning alert deduplicates per
    reminder in the sink, so a persistent fault does not storm the channel on every
    restart; only the terminal disable pings the channel.
  • A single bad startup cannot mass-disable. Reconcile bumps each reminder's
    count by one, so a transient boot fault raises alerts but disables nothing.
  • Health surfaced via the existing FailedCount — scheduling failures now
    appear there for free. The richer "enabled-but-unscheduled" count is a parked
    open question in design.md (would need an async health handler + a new
    message field).

Cross-boundary check

AlertType has no exhaustive switch/map consumer anywhere — the Slack payload
builder and webhook service render generically off Type/Summary/Severity
(the only switch is on AlertSeverity, with a safe default). So the new value
is handled with no silent default drop.

Tests

  • 4 new actor-level cases (fake notification sink, deterministic Feb-30 cron, no
    sleeps): surfacing + count, auto-disable + Critical alert, no-timer-installed
    (anti-pattern guard), and health FailedCount includes scheduling failures.
  • Full Reminders suite green: 158/158.
  • Two parked test gaps (documented in 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) and
grounding_cron_tz_schedule (action). They verify the model uses CRON_TZ for
local-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

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,
@Aaronontheweb Aaronontheweb added the reminders Reminder scheduling, execution, and history label Aug 11, 2026
@Aaronontheweb
Aaronontheweb merged commit 65acae5 into dev Aug 11, 2026
23 checks passed
@Aaronontheweb
Aaronontheweb deleted the feat/surface-reminder-schedule-failures branch August 11, 2026 23:48
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reminders Reminder scheduling, execution, and history

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant