Files
2026-09-06 13:51:14 +00:00

4.8 KiB
Raw Permalink Blame History

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)

# 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:

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:

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)

# 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:

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
    hermes cron update <archive-id> --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:

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.