singularity-forge/docs/adr/0002-sf-schedule-pull-based.md

82 lines
4.5 KiB
Markdown

# ADR-0002: SF Schedule System is Pull-Based, Not Daemon-Based
**Date:** 2026-05-05
**Status:** Accepted
**Deciders:** SF core team (M010)
**Related:** M010 S01 (schedule store), M010 S02 (schedule CLI), M010 S03 (milestone YAML integration), M010 S05 (this slice)
---
## Context
The SF schedule system requires time-bound reminders that surface at a future date. Several design options were considered:
1. **Daemon-based (cron/launchd)** — A background process fires items at their due time using the OS scheduler.
2. **Daemon-based (in-process timer)** — SF itself runs as a long-lived process with in-process timers.
3. **Pull-based (on-demand query)** — Items are stored durably and queried at integration points (launch, auto-mode boundaries, explicit CLI query).
Option 1 was explicitly ruled out early: platform-specific (cron on Unix, launchd on macOS, Task Scheduler on Windows), requires daemon installation, and cannot fire items when SF is not running.
Option 2 was ruled out because SF is designed to be a session-based tool — agents run in fresh contexts per unit, state does not accumulate across sessions, and there is no persistent long-lived process in the happy path.
Option 3 (pull-based) is what we adopted.
---
## Decision
The SF schedule system is **pull-based**:
- Schedule entries are stored in SQLite (`schedule_entries`). Legacy `.sf/schedule.jsonl` rows are import-only compatibility input, and rows without `schemaVersion` are treated as legacy version 1 by the current reader.
- There is no background daemon or timer process.
- Entries are queried ("pulled") at defined integration points:
1. **Launch**`loader.ts` calls `findDue()` and prints a banner if items are overdue
2. **Auto-mode boundaries**`sf headless query` populates a machine snapshot `schedule` field with `due` and `upcoming` entries
3. **CLI**`sf schedule list --due` for explicit human query
4. **TUI status overlay** — displays due/upcoming schedule entries in the dashboard
---
## Consequences
### Positive
- **Portable** — works identically on Linux, macOS, and Windows without platform-specific code
- **Simple** — no process management, no signal handlers, no daemon lifecycle
- **Auditable** — the DB ledger preserves append-style schedule operations
- **Resilient** — no fire-and-forget timer that might miss if the process is restarted
- **Stateless** — fits SF's session model: fresh context per unit, no in-memory state
### Negative / Explicitly Deferred
- **No fire-at-exact-time** — items are not delivered at their exact `due_at`; they surface at the next pull query. If an item is due at 3 AM and the user opens SF at 9 AM, the item appears as overdue.
- **No background notification** — SF cannot send a system notification when an item becomes due unless SF is open and the user is interacting with it.
- **No recurring fire precision** — `kind: recurring` entries are stored but the recurring fire mechanism is deferred to a future iteration.
These limitations are accepted trade-offs for the portability and simplicity benefits. A future iteration could add an optional lightweight notification helper (e.g. a separate binary that reads the schedule and posts system notifications) without changing the core design.
---
## Implementation Notes
- `schedule-store.js` — DB-primary store with `findDue()` and `findUpcoming()` queries plus legacy JSONL import
- `loader.ts` — calls `findDue()` on both scopes at startup; prints banner if any items are due
- `headless-query.ts` — populates `schedule: { due, upcoming }` in `QuerySnapshot`
- `sf schedule` CLI — add, list, done, cancel, snooze, run subcommands
- `sf_plan_milestone` YAML — supports `schedule[]` array with `in` and `on_complete` duration fields
---
## Alternatives Considered
### In-Process Timer (Rejected)
A long-lived SF process could maintain a timer queue and fire items at their due time. Rejected because it conflicts with SF's session architecture — each unit runs in isolation with no shared timer state across dispatch cycles.
### External Cron Wrapper (Rejected)
A `sf-schedule-daemon` sidecar process managed by the user. Rejected because it adds an installation and运维 burden that conflicts with the "install and use immediately" experience goal.
### Third-Party Scheduling Service (Rejected)
Using a hosted service (e.g. cron-job.org, AWS EventBridge) to fire webhook calls. Rejected because it introduces an external dependency and network requirement that does not fit SF's self-contained model.