Control Plane: split evie into daemon + CLI with Verbs and dynamic Schedules #55

Closed
opened 2026-07-08 02:06:16 +08:00 by weiwen · 1 comment
Owner

Problem Statement

Scheduled prompts today are static [[schedule]] TOML entries fired by an in-process scheduler_task. As a user I can only create a scheduled prompt by editing config and restarting the bot; I can't ask evie to remind me of something from a conversation, I can't schedule anything from an external script, and every scheduled prompt is forced to target one hardcoded chat with silent delivery. The agent can't schedule its own follow-ups, and nothing outside the process can drive evie at all.

Solution

Split evie into a Daemon (evie daemon — the long-running process: Telegram, HTTP API, Session manager, and a new Schedule store + timer) and a CLI (evie <verb> — short-lived invocations that issue one Verb to a running Daemon over a local Control Plane and exit). The same binary is both roles; the subcommand selects which.

The Control Plane is a local Unix-domain socket (JSON-lines, mode 0600), never network-exposed, authed by filesystem permissions. Its Verbs let humans at a shell, external scripts, and — via a skill — the notes agent all drive evie through one interface:

  • Send — deliver verbatim text to a chat (no pi).
  • Prompt — run text through pi and deliver the response to the chat.
  • Query — run text through pi and return the response to the caller, delivering nothing.
  • Run — execute a local script with evie's env context populated, fire-and-forget.
  • Schedule / List / Cancel — manage Schedules.

A Schedule argv-wraps one delivery Verb and runs it once (--in/--at) or recurring (--cron), persisted so it survives restarts. "Remind me in 1 hour to check X" becomes the agent running evie schedule --in 1h -- prompt "..."; "morning digest" becomes evie schedule --cron "0 8 * * *" -- prompt "...", replacing the old static config entirely.

