Schedules: store + schedule/list/cancel verbs + timer (delete old scheduler) #60

Closed
opened 2026-07-08 02:12:43 +08:00 by weiwen · 0 comments
Owner

Parent

#55

What to build

Dynamic, persisted Schedules — a Schedule is an instruction to run a delivery Verb (send/prompt/run) against a chat at a future time, replacing the old static [[schedule]] config entirely.

Grammar (argv-wrap): evie schedule (--in <dur> | --at <ts> | --cron <expr>) [--name <name>] [--update] [--chat <id>] -- <verb> [args…]. Timing flags are a required, mutually-exclusive group. At creation the Daemon eagerly parses and validates the trailing verb, rejects anything but send|prompt|run, resolves and freezes the target chat, then stores the normalized argv. At fire time the Daemon re-dispatches the stored argv in-process (no re-exec), so fire-time behavior — notably prompt-script resolution via PromptSource — is identical to an immediate run.

Identity & upsert: primary key (Chat ID, name). --name omitted → generate a short opaque handle and print it. Name exists without --update → error. --update → replace if present, create if absent (upsert). Names scoped per chat.

Store & timer: persist to ~/.config/evie/schedules.json (atomic temp+rename, reloaded on Daemon startup — same idiom as chat_sessions.json). A timer fires due Schedules by re-dispatching. Firing decisions ("is this one-shot due / missed?", "next occurrence") are pure functions over an injected now, extracted from the tokio::sleep loop. A one-shot whose time elapsed during downtime fires late and flagged (" due at T (evie was offline)"); a recurring Schedule fires at its next occurrence. --cron and --at evaluated in chrono::Local.

Management verbs: list (human table by default; --json emits raw entries {name, chat, schedule, argv, created_at, next_fire}, scoped to the resolved chat) and cancel --name <name> (prints confirmation; non-zero exit + not-found message if absent; no --all).

Fired-run env: when a Schedule fires a run, additionally inject EVIE_SCHEDULE_NAME and EVIE_SCHEDULE_TIME (local, ISO 8601).

Deletions: remove [[schedule]], ScheduleConfig, and scheduler_task. No migration — a stale [[schedule]] table is simply ignored (serde unknown field).

Demoable: evie schedule --in 10s -- send "ping" fires ~10s later; evie schedule --cron "0 8 * * *" -- prompt "morning digest" recurs; evie list shows both; evie cancel --name <n> removes one; a Schedule survives a daemon restart.

Acceptance criteria

  • evie schedule creates a Schedule wrapping send/prompt/run; wrapping any other verb is rejected at creation.
  • --in/--at/--cron are mutually exclusive and exactly one is required; --at/--cron interpreted in local time.
  • The target chat is frozen at creation; the Schedule fires against it regardless of the creating caller's environment.
  • --name collision errors without --update; --update upserts; omitted --name generates and prints a handle; names are unique per chat.
  • Schedules persist to schedules.json (atomic temp+rename) and reload on startup — verified by a restart surviving a pending Schedule.
  • A missed one-shot fires late and flagged; a recurring Schedule fires at its next occurrence after downtime.
  • evie list shows the resolved chat's Schedules; --json emits parseable entries. evie cancel --name removes one and errors on not-found.
  • A fired run receives EVIE_SCHEDULE_NAME + EVIE_SCHEDULE_TIME in addition to EVIE_CHAT_ID + EVIE_SOCKET.
  • [[schedule]], ScheduleConfig, and scheduler_task are removed; a config still containing [[schedule]] starts without error.
  • Tests: CLI-parse (grammar, timing mutual-exclusion, send|prompt|run whitelist, --name/--update); dispatch-core CRUD (collision→error, update upsert, per-chat scoping, list, cancel not-found); pure firing-decision functions (due / missed-late / next-occurrence / local time, no tokio::sleep); store round-trip incl. frozen chat + argv.
  • Uses CONTEXT.md vocabulary (Schedule); respects ADR 0006.

Blocked by

