Skip to content

feat(reminders): operator visibility into reminder failures and skips (#1494)#1503

Merged
Aaronontheweb merged 4 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/1494-reminder-failure-visibility
Jun 26, 2026
Merged

feat(reminders): operator visibility into reminder failures and skips (#1494)#1503
Aaronontheweb merged 4 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/1494-reminder-failure-visibility

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

What

Fixes #1494. Reminder failures were invisible to operators — only a buried daemon log line — so the gotowebinar reminder silently skipped 49 fires in a day before anyone noticed. This surfaces failures where operators actually look, and adds a status command.

Changes

1. Failure → destination channel (your primary ask)

When a channel-delivery reminder execution fails for any reason — including the #1500 stall backstop recovering a wedged run, or hitting the auto-disable threshold — a plain-language notice is posted to the reminder's own destination channel, where the operator expects that reminder's output.

  • New IReminderChannelNotifier seam: interface in Netclaw.Actors (transport-agnostic — the manager hands over an already-resolved ChannelDeliveryTargetInfo), implemented in Netclaw.Daemon over IChannelRegistry. Keeps the actor boundary transport-agnostic per the constitution.
  • Bounded, not spammy: at most a few notices plus the disabled notice (auto-disable threshold). Skips are not posted (they can be many) — only counted.
  • NullReminderChannelNotifier Null Object for tests / no-channel daemons — a required dependency (per the new "no optional/nullable deps" rule from fix(tools): allow autonomous sessions to write the workspace directory (#1493) #1498), not a nullable one.

2. Skipped-duplicate visibility

The two skip sites (scheduled fire + deferred-queue dequeue) now increment an in-memory per-reminder counter instead of only logging — so the silent-skip pattern is finally countable.

3. netclaw reminder status <id>

New CLI subcommand + GET /api/reminders/{id}/status REST endpoint, backed by a new GetReminderStatusQuery the manager answers from live state: enabled, in-flight right now, next fire, consecutive failures, skipped-fire count (since daemon start), and recent run history. The at-a-glance "is this reminder healthy or silently failing?" view the incident lacked.

How it fits with #1500

#1500 makes a wedged reminder recover (fail after ~20 min instead of forever). #1494 makes that recovery visible: the failure posts to the channel, and the skips that piled up in between are countable via status. Together they close the incident.

Layering

ReminderManagerActor (in Netclaw.Actors) resolves the channel target via ReminderExecutionActor.ResolveChannelDeliveryTarget (same assembly) and hands it to the notifier; the daemon-side notifier owns the actual channel post. The actor never references channel/transport types.

Tests

  • ReminderChannelFailureNotifierTests: posts the correct ChannelSendRequest for both destination and direct_message kinds (real fire-and-forget awaited via a capture signal, no sleeps).
  • ReminderManagerActorTests: status query returns per-reminder health; unknown reminder → not-found.
  • Full Netclaw.Actors.Tests (2482) and Netclaw.Daemon.Tests (754) green. Slopwatch clean (3 pre-existing warnings, unrelated files); headers verified.

Docs

Updates the netclaw-operations skill (scheduling reference) with the failure-to-channel behavior and the reminder status command; bumps metadata.version 2.19.0 → 2.20.0.

Gate not run locally

The behavioral eval suite (skill-content edit) needs a live model endpoint not configured here — should run in CI / before merge.

…netclaw-dev#1494)

Reminder failures were invisible: a stuck/failing reminder only emitted a daemon
log warning, so the gotowebinar incident skipped 49 fires in a day unnoticed.
This surfaces failures where operators look.

- Failure → destination channel: when a Channel-delivery reminder execution
  fails for any reason (including the netclaw-dev#1500 stall backstop, or auto-disable), a
  plain-language notice is posted to the reminder's own destination channel via a
  new IReminderChannelNotifier seam — interface in Netclaw.Actors (transport-
  agnostic, hands over a resolved ChannelDeliveryTargetInfo), implemented in
  Netclaw.Daemon over IChannelRegistry. Bounded by the auto-disable threshold, so
  no channel spam. Skips are NOT posted (too noisy) — only counted.

- Skipped-duplicate visibility: the two skip sites (scheduled + deferred queue)
  now increment an in-memory per-reminder counter (since daemon start) instead of
  only logging.

- `netclaw reminder status <id>`: new CLI subcommand + GET /api/reminders/{id}/
  status REST endpoint, backed by a new GetReminderStatusQuery the manager
  answers from live state — enabled, in-flight, next fire, consecutive failures,
  skipped fires, and recent run history.

The actor stays transport-agnostic: ReminderManagerActor resolves the channel
target (same assembly) and the daemon notifier owns the channel post; a
NullReminderChannelNotifier Null Object serves tests / no-channel daemons (a
required dependency, not a nullable one).

Tests: ReminderChannelFailureNotifier posts the right ChannelSendRequest
(destination + DM kinds); status query returns per-reminder health and not-found.
Full Netclaw.Actors.Tests (2482) and Netclaw.Daemon.Tests (754) green. Updates the
netclaw-operations skill (scheduling reference, 2.19.0 → 2.20.0).

Note: the behavioral eval suite (skill-content edit) needs a live model endpoint
not available here — run in CI / before merge.
Verified findings from the workflow code review:

- LlmSessionTestBase: register IReminderChannelNotifier (the new required
  reminder-manager dependency) so DI-resolved reminder managers in session
  integration tests initialize instead of dying with ActorInitializationException.
- HandleGetStatusAsync: a transient read failure (schedule fetch / history IO)
  no longer impersonates "not found" — it faults the Ask (Status.Failure → the
  endpoint maps to 5xx) so the operator sees a real error instead of being told a
  wedged reminder was deleted. That was a silent fallback on the very feature
  meant to expose them.
- Status query: read and display the same RecentHistoryCount (5) records, newest
  first — the CLI previously showed the OLDEST 5 of a last-10 window, hiding the
  most recent (failing) runs.
- Channel notice: post the per-failure notice only below the auto-disable
  threshold; on the threshold-hitting failure the disabled notice already carries
  the last error, so both was double noise on the most important event.
- NextFire: pass null through (FormatTimestamp renders null as "unknown"); the CLI
  shows "not scheduled" for disabled/unscheduled reminders.
- Status handler runs its two independent backend reads concurrently (Task.WhenAll).

Full Netclaw.Actors.Tests (2482) green; reminder + Daemon notifier tests green.

Not changed (verified lower-severity / pre-existing patterns): whole-list
scheduler fetch (matches existing GetReminder), notifier↔ChannelSendTools and
CLI-subcommand scaffolding duplication.

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

/// Fire-and-forget (<c>void</c>) — the manager must never block on channel
/// delivery, mirroring <see cref="Configuration.IOperationalNotificationSink"/>.
/// </summary>
public interface IReminderChannelNotifier

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

public void NotifyFailure(ChannelDeliveryTargetInfo target, string text)
=> _ = PostAsync(target, text);

private async Task PostAsync(ChannelDeliveryTargetInfo target, string text)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

// unbounded skip stream that #1494 makes visible via status instead.
if (count < FailurePauseThreshold)
{
PostFailureNoticeToChannel(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) June 26, 2026 17:05
@Aaronontheweb
Aaronontheweb merged commit 6c26283 into netclaw-dev:dev Jun 26, 2026
15 checks passed
This was referenced Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: no operator visibility into reminder execution failures

1 participant