---
name: reminder_system
description: "Run reliable reminders an agent actually keeps: schedule one-shot and recurring reminders, poll them from cron, retry delivery with backoff, escalate unacknowledged ones up a role ladder, and keep a replayable audit trail. Use when an agent must remember and nudge something later (follow-ups, deadlines, health checks, SLA warnings) without dropping it silently."
metadata: { "includeInPrompt": true }
---

# Reminder System

## Purpose

A durable reminder pipeline that never loses an item silently: schedule reminders against a persistent store, deliver them through a pluggable channel, retry transient delivery failures with exponential backoff and jitter, escalate items that stay unacknowledged, and log every state transition to an append-only audit trail. One binary (`bin/remind.py`) implements the whole thing on SQLite, so it runs on any box without infrastructure.

## Workflow

1. **Create** the reminder with `remind.py add`: a message, a due time (ISO 8601 or relative like `+30m`, `+2h`, `+3d`), an optional `--repeat` interval, a priority, and an escalation ladder. Every reminder gets a stable id that doubles as the idempotency key for delivery.
2. **Poll** from cron: `remind.py due` lists items whose due time has passed and that are not acknowledged, exhausted, or in a quiet window. Run it every 1-5 minutes; it records a scan watermark so missed runs are rediscovered by the next scan, not by trusting the queue.
3. **Deliver** through a channel adapter: `remind.py deliver --channel stdout|log|exec --exec-cmd "..."` where `{id}`, `{message}`, `{due}` in the command are substituted. The channel write commits first, then the reminder transitions to `delivered`. Crash between send and commit = the next scan redelivers using the same id; the channel adapter must dedupe on that id.
4. **Retry**: transient failures (timeout, connection error, HTTP 429/502/503/504) requeue with backoff `base * 2^attempt` capped at `max`, with full jitter, up to `--max-attempts` (default 5). Permanent failures (validation, most other 4xx) go straight to `failed`, never retried. Ambiguous failures (timeout after send) retry only if the channel confirms idempotent delivery.
5. **Escalate**: `remind.py escalate` moves any delivered-but-unacknowledged reminder past its `--escalate-after` to the next level of its `--escalate-to` ladder (a comma-separated list of roles/channels). Recipients are roles, never individuals, so staff changes don't break the chain.
6. **Audit**: every event (`created`, `attempt`, `delivered`, `failed`, `ack`, `escalated`, `redriven`, `expired`) is appended to the `audit` table with timestamp, reminder id, actor, and detail JSON. Nothing is ever deleted; failed items park in `failed` for manual redrive.

## Output Contract

- The store is SQLite at the path given by `--db` (default `~/.reminders.db`): tables `reminders`, `audit`, `scan_watermark`.
- `remind.py list` and `due` emit one JSON object per line for easy piping into `jq`.
- `deliver` prints per-item `ok`/`retry`/`failed` verdicts and exits nonzero if anything failed permanently or exhausted attempts.
- A reminder is `done` only via explicit `ack <id>` (or auto-ack on a repeat's next scheduled occurrence). Silence never counts as success.
- Stale items past `--stale-after` (default 7d) transition to `expired` instead of being delivered: a reminder that missed its lateness objective is usually worse than silence, and the expiry is audited.

## Operating Rules

1. Classify before you retry: transient, permanent, ambiguous. When in doubt, classify as permanent and escalate a human decision; a mislabeled transient is a duplicate delivery, a mislabeled permanent is a lost reminder.
2. Keep per-reminder state in the database, never in the queue or the process. The scheduler may crash, duplicate, or skip runs; state convergence happens because the DB row is the single source of truth.
3. Cap everything up front: `--max-attempts`, backoff base/max, retry budget per run, and a concurrency limit for delivery. Set these before the first incident, not during it.
4. Quiet hours are per-reminder (`--quiet 22:00-07:00` with `--tz`): due items in a quiet window wait, and the wait is audited as `deferred`, not as a failure.
5. Repeats are generated as new rows with the same `series_id` on each due scan; never mutate the original row's due time, so history stays intact.
6. Redrive from `failed` only after the cause is known and idempotency is confirmed: `remind.py redrive <id> --note "..."`. Who did it and why goes in the audit row.
7. Deep details live in `references/patterns.md` (scan patterns, backoff math, escalation-ladder design, redrive runbook). Read it when designing a deployment; `SKILL.md` alone is enough for daily operation.