User Stories

  1. As a user, I want to tell evie in conversation "remind me in 1 hour to check the deploy", so that I get a message an hour later without touching config.
  2. As a user, I want to say "every morning at 8, give me a digest", so that a recurring Prompt runs on a cron schedule I created at runtime.
  3. As a user, I want to cancel a reminder I set earlier, so that I stop receiving a Schedule I no longer want.
  4. As a user, I want to ask "what reminders do I have?", so that I can see my pending Schedules.
  5. As a user, I want a reminder I set to still fire after the bot restarts, so that scheduling survives deploys and reboots.
  6. As a user, I want a one-shot reminder whose time passed while the bot was offline to still reach me (flagged as late), so that I never silently miss a reminder.
  7. As a user, I want a recurring Schedule to simply resume at its next occurrence after downtime, so that missed recurring fires don't pile up.
  8. As an external script author, I want to run evie send to post a verbatim message to my chat, so that my own tooling can notify me through evie without invoking pi.
  9. As an external script author, I want to run evie query "..." and receive the answer on stdout, so that I can use evie's notes knowledge headlessly in a pipeline without it appearing in my chat.
  10. As an external script author, I want to run evie prompt "..." and have the answer delivered to my chat, so that I can trigger a proactive message that goes through pi.
  11. As an external script author, I want to schedule any of the above for later with evie schedule, so that my automation can defer work without running its own cron.
  12. As an external script author, I want to schedule evie schedule --in 1h -- run /path/to/script.sh, so that arbitrary local logic runs later with evie's context available.
  13. As a script invoked by Run, I want EVIE_CHAT_ID and EVIE_SOCKET in my environment, so that my evie … callbacks resolve the right chat and daemon with no config.
  14. As a script fired from a Schedule, I want EVIE_SCHEDULE_NAME and EVIE_SCHEDULE_TIME in my environment, so that I know my own identity and scheduled fire time (e.g. to log, compute lateness, or re-schedule myself).
  15. As the notes agent, I want to schedule my own follow-ups by running the evie CLI mid-turn, so that "remind me…" requests become real Schedules I created.
  16. As the notes agent, I want a skill that documents the evie CLI (and evie --help), so that I can discover and use the Verbs without a bespoke tool.
  17. As the notes agent, I want EVIE_CHAT_ID and EVIE_SOCKET injected into my process, so that my evie calls target my own chat and daemon automatically.
  18. As a user, I want a fired Prompt or Query to run in an Ephemeral Session, so that a scheduled digest or a headless query never aborts, blocks, or pollutes the conversation I'm actively having.
  19. As a user, I want a Send to post immediately regardless of whether a turn is in-flight, so that verbatim notifications never wait on or disturb a live conversation.
  20. As a user creating a Schedule, I want to give it a --name, so that I can cancel or update it later by that name.
  21. As a user, I want a --name collision to error by default, so that I don't silently clobber an existing Schedule.
  22. As a user, I want --update to replace (or create) a Schedule by name, so that an agent re-issuing a Schedule is idempotent rather than failing.
  23. As a user who omits --name, I want evie to generate a handle and print it, so that anonymous one-shots can still be listed and cancelled.
  24. As a user, I want Schedule names scoped per chat, so that names in one chat don't collide with another chat's.
  25. As a user, I want --in, --at, and --cron to be mutually exclusive and one required, so that a Schedule's timing is unambiguous.
  26. As a user, I want --cron and --at interpreted in local time, so that scheduling matches how the previous scheduler behaved and how I think about time.
  27. As a user, I want a Schedule to only wrap send, prompt, or run (not query/list/cancel/schedule), so that nonsensical deferrals are rejected at creation.
  28. As a user, I want the target chat frozen into a Schedule at creation, so that it fires against the intended chat even though the creating caller's environment is long gone.
  29. As a user, I want a Verb's target resolved as --chat$EVIE_CHAT_ID → the sole configured Telegram chat → error, so that the common single-user case needs no flag while explicit targeting stays possible.
  30. As a user with a Run Schedule, I want fire-and-forget execution with stdout logged at debug, exit code at info, and errors at warn, so that Run stays an escape hatch that reports failures without muddying delivery.
  31. As an operator, I want the socket at $XDG_RUNTIME_DIR/evie.sock (or /run/evie/evie.sock under systemd) with mode 0600, so that only my user can drive the Daemon.
  32. As an operator, I want the socket path overridable via [rpc] socket_path and a --socket flag, so that I can relocate it when needed.
  33. As an operator, I want Schedules persisted to ~/.config/evie/schedules.json (atomic temp+rename, reloaded on startup), so that they survive restarts using the same idiom as chat sessions.
  34. As an operator upgrading, I want the old [[schedule]] config to simply stop being read (no crash, no migration), so that upgrading is a clean break and I re-create anything I still want via evie schedule --cron.
  35. As a developer, I want the existing HTTP API left unchanged, so that remote chat access over api- sessions keeps working exactly as before.

