MeridiansMeridians

Scheduler — cadence to fired run [Flow]

From the Meridians Wiki · Public · Maintained · joint

The headless Program scheduler is the time-based orchestration layer: it reads enabled schedules from the record, arms Croner timers, and on each tick runs a Research/Opinion/Merge/Position operation to completion — under concurrency, serialization, and spend gates. The pure when-to-fire logic is separate from the imperative shell that does the waking.

flowchart TB
    Rec[("record — enabled schedules<br/>(days · hours · everyNWeeks)")]
    Meta[("META — **baseline operations timezone**<br/>(aux/meta.json · one instance-wide IANA zone)")]
    Rec -->|"reconcile() · per-document fingerprint + compact projection<br/>read only the source that changed"| Arm
    Meta -->|"all cadences interpreted in it"| Arm

    subgraph Arm["arm — pure WHEN → live timers"]
        direction TB
        Cron["scheduleToCron(schedule)<br/>(future windows only — no boot replay)"]
        Timer["new Cron(pattern, tz, onFire)<br/>diff vs live timers: add new · drop stale"]
        Cron --> Timer
    end

    Timer -->|"tick"| Fire

    subgraph Fire["onFire — the gates"]
        direction TB
        Week{"weekGatePasses?<br/>(fortnightly/monthly — cron can't express)"}
        Spend{"spend cap?<br/>(provider-spending jobs only)"}
        Serial{"domain already running?<br/>(per-domain serialization)"}
        Conc{"concurrency slot?<br/>(global cap, default 3)"}
        Week -->|"no"| Skip1(["skip until due"])
        Week -->|"yes"| Spend -->|"blocked"| Skip2(["skip — cap reached"])
        Spend -->|"ok"| Serial -->|"busy"| Queue(["queue — one run per domain"])
        Serial -->|"free"| Conc -->|"full"| Wait(["wait for a slot"])
        Conc -->|"slot"| Run
    end

    Run["runOperationToEnd — the Research/Opinion/Merge/Position op<br/>(coalesced cycleIds if cadence matches)"]
    Run -->|"same op machinery"| Ops(["operation-lifecycle → commits + echo"])

Invariants

  • Croner wakes; the record decides. The scheduler holds no schedule authority of its own — it derives timers from enabled schedules. The 30-second reconcile() poll checks cheap per-document sidecar/body-stat fingerprints and retains only each source's compact job projection, so one canonical write re-reads one Domain/Constellation rather than the whole record; incomplete reads are never cached. Timers fire in future windows only; there is no boot replay of missed runs.
  • One baseline timezone, all cadences relative. A cadence stores only its wall-clock day/hour; the ONE instance-wide baseline operations timezone (persisted in the record's META, Director-set in Settings) interprets every cadence at once. Croner fires the local hour in the baseline zone → the exact UTC instant; changing the baseline re-relativizes every schedule with no per-domain rewrite. Hydration seeds it from META, else the operator's browser zone. Cadence editors and the agenda render local-primary + UTC-secondary.
  • The everyNWeeks gate is checked at fire time. Cron can't express fortnightly/monthly, so weekGatePasses(schedule, lastRunAt, now) gates it in the shell. Research cycles sharing a schedule coalesce into one Watch pass (days/hours sorted → order-independent).
  • Three discipline gates, all independent. Per-domain serialization (at most one mutating run per domain), a global concurrency cap (default 3), and a per-instance weekly spend ceiling that gates only provider-spending jobs (syncs are exempt). Failures are isolated — one domain's error never stalls another.
  • Only always-on hosts arm it. isAlwaysOnServer() gates the scheduler, reconcile loop, and journal drain (platform-model); serverless hosts run no timers.

Where it lives: src/lib/server/program/scheduler.ts (imperative shell — timers + gates), src/lib/core/program/schedule-cron.ts (pure scheduleToCron · weekGatePasses · nextFireAt, all baseline-zone-aware), src/lib/core/program/operations-timezone.ts (the baseline resolver: get/set/subscribe, validated), src/lib/server/record/instance-usage.ts (spend estimate). The run itself: operation-lifecycle · maintenance-run. Timing rules owned by src/lib/core/program/schedule-cron.ts.