02-sops

system cron runner

2026-06-11·sop·status: active

System cron runner — lean headless scheduling for the Ray cron suite

Why this exists

Measured 2026-06-11 (1.7 days of Fable 5 transcripts since the 6/9 upgrade): the cron suite firing inside the always-on channels session cost ~$438 at credit prices — ~$7.8k/mo extrapolated — and 70% of it was cache reads: ~30 wakes/day each re-reading the session's ~300K-token context (5-min cache TTL means every fire is cold). Founder-approved restructure (iMessage, 2026-06-11 ~08:09 ET): (1) stop waking the big session for no-ops, (2) run cron jobs in lean headless sessions on subscription-tier models. Phase 3 (Fable dispatch gate + spend log) deferred until closer to the June 22 Fable subscription cutover.

Headless feasibility was verified empirically before migration: all 7 required MCP surfaces work in claude -p — claude.ai connectors (Gmail, Notion, Google Calendar) AND local plugins (iMessage, Discord, Monarch, qmd). Lean session base context is ~40K tokens vs ~300K in the channels session.

Architecture

LaunchAgent co.raydata.cron-dispatcher (every minute)
  └─ cron-dispatcher.py        reads manifest, matches due system jobs,
       │                       runs prechecks, dedups per-minute
       └─ cron-job-runner.sh   per-job lock, headless `claude -p /skill
            │                  --model <tier>`, wall-clock timeout,
            │                  logging, failure recording
            └─ skill runs exactly as before; reports to Discord #ops /
               vault / state files (output surfaces unchanged)

Observability / feedback (founder requirement: keep access to results)

Operations

Where does a NEW scheduled task live? (decision rubric, added per founder ask 2026-06-11)

Walk top-down; first match wins:

  1. No model needed at all (pure script: health pings, file moves, backups) → user crontab or its own LaunchAgent, no claude run. Precedent: check-public-ip-drift.sh. Never burn a model call on deterministic work.
  2. Needs the live channels session — founder-facing synthesis that depends on in-session context (open threads, conversation state), OR reads iMessage via the plugin (chat.db FDA — blocked in the dispatcher tree until the founder grants FDA to /opt/homebrew/bin/python3) → runner=session. Current members: /morning-prep, /open-threads-check.
  3. Everything elserunner=system (the default). Lean headless, Max-sub models, full MCP access (claude.ai connectors + Discord + Monarch + qmd all verified working).
  4. Future tier: cloud-shaped jobs with no local-filesystem deps (EDGAR, smart-money) → Anthropic scheduled deployments, pending the pilot decision.

Model tier: haiku = mechanical AND no channel-reporting duty · sonnet = floor for anything that posts to Discord/iMessage or does moderate synthesis · opus = judgment/writing quality. When unsure: sonnet.

Build-out checklist for a new system job

  1. Prove the skill works in-session first (or on-demand) before scheduling it.
  2. Audit dependencies: MCP servers (iMessage = session-only until FDA; all others fine), CLIs (must be reachable from the runner PATH — currently ~/.bun/bin, ~/.local/bin, /opt/homebrew/bin, system dirs), state files (~/.claude/state/** writes must use Bash, not the Write tool).
  3. If the job frequently no-ops, write a bash-only precheck in ~/.claude/scripts/prechecks/ (exit 0 = work exists). Fail-open.
  4. Add the manifest line: cron | system | <model> | <precheck|-> | /command. Prefer off-:00 minutes for new jobs.
  5. If the run can exceed 30 min, add its slug to TIMEOUTS in cron-dispatcher.py. (Slug = command lowercased, non-alphanumerics → hyphens: /process-youtube watchprocess-youtube-watch.)
  6. Smoke-test through the REAL launchd path: echo "<slug>" > ~/.claude/state/cron-force.txt, then check ~/.claude/logs/cron-runs/<slug>-*.log AND verify the output surface actually received the report — exit 0 is not proof (haiku once claimed a Discord post it never made; that's why the sonnet floor exists).
  7. New Python tooling: use uv (uv run, uv venv) per founder preference 2026-06-11 — Bash(uv *) is already wired into the package-security-check hook. Deliberate exception: cron-dispatcher.py stays stdlib-only on /opt/homebrew/bin/python3 — it fires every minute (zero resolution overhead wanted) and that binary is the stable FDA-grant target for the chat.db unlock.

Known limitations / v2 candidates

Related