Implementation Decisions

  • Daemon/CLI split. evie daemon runs today's long-running process. evie <verb> invocations are CLIs that connect to a running Daemon over the Control Plane, issue one Verb, print the result, and exit. Single binary, subcommand selects the role. See ADR 0006.
  • Control Plane transport. Local Unix-domain socket, JSON-lines request/response, mode 0600, filesystem-permission auth, never network-exposed. Default path $XDG_RUNTIME_DIR/evie.sock, falling back to ~/.config/evie/evie.sock; under the NixOS/systemd module set RuntimeDirectory=evie so it lands at /run/evie/evie.sock. Overridable via a new [rpc] socket_path config key and a --socket flag; the CLI resolves --socket$EVIE_SOCKET → default.
  • Dispatch core is transport-agnostic. A single dispatch(verb, resolved_chat) → response core executes every Verb against the existing SessionManager and the new Schedule store. The socket is a thin adapter over it, mirroring how http.rs sits over SessionManager. This is the primary test seam.
  • Verbs. send (verbatim to chat, no pi), prompt (pi → deliver to chat), query (pi → return to caller, deliver nothing), run (spawn a local script), schedule/list/cancel.
  • Ephemeral Sessions. prompt and query run in throwaway pi sessions, isolated from the chat's live conversation Session, so RPC-driven turns never abort/block/pollute an in-flight turn. send uses no session. This deletes any need for queueing or wait-for-idle machinery. The accepted tradeoff is context-blindness.
  • Schedule 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 inner argv, 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 between an immediate and a scheduled run.
  • Schedule identity & upsert. Primary key is (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).
  • Schedule store. ~/.config/evie/schedules.json, atomic temp+rename, reloaded on Daemon startup — same idiom as chat_sessions.json. Each entry: { name, chat, schedule: oneshot(ts) | cron(expr), argv, created_at }.
  • Firing decisions are pure. Extract "is this one-shot due / missed?" and "next occurrence" into pure functions over an injected now, out of the tokio::sleep timing loop. Missed one-shot (fire time elapsed during downtime) fires late with a flag (" due at T (evie was offline)"); recurring fires at its next occurrence. Cron and --at evaluated in chrono::Local.
  • list output. Human table by default; --json emits raw store entries ({name, chat, schedule, argv, created_at, next_fire}) for the agent to parse. Scoped to the resolved chat, --chat overrides.
  • cancel output. evie cancel --name <name>, chat resolved via the standard chain. Prints confirmation; exits nonzero with a not-found message if absent. No --all.
  • Run semantics. The Daemon (not the CLI) spawns the script for both immediate and scheduled Run, so both share the Daemon's context. Env: EVIE_CHAT_ID + EVIE_SOCKET always; EVIE_SCHEDULE_NAME + EVIE_SCHEDULE_TIME when fired from a Schedule. Fire-and-forget: stdout→debug, exit code→info, errors→warn.
  • Targeting chain. Every Verb resolves its target chat as --chat$EVIE_CHAT_ID → the sole configured Telegram chat → error if ambiguous/none. Uses the existing ChatId grammar.
  • Agent integration (no extension). Ship a pi Skill in-repo (wired via [pi].skills) documenting the evie CLI, with evie --help carrying the detail. The agent uses its existing shell tool to run evie …. The Daemon injects EVIE_CHAT_ID + EVIE_SOCKET into the interactive pi process env at spawn. Dependency to verify before building: the spawned pi sessions must actually expose a shell/command-exec tool; if that is gated, fall back to a thin pi extension that shells out to evie.
  • Deletions. Remove [[schedule]], ScheduleConfig, and scheduler_task. No migration; a stale [[schedule]] table is simply ignored (serde unknown field).
  • HTTP API unchanged. Remote chat access over api- sessions stays as-is; the Control Plane is a separate, local surface.

Testing Decisions

Good tests here assert external behavior at a seam, not internals: given a Verb and a resolved chat, what got delivered / returned / stored / fired — never how the socket framed bytes or how the timing loop slept. Follow the existing colocated #[cfg(test)] mod tests style with a fake pi (PiProcess::fake_responder), tempdir for on-disk state, and helpers like insert_slot_for_test.

  • Dispatch core (primary seam). Test dispatch(verb, resolved_chat) directly with fake_responder + a tempdir schedule store. Cover: send/prompt/query/run delivery-vs-return semantics; Ephemeral-Session isolation (a fired prompt/query never touches or aborts a live slot inserted via insert_slot_for_test); targeting resolution and the ambiguity error; Schedule CRUD (create, --name collision → error, --update upsert, per-chat name scoping, list, cancel incl. not-found). Prior art: the session.rs tests (get_status_does_not_block…, cleanup_idle_skips…) that drive SessionManager with fake slots.
  • CLI parsing seam. clap try_parse_from over evie <verb> … argv → request struct: schedule … -- <verb> argv-wrap, timing mutual-exclusion (--in/--at/--cron), the send|prompt|run inner-verb whitelist, --name/--update. Pure, mirrors the config.rs parse/validation tests (test_schedule_config_parses, test_schedule_validation_fails_on_invalid_cron).
  • Firing-decision functions. Unit-test the pure now-injected functions: one-shot due, one-shot missed-during-downtime (fires late + flagged), recurring next-occurrence, local-time interpretation. No tokio::sleep, no wall-clock dependence.
  • Store round-trip. Persist → reload from a tempdir schedules.json and assert entries survive, including the frozen chat and argv. Prior art: session-map persistence patterns.

The Unix socket adapter and the tokio::sleep timing loop stay thin and are left largely untested — behavior lives behind the seams above.

