Continuation Emails

When the orchestrator hits the per-run iteration limit (default 8, configurable via --max-iterations), or the LLM sets status='waiting', it sends a continuation email to envoy@critchley.biz. On the next cron run, the continuation email is detected and processing resumes with full state restored.

Design

Based on the self-email checkpoint design in Design Decisions Supplement (section 7). Continuation emails enable crash recovery, pause/resume, visible progress, manual abort, and natural rate limiting.

The continuation email carries state in the same way data passes between iterations of the processing loop — the working_note, bundle_key, current_phase, gathered note keys, and gathered email references are all preserved.

Email Format

A MIME multipart email with two parts:

1. text/plain body — Human-readable summary containing: • Original email subject, sender, and Message-ID • Iteration count (per-run and total) • Full working_note contents • All notes in context (listed by key) • All emails in context (listed by Message-ID, folder, and subject)

2. application/json attachment (continuation.json) — Machine-readable state:

{
  "type": "continuation",
  "original_message_id": "<...>",
  "original_subject": "...",
  "original_from": "...",
  "iteration": 3,
  "total_iterations": 3,
  "current_phase": "composing",
  "working_note": "...",
  "bundle_key": "scratch/task-bundle",
  "gathered_note_keys": ["envoy/design-spec", ...],
  "gathered_email_refs": [
    {"message_id": "<...>", "folder": "INBOX"}
  ],
  "failed_fetches": {"missing-key": 2}
}

The type: "continuation" key is what identifies the attachment as continuation data.

Detection

In parse_email_message(), all MIME parts are scanned. If an application/json attachment is found containing "type": "continuation", the parsed email dict gets a continuation field with the parsed JSON data.

Resumption

When the main loop encounters an email with continuation data:

1. Restores working_note and current_phase from continuation data2. Re-fetches gathered notes by key3. Re-fetches gathered emails by Message-ID (with folder hints for faster lookup)4. Restores bundle_key — auto-loading resumes from the first iteration5. Restores failed_fetches — prevents re-requesting unavailable resources6. Fetches the original email by Message-ID so the LLM sees the real email, not the continuation7. Processing resumes as a normal iteration with all context restored

Iteration Limits

Two limits prevent runaway processing:

Per-run limit (default 8): configurable via --max-iterations. When hit, sends a continuation email and stops.• Total limit (24): hard limit across all continuations for a single email. Continuation emails that have hit this limit are moved to Done and skipped.

Cleanup

After processing a continuation email (or skipping it due to the total limit), the continuation email itself is moved to Done. The orchestrator also expunges any \Deleted-flagged messages at the start of each run.

Sending

Continuation emails are sent via smtplib to localhost (not the mail command) because MIME multipart with attachments requires proper MIME construction. Uses MIMEMultipart, MIMEText, and MIMEApplication from the email stdlib.

A best-effort copy is also appended to the Sent IMAP folder via client.append() immediately after sending. This enables the --resume flag in test_orchestrator.py to retrieve the latest continuation for testing without waiting for the external mail loop.

Testing

See Testing Guide for test harness details. Key workflow:

1. Inject email: python3 test_orchestrator.py --inject 'Subject' --run --max-iter 32. Continuation is sent externally and stored in Sent IMAP folder3. Resume: python3 test_orchestrator.py --resume --run (fetches continuation from Sent, injects into INBOX as UNSEEN, runs orchestrator)

Confirmed working 2026-02-22: continuation correctly preserves bundle_key, current_phase, failed_fetches, and all gathered context.

version2
updated2026-02-22