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 |
Preview summaries |
Short excerpts taken from each message body when |
Dry-run |
|
Cron mode |
|
Partial latest |
|
Do not confuse preview summaries with dry-run.
Recommended setup¶
Bring up with Docker Compose (DOCKER.md); put sticky paths in
~/.config/rollup/config.tomlvia Settings or by hand (CONFIG.md).Run
docker compose exec rollup rollup doctor …(or hostrollup doctor) and fix any errors.Run a manual digest once (Run Studio or CLI), then inspect
output/andstate/manifests/.Schedule a weekly job —
docker compose execfrom launchd/crontab, or a host venv withrollup cron print-launchd/print-crontab(advanced host install).
Advanced: host Python scheduling¶
When running without Compose, install Rollup in a project venv with a stable absolute Python path, then use rollup cron print-launchd (macOS) or print-crontab.
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 ( |
0 |
Dated empty digest + required writers may write; |
Mbox mutation during parse |
2 |
Mutated folders excluded from published content; |
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 |
2 |
Dated digest remains usable; inspect the final-review sidecar or manifest block |
|
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-only run ( |
1 |
Nothing published |
Reddit fetch fails; mail still publishes |
2 |
Dated mail digest usable; Reddit section omitted; |
Reddit-only run ( |
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.mdoutput/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:
Write dated Markdown + HTML outputs.
If requested and allowed by status, atomically publish
latest.mdandlatest.htmltogether.Update seen-state for rendered undated items. This still runs when
latest.*publication fails, because the dated digest exists.Write the run manifest.
dated_outputs_writtenrecords whether the dated Markdown + HTML outputs were written;latest_outputs_updatedrecords 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.