Out of Scope

  • Remote reach for the Control Plane verbs (HTTP exposure of send/prompt/etc.). Local-only Unix socket; if a story ever needs remote, it wraps the same transport-agnostic dispatch core.
  • Broadcasting a Verb to multiple chats. Single target per Verb; a caller loops if needed.
  • A pi extension registering structured tools — replaced by the Skill + CLI approach (revisit only if pi sessions lack a shell tool).
  • Editing/rescheduling a Schedule in place — cancel + schedule again.
  • Deferring query (nobody holds the connection at fire time), and list/cancel --all.
  • Migrating existing [[schedule]] entries into the store.
  • Context-aware fired Prompts (Ephemeral Sessions are deliberately context-blind).
  • Wait-for-idle/queueing against the live conversation session (obviated by Ephemeral Sessions).

Further Notes

  • Domain vocabulary and the daemon/CLI decision are recorded in CONTEXT.md (Control Plane cluster: Daemon, CLI, Control Plane, Verb, Schedule, Ephemeral Session) and docs/adr/0006-daemon-cli-control-plane.md, which also captures the four rejected alternatives (external OS scheduler, HTTP control plane, pi extension, flat schedule flags).
  • Naming: "RPC" is deliberately avoided as the canonical term because it already denotes the RPC Event Stream (the Daemon's protocol to pi); the interface is the Control Plane.
  • The single hard dependency to confirm before implementation: whether evie's spawned pi sessions expose a shell/command-exec tool (drives Skill-vs-extension for the agent path).
## Problem Statement Scheduled prompts today are static `[[schedule]]` TOML entries fired by an in-process `scheduler_task`. As a user I can only create a scheduled prompt by editing config and restarting the bot; I can't ask evie to remind me of something from a conversation, I can't schedule anything from an external script, and every scheduled prompt is forced to target one hardcoded chat with silent delivery. The agent can't schedule its own follow-ups, and nothing outside the process can drive evie at all. ## Solution Split evie into a **Daemon** (`evie daemon` — the long-running process: Telegram, HTTP API, Session manager, and a new Schedule store + timer) and a **CLI** (`evie <verb>` — short-lived invocations that issue one **Verb** to a running Daemon over a local **Control Plane** and exit). The same binary is both roles; the subcommand selects which. The Control Plane is a local Unix-domain socket (JSON-lines, mode 0600), never network-exposed, authed by filesystem permissions. Its Verbs let humans at a shell, external scripts, and — via a skill — the notes agent all drive evie through one interface: - **Send** — deliver verbatim text to a chat (no `pi`). - **Prompt** — run text through `pi` and deliver the response to the chat. - **Query** — run text through `pi` and return the response to the caller, delivering nothing. - **Run** — execute a local script with evie's env context populated, fire-and-forget. - **Schedule / List / Cancel** — manage Schedules. A **Schedule** argv-wraps one delivery Verb and runs it once (`--in`/`--at`) or recurring (`--cron`), persisted so it survives restarts. "Remind me in 1 hour to check X" becomes the agent running `evie schedule --in 1h -- prompt "..."`; "morning digest" becomes `evie schedule --cron "0 8 * * *" -- prompt "..."`, replacing the old static config entirely. ## User Stories 1. As a user, I want to tell evie in conversation "remind me in 1 hour to check the deploy", so that I get a message an hour later without touching config. 2. As a user, I want to say "every morning at 8, give me a digest", so that a recurring Prompt runs on a cron schedule I created at runtime. 3. As a user, I want to cancel a reminder I set earlier, so that I stop receiving a Schedule I no longer want. 4. As a user, I want to ask "what reminders do I have?", so that I can see my pending Schedules. 5. As a user, I want a reminder I set to still fire after the bot restarts, so that scheduling survives deploys and reboots. 6. As a user, I want a one-shot reminder whose time passed while the bot was offline to still reach me (flagged as late), so that I never silently miss a reminder. 7. As a user, I want a recurring Schedule to simply resume at its next occurrence after downtime, so that missed recurring fires don't pile up. 8. As an external script author, I want to run `evie send` to post a verbatim message to my chat, so that my own tooling can notify me through evie without invoking `pi`. 9. As an external script author, I want to run `evie query "..."` and receive the answer on stdout, so that I can use evie's notes knowledge headlessly in a pipeline without it appearing in my chat. 10. As an external script author, I want to run `evie prompt "..."` and have the answer delivered to my chat, so that I can trigger a proactive message that goes through `pi`. 11. As an external script author, I want to schedule any of the above for later with `evie schedule`, so that my automation can defer work without running its own cron. 12. As an external script author, I want to schedule `evie schedule --in 1h -- run /path/to/script.sh`, so that arbitrary local logic runs later with evie's context available. 13. As a script invoked by Run, I want `EVIE_CHAT_ID` and `EVIE_SOCKET` in my environment, so that my `evie …` callbacks resolve the right chat and daemon with no config. 14. As a script fired from a Schedule, I want `EVIE_SCHEDULE_NAME` and `EVIE_SCHEDULE_TIME` in my environment, so that I know my own identity and scheduled fire time (e.g. to log, compute lateness, or re-schedule myself). 15. As the notes agent, I want to schedule my own follow-ups by running the `evie` CLI mid-turn, so that "remind me…" requests become real Schedules I created. 16. As the notes agent, I want a skill that documents the `evie` CLI (and `evie --help`), so that I can discover and use the Verbs without a bespoke tool. 17. As the notes agent, I want `EVIE_CHAT_ID` and `EVIE_SOCKET` injected into my process, so that my `evie` calls target my own chat and daemon automatically. 18. As a user, I want a fired Prompt or Query to run in an Ephemeral Session, so that a scheduled digest or a headless query never aborts, blocks, or pollutes the conversation I'm actively having. 19. As a user, I want a Send to post immediately regardless of whether a turn is in-flight, so that verbatim notifications never wait on or disturb a live conversation. 20. As a user creating a Schedule, I want to give it a `--name`, so that I can cancel or update it later by that name. 21. As a user, I want a `--name` collision to error by default, so that I don't silently clobber an existing Schedule. 22. As a user, I want `--update` to replace (or create) a Schedule by name, so that an agent re-issuing a Schedule is idempotent rather than failing. 23. As a user who omits `--name`, I want evie to generate a handle and print it, so that anonymous one-shots can still be listed and cancelled. 24. As a user, I want Schedule names scoped per chat, so that names in one chat don't collide with another chat's. 25. As a user, I want `--in`, `--at`, and `--cron` to be mutually exclusive and one required, so that a Schedule's timing is unambiguous. 26. As a user, I want `--cron` and `--at` interpreted in local time, so that scheduling matches how the previous scheduler behaved and how I think about time. 27. As a user, I want a Schedule to only wrap `send`, `prompt`, or `run` (not `query`/`list`/`cancel`/`schedule`), so that nonsensical deferrals are rejected at creation. 28. As a user, I want the target chat frozen into a Schedule at creation, so that it fires against the intended chat even though the creating caller's environment is long gone. 29. As a user, I want a Verb's target resolved as `--chat` → `$EVIE_CHAT_ID` → the sole configured Telegram chat → error, so that the common single-user case needs no flag while explicit targeting stays possible. 30. As a user with a Run Schedule, I want fire-and-forget execution with stdout logged at debug, exit code at info, and errors at warn, so that Run stays an escape hatch that reports failures without muddying delivery. 31. As an operator, I want the socket at `$XDG_RUNTIME_DIR/evie.sock` (or `/run/evie/evie.sock` under systemd) with mode 0600, so that only my user can drive the Daemon. 32. As an operator, I want the socket path overridable via `[rpc] socket_path` and a `--socket` flag, so that I can relocate it when needed. 33. As an operator, I want Schedules persisted to `~/.config/evie/schedules.json` (atomic temp+rename, reloaded on startup), so that they survive restarts using the same idiom as chat sessions. 34. As an operator upgrading, I want the old `[[schedule]]` config to simply stop being read (no crash, no migration), so that upgrading is a clean break and I re-create anything I still want via `evie schedule --cron`. 35. As a developer, I want the existing HTTP API left unchanged, so that remote chat access over `api-` sessions keeps working exactly as before. ## Implementation Decisions - **Daemon/CLI split.** `evie daemon` runs today's long-running process. `evie <verb>` invocations are CLIs that connect to a running Daemon over the Control Plane, issue one Verb, print the result, and exit. Single binary, subcommand selects the role. See ADR 0006. - **Control Plane transport.** Local Unix-domain socket, JSON-lines request/response, mode 0600, filesystem-permission auth, never network-exposed. Default path `$XDG_RUNTIME_DIR/evie.sock`, falling back to `~/.config/evie/evie.sock`; under the NixOS/systemd module set `RuntimeDirectory=evie` so it lands at `/run/evie/evie.sock`. Overridable via a new `[rpc] socket_path` config key and a `--socket` flag; the CLI resolves `--socket` → `$EVIE_SOCKET` → default. - **Dispatch core is transport-agnostic.** A single `dispatch(verb, resolved_chat) → response` core executes every Verb against the existing `SessionManager` and the new Schedule store. The socket is a thin adapter over it, mirroring how `http.rs` sits over `SessionManager`. This is the primary test seam. - **Verbs.** `send` (verbatim to chat, no `pi`), `prompt` (`pi` → deliver to chat), `query` (`pi` → return to caller, deliver nothing), `run` (spawn a local script), `schedule`/`list`/`cancel`. - **Ephemeral Sessions.** `prompt` and `query` run in throwaway `pi` sessions, isolated from the chat's live conversation Session, so RPC-driven turns never abort/block/pollute an in-flight turn. `send` uses no session. This deletes any need for queueing or wait-for-idle machinery. The accepted tradeoff is context-blindness. - **Schedule 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 inner argv, 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 between an immediate and a scheduled run. - **Schedule identity & upsert.** Primary key is `(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). - **Schedule store.** `~/.config/evie/schedules.json`, atomic temp+rename, reloaded on Daemon startup — same idiom as `chat_sessions.json`. Each entry: `{ name, chat, schedule: oneshot(ts) | cron(expr), argv, created_at }`. - **Firing decisions are pure.** Extract "is this one-shot due / missed?" and "next occurrence" into pure functions over an injected `now`, out of the `tokio::sleep` timing loop. Missed one-shot (fire time elapsed during downtime) fires late with a flag ("⏰ due at T (evie was offline)"); recurring fires at its next occurrence. Cron and `--at` evaluated in `chrono::Local`. - **`list` output.** Human table by default; `--json` emits raw store entries (`{name, chat, schedule, argv, created_at, next_fire}`) for the agent to parse. Scoped to the resolved chat, `--chat` overrides. - **`cancel` output.** `evie cancel --name <name>`, chat resolved via the standard chain. Prints confirmation; exits nonzero with a not-found message if absent. No `--all`. - **Run semantics.** The Daemon (not the CLI) spawns the script for both immediate and scheduled Run, so both share the Daemon's context. Env: `EVIE_CHAT_ID` + `EVIE_SOCKET` always; `EVIE_SCHEDULE_NAME` + `EVIE_SCHEDULE_TIME` when fired from a Schedule. Fire-and-forget: stdout→debug, exit code→info, errors→warn. - **Targeting chain.** Every Verb resolves its target chat as `--chat` → `$EVIE_CHAT_ID` → the sole configured Telegram chat → error if ambiguous/none. Uses the existing `ChatId` grammar. - **Agent integration (no extension).** Ship a `pi` **Skill** in-repo (wired via `[pi].skills`) documenting the `evie` CLI, with `evie --help` carrying the detail. The agent uses its existing shell tool to run `evie …`. The Daemon injects `EVIE_CHAT_ID` + `EVIE_SOCKET` into the interactive `pi` process env at spawn. **Dependency to verify before building:** the spawned `pi` sessions must actually expose a shell/command-exec tool; if that is gated, fall back to a thin `pi` extension that shells out to `evie`. - **Deletions.** Remove `[[schedule]]`, `ScheduleConfig`, and `scheduler_task`. No migration; a stale `[[schedule]]` table is simply ignored (serde unknown field). - **HTTP API unchanged.** Remote chat access over `api-` sessions stays as-is; the Control Plane is a separate, local surface. ## Testing Decisions Good tests here assert **external behavior at a seam**, not internals: given a Verb and a resolved chat, what got delivered / returned / stored / fired — never how the socket framed bytes or how the timing loop slept. Follow the existing colocated `#[cfg(test)] mod tests` style with a fake `pi` (`PiProcess::fake_responder`), `tempdir` for on-disk state, and helpers like `insert_slot_for_test`. - **Dispatch core (primary seam).** Test `dispatch(verb, resolved_chat)` directly with `fake_responder` + a `tempdir` schedule store. Cover: `send`/`prompt`/`query`/`run` delivery-vs-return semantics; Ephemeral-Session isolation (a fired `prompt`/`query` never touches or aborts a live slot inserted via `insert_slot_for_test`); targeting resolution and the ambiguity error; Schedule CRUD (create, `--name` collision → error, `--update` upsert, per-chat name scoping, `list`, `cancel` incl. not-found). Prior art: the `session.rs` tests (`get_status_does_not_block…`, `cleanup_idle_skips…`) that drive `SessionManager` with fake slots. - **CLI parsing seam.** `clap` `try_parse_from` over `evie <verb> …` argv → request struct: `schedule … -- <verb>` argv-wrap, timing mutual-exclusion (`--in`/`--at`/`--cron`), the `send|prompt|run` inner-verb whitelist, `--name`/`--update`. Pure, mirrors the `config.rs` parse/validation tests (`test_schedule_config_parses`, `test_schedule_validation_fails_on_invalid_cron`). - **Firing-decision functions.** Unit-test the pure `now`-injected functions: one-shot due, one-shot missed-during-downtime (fires late + flagged), recurring next-occurrence, local-time interpretation. No `tokio::sleep`, no wall-clock dependence. - **Store round-trip.** Persist → reload from a `tempdir` `schedules.json` and assert entries survive, including the frozen chat and argv. Prior art: session-map persistence patterns. The Unix socket adapter and the `tokio::sleep` timing loop stay thin and are left largely untested — behavior lives behind the seams above. ## Out of Scope - Remote reach for the Control Plane verbs (HTTP exposure of `send`/`prompt`/etc.). Local-only Unix socket; if a story ever needs remote, it wraps the same transport-agnostic dispatch core. - Broadcasting a Verb to multiple chats. Single target per Verb; a caller loops if needed. - A `pi` extension registering structured tools — replaced by the Skill + CLI approach (revisit only if `pi` sessions lack a shell tool). - Editing/rescheduling a Schedule in place — `cancel` + `schedule` again. - Deferring `query` (nobody holds the connection at fire time), and `list`/`cancel --all`. - Migrating existing `[[schedule]]` entries into the store. - Context-aware fired Prompts (Ephemeral Sessions are deliberately context-blind). - Wait-for-idle/queueing against the live conversation session (obviated by Ephemeral Sessions). ## Further Notes - Domain vocabulary and the daemon/CLI decision are recorded in `CONTEXT.md` (Control Plane cluster: Daemon, CLI, Control Plane, Verb, Schedule, Ephemeral Session) and `docs/adr/0006-daemon-cli-control-plane.md`, which also captures the four rejected alternatives (external OS scheduler, HTTP control plane, `pi` extension, flat schedule flags). - Naming: "RPC" is deliberately avoided as the canonical term because it already denotes the RPC Event Stream (the Daemon's protocol *to* `pi`); the interface is the **Control Plane**. - The single hard dependency to confirm before implementation: whether evie's spawned `pi` sessions expose a shell/command-exec tool (drives Skill-vs-extension for the agent path).
Author
Owner

Open dependency resolved: pi exposes a shell/command-exec tool, so the agent path is confirmed as skill-drives-CLI (agent runs evie … directly). No pi extension fallback needed. ADR 0006 updated accordingly.

Open dependency resolved: `pi` exposes a shell/command-exec tool, so the agent path is confirmed as skill-drives-CLI (agent runs `evie …` directly). No `pi` extension fallback needed. ADR 0006 updated accordingly.
weiwen referenced this issue from a commit 2026-07-12 01:02:00 +08:00
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#55
No description provided.