# Cron Workflows for Email Archive ## Architecture overview Two independent cron pipelines that share the same email storage: ``` mail-archive (every 5 min, no_agent=True) ↓ INBOX/UID/email.md ←── contacts-extractor (every 30 min, script-based, LLM) ↓ contacts.vcf + contacts.json ``` Both use **script-based cron** (the `script=` parameter) with the script in `~/.hermes/scripts/`. The archive job is `no_agent=True` (pure shell — no LLM tokens consumed). The contacts job uses the agent loop because it calls LLM internally via Python. ## Mail archive cron (bulk sync) ```bash # Script: ~/.hermes/scripts/mail-archive.sh #!/usr/bin/env bash set -euo pipefail cd /opt/hermes/email-assistant exec python3 scripts/mail_archive.py --all --limit 10 ``` Cron creation: ```bash hermes cron create \ --name "mail-archive-every-5min" \ --schedule "every 5m" \ --script mail-archive.sh \ --no-agent \ --deliver local ``` Key details: - `no_agent=True` → pure script mode, zero LLM cost per tick - `deliver=local` → output saved, no notification (noisy at 5min intervals) - Script path MUST be relative in `~/.hermes/scripts/` — absolute paths rejected - `--all --limit 10` processes all 22 folders with 10 emails per folder per tick ### Shell wrapper with per-folder timeouts When using `--all`, one slow IMAP folder can stall the entire run. The shell-embedded approach handles this natively — each folder gets its own `timeout`: ```bash FOLDERS=(INBOX "Отправленные" Archive Sent ...) TIMEOUT=60 LIMIT=10 for folder in "${FOLDERS[@]}"; do timeout $TIMEOUT python3 scripts/mail_archive.py --folder "$folder" --limit $LIMIT 2>&1 || true done ``` This is the ACTUAL approach used in `mail-archive.sh`. The `--all` flag in `mail_archive.py` iterates folders internally but without per-folder timeouts, so the shell wrapper is the recommended pattern when you control the cron script. ## Contacts extractor cron (LLM-based) ```bash # Script: ~/.hermes/scripts/contacts-cron.sh #!/usr/bin/env bash set -euo pipefail cd /opt/hermes/email-assistant exec python3 scripts/contacts_extractor.py --limit 15 ``` Cron creation: ```bash hermes cron create \ --name "contacts-extractor-every-30m" \ --schedule "every 30m" \ --script contacts-cron.sh \ --deliver local ``` Key differences from archive cron: - **NO `--no-agent`** — the contacts extractor calls LLM (Qwen3:8b via Ollama) internally. Without the agent loop, the script runs but output isn't delivered/visible. - **`--limit 15`** — Qwen3:8b takes ~20s per email. 15 emails × 20s = ~5 min, well within the 30-min window. Bump to 25-30 if Qwen is on a GPU. - **`deliver=local`** — results saved to disk, no notification. The agent generates a summary message on each tick. ## Transition from bulk to incremental When state files stop advancing (all emails archived): 1. Update archive cron: smaller limit or longer interval ```bash hermes cron update --schedule "every 30m" ``` 2. Keep contacts cron at `every 30m` — it always processes only new emails (UID tracking in `last_scan.json`) ## Testing cron scripts Before scheduling, verify the script works by running it once: ```bash timeout 120 bash ~/.hermes/scripts/contacts-cron.sh ``` Check for: - Exit code 0 = success; 124 = timeout (reduce `--limit`) - Stale output = script isn't finding new files (check `last_scan.json` UIDs) - Python import errors = missing dependencies (run `pip install -r requirements.txt`) ## Pitfalls 1. **Script path MUST be relative.** `--script /absolute/path` is silently rejected. Copy the script to `~/.hermes/scripts/` and pass just the filename. 2. **Contacts cron needs the agent loop.** Unlike the pure-shell archive cron, contacts cron must NOT have `--no-agent`. Without the agent, the LLM calls in `contacts_extractor.py` still execute (it's Python), but the job output is never delivered — you'd see "last_status=completed, last_output=" even though contacts.vcf was updated. 3. **Qwen3:8b speed varies.** On CPU-only Ollama it's ~20s/email. On discrete GPU (NVIDIA, AMD ROCm) it's ~2-3s/email. Set `--limit` accordingly: - CPU: 10-15 emails per 5-min cron window - GPU: 50-100 emails per 5-min window 4. **Cron jobs run from the session's last state, not a fresh login.** Environment variables (like `PATH`) may differ. Always use absolute paths or `cd` to the project directory in the script. 5. **`deliver=local` vs `deliver=origin`.** `local` saves output to the cron DB only (viewable via `cronjob action=list`). `origin` sends it back to the Hermes session that created the cron. For per-5min archive runs, `local` avoids spam. For contacts (every 30min), consider `origin` if you want a notification.