Personal cron / unattended setup

Rollup is designed for a calm weekly reading habit. On macOS, prefer launchd. Crontab remains a portable alternative.

Terminology

Term

Meaning

Weekly non-AI digest

Normal rollup digest / rollup digest --cron using preview excerpts (no Ollama)

Preview summaries

Short excerpts taken from each message body when --ollama is off

Dry-run

--dry-run — parse and report only; no output files, state, logs, or network

Cron mode

--cron — quieter logs, publish latest.*, mode=cron in the run manifest

Partial latest

--allow-partial-latest — permit latest.* updates for partial runs; default is success-only

Do not confuse preview summaries with dry-run.

Single-run lock

Only one digest may run at a time. The lock file lives at state_dir/rollup.lock (never under the mail root). A second invocation exits with code 1 and:

ERROR: Another digest run is in progress (run_id=...)

Stale locks (dead PID or older than 6 hours) are recovered automatically.

Exit codes

Code

Meaning

0

Success (or dry-run success), including empty-window success and web-index-only degradation

1

Hard failure (safety, lock, missing root, invalid config, no-input, required publication failure, no usable digest)

2

Partial success — required dated digest usable, but material issues occurred

Degradation details (integrity matrix; see also CONTRACT.md and rollup.run_contracts):

Condition

Exit

Durable-write behavior

No-input (include miss / no readable folders / all parse candidates failed)

1

Nothing published; gate runs before Ollama, review, render, writers, state, publication

Empty date window (messages_included == 0, healthy discovery/parse)

0

Dated empty digest + required writers may write; latest.* refused

Mbox mutation during parse

2

Mutated folders excluded from published content; latest.* refused; escalate to 1 if exclusion yields no-input

Required dated artifact or required writer failure

1

Irreversible boundary not crossed (or partial rename recovery on next run → 2 if half-published)

Optional writer failure

2

Required pubs already succeeded

Final-review sidecar write fails

2

Dated digest remains usable; sidecar is outside the required dated set

Final-review overall status is fail

2

Dated digest remains usable; inspect the final-review sidecar or manifest block

latest.* publication fails

2

Dated digest remains source of truth; seen-state runs only after required pubs succeeded

Manifest write fails after a usable digest

2

Original failure diagnosis preserved; minimal fallback diagnostic on stderr / run events

Seen-state update fails after required pubs

2

Dated digest may exist; undated items may repeat on future runs

Web-index failure

0 if otherwise success

Typed secondary degradation; manifest event + diagnostic; does not alone force partial

Group summaries degrade

2

Member summaries still render; cache/read/write or stream errors are recorded

LinkedIn fetch fails; mail still publishes

2

Dated mail digest usable; LinkedIn section omitted; linkedin_fetch_failed (or similar) in warnings

LinkedIn-only run (--folder linkedin:…, no mbox) and fetch fails

1

Nothing published

Reddit fetch fails; mail still publishes

2

Dated mail digest usable; Reddit section omitted; reddit_fetch_failed (or similar) in warnings

Reddit-only run (--folder reddit:…, no mbox) and fetch fails

1

Nothing published

High parse/summary error rates

2

Dated digest remains usable but incomplete or lower quality

Irreversible boundary: latest.*, seen-state, web index, and success status wait until every required dated artifact and required output writer has final paths. Manifest / web-index failures must not mark unpublished messages seen.

A global apply skip (e.g. missing fingerprint echo) alone does not force partial when the digest is otherwise successful—check the manifest final_review block.

Invalid Phase-3 flags (e.g. --group-summaries without --ollama, non-primary variant policy, cron apply without --final-review-allow-cron-apply) fail before the run with exit 1.

Unattended apply uses conservative whole-set caps (final_review_max_patches_unattended / final_review_max_changed_chars_unattended): exceeding either skips all patches.

Dry-run must not create databases, migrate schema, WAL/SHM files, locks, profile exports, staged publication temps, or writer artifacts.

Environment variables (network sources)