## Parent #55 ## What to build Dynamic, persisted **Schedules** — a **Schedule** is an instruction to run a delivery Verb (`send`/`prompt`/`run`) against a chat at a future time, replacing the old static `[[schedule]]` config entirely. **Grammar (argv-wrap):** `evie schedule (--in <dur> | --at <ts> | --cron <expr>) [--name <name>] [--update] [--chat <id>] -- <verb> [args…]`. Timing flags are a required, mutually-exclusive group. At creation the Daemon eagerly parses and validates the trailing verb, rejects anything but `send|prompt|run`, resolves and **freezes the target chat**, then stores the normalized argv. At fire time the Daemon re-dispatches the stored argv **in-process** (no re-exec), so fire-time behavior — notably prompt-script resolution via `PromptSource` — is identical to an immediate run. **Identity & upsert:** primary key `(Chat ID, name)`. `--name` omitted → generate a short opaque handle and print it. Name exists without `--update` → error. `--update` → replace if present, create if absent (upsert). Names scoped per chat. **Store & timer:** persist to `~/.config/evie/schedules.json` (atomic temp+rename, reloaded on Daemon startup — same idiom as `chat_sessions.json`). A timer fires due Schedules by re-dispatching. Firing decisions ("is this one-shot due / missed?", "next occurrence") are **pure functions over an injected `now`**, extracted from the `tokio::sleep` loop. A one-shot whose time elapsed during downtime fires late and flagged ("⏰ due at T (evie was offline)"); a recurring Schedule fires at its next occurrence. `--cron` and `--at` evaluated in `chrono::Local`. **Management verbs:** `list` (human table by default; `--json` emits raw entries `{name, chat, schedule, argv, created_at, next_fire}`, scoped to the resolved chat) and `cancel --name <name>` (prints confirmation; non-zero exit + not-found message if absent; no `--all`). **Fired-`run` env:** when a Schedule fires a `run`, additionally inject `EVIE_SCHEDULE_NAME` and `EVIE_SCHEDULE_TIME` (local, ISO 8601). **Deletions:** remove `[[schedule]]`, `ScheduleConfig`, and `scheduler_task`. No migration — a stale `[[schedule]]` table is simply ignored (serde unknown field). Demoable: `evie schedule --in 10s -- send "ping"` fires ~10s later; `evie schedule --cron "0 8 * * *" -- prompt "morning digest"` recurs; `evie list` shows both; `evie cancel --name <n>` removes one; a Schedule survives a daemon restart. ## Acceptance criteria - [ ] `evie schedule` creates a Schedule wrapping `send`/`prompt`/`run`; wrapping any other verb is rejected at creation. - [ ] `--in`/`--at`/`--cron` are mutually exclusive and exactly one is required; `--at`/`--cron` interpreted in local time. - [ ] The target chat is frozen at creation; the Schedule fires against it regardless of the creating caller's environment. - [ ] `--name` collision errors without `--update`; `--update` upserts; omitted `--name` generates and prints a handle; names are unique per chat. - [ ] Schedules persist to `schedules.json` (atomic temp+rename) and reload on startup — verified by a restart surviving a pending Schedule. - [ ] A missed one-shot fires late and flagged; a recurring Schedule fires at its next occurrence after downtime. - [ ] `evie list` shows the resolved chat's Schedules; `--json` emits parseable entries. `evie cancel --name` removes one and errors on not-found. - [ ] A fired `run` receives `EVIE_SCHEDULE_NAME` + `EVIE_SCHEDULE_TIME` in addition to `EVIE_CHAT_ID` + `EVIE_SOCKET`. - [ ] `[[schedule]]`, `ScheduleConfig`, and `scheduler_task` are removed; a config still containing `[[schedule]]` starts without error. - [ ] Tests: CLI-parse (grammar, timing mutual-exclusion, `send|prompt|run` whitelist, `--name`/`--update`); dispatch-core CRUD (collision→error, update upsert, per-chat scoping, list, cancel not-found); pure firing-decision functions (due / missed-late / next-occurrence / local time, no `tokio::sleep`); store round-trip incl. frozen chat + argv. - [ ] Uses CONTEXT.md vocabulary (Schedule); respects ADR 0006. ## Blocked by - #58 - #59
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
weiwen/evie#60
No description provided.