singularity-forge/docs/adr/0002-sf-schedule-pull-based.md
2026-05-07 03:25:20 +02:00

4.5 KiB

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 as versioned append-only JSONL in .sf/schedule.jsonl (project) or ~/.sf/schedule.jsonl (global). 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. Launchloader.ts calls findDue() and prints a banner if items are overdue
    2. Auto-mode boundariessf headless query populates a schedule field with due and upcoming entries
    3. CLIsf 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 JSONL file is a complete, append-only audit trail of all 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 precisionkind: 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 — versioned append-only JSONL store with findDue() and findUpcoming() queries
  • 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.