CLI startup loads ~/.config/rollup/env (or ROLLUP_ENV_FILE) and does not override variables already in the process environment. A launchd/cron job that runs as your user therefore picks up LinkedIn cookies (and optional Reddit OAuth) from that file — you do not have to duplicate them in the plist.

If [linkedin].enabled is set and the env file is missing, put cookies in the job environment (never TOML):

export ROLLUP_LINKEDIN_LI_AT='…'
export ROLLUP_LINKEDIN_JSESSIONID='ajax:…'

For launchd, EnvironmentVariables in the plist or a wrapper script still work. Refresh both cookies when LinkedIn 401s. How to copy cookies: EXAMPLES.md. Article fetch (default on) adds extra HTTP to linked article hosts; disable with [linkedin].article_fetch = false if the scheduled job should stay Voyager-only.

Optional Reddit script-app OAuth uses ROLLUP_REDDIT_CLIENT_ID, ROLLUP_REDDIT_CLIENT_SECRET, ROLLUP_REDDIT_USERNAME, and ROLLUP_REDDIT_PASSWORD in the same env file.

See CONFIG.md.

Listing cache vs weekly jobs

Reddit and LinkedIn listings reuse SQLite snapshots for fetch_ttl_hours (default 24). A weekly job usually expires that window and fetches live. A daily job within 24 hours will skip the network unless you pass --reddit-refresh / --linkedin-refresh or set TTL to 0.

launchd (preferred on macOS)

Generate a LaunchAgent plist with explicit paths (or rely on config.toml and omit --root / --mail-root):

rollup cron print-launchd \
  --python /Users/you/rollup/.venv/bin/python \
  --workdir /Users/you/rollup \
  --output-dir /Users/you/Documents/rollup-outputs \
  --state-dir /Users/you/rollup/state \
  --log-dir /Users/you/rollup/logs \
  --weekday 0 --hour 8 --minute 0 \
  > ~/Library/LaunchAgents/com.rollup.digest.plist

launchctl load ~/Library/LaunchAgents/com.rollup.digest.plist

If paths are not in TOML yet, pass --root / --mail-root pointing at your Thunderbird Newsletters.sbd and its parent mail account directory.

The plist sets WorkingDirectory, StandardOutPath, and StandardErrorPath.

crontab (alternative)

rollup cron print-crontab \
  --python /Users/you/rollup/.venv/bin/python \
  --workdir /Users/you/rollup \
  --output-dir /Users/you/Documents/rollup-outputs \
  --state-dir /Users/you/rollup/state \
  --log-dir /Users/you/rollup/logs

Example weekly non-AI digest (Sundays 08:00), assuming ~/.config/rollup/config.toml sets root / mail_root:

0 8 * * 0 cd /Users/you/rollup && /Users/you/rollup/.venv/bin/python -m rollup digest --cron \
  --output-dir /Users/you/Documents/rollup-outputs \
  --state-dir /Users/you/rollup/state \
  --log-dir /Users/you/rollup/logs >> /Users/you/rollup/logs/cron.log 2>&1

Check last run

rollup cron status --state-dir /Users/you/rollup/state

Latest outputs

On successful --cron (or --latest) runs, Rollup atomically updates:

  • output/latest.md

  • output/latest.html

Partial/failed runs do not replace last-known-good latest digests by default. Pass --allow-partial-latest only if you want partial but usable runs to update the latest.* aliases. latest.md and latest.html are published as one file set, so they do not point at different runs.

Durable write ordering

For non-dry-run digests, durable writes are ordered so the dated digest is the source of truth:

  1. Write dated Markdown + HTML outputs.

  2. If requested and allowed by status, atomically publish latest.md and latest.html together.

  3. Update seen-state for rendered undated items. This still runs when latest.* publication fails, because the dated digest exists.

  4. Write the run manifest. dated_outputs_written records whether the dated Markdown + HTML outputs were written; latest_outputs_updated records whether the latest aliases moved.

Readers still accept the legacy manifest key outputs_published as an alias for dated_outputs_written, but new manifests write dated_outputs_written.