---
name: email-ingest
description: "Operator-initiated email ingestion. Activate when the operator asks to check, triage, or pull in new email ('check my emails', 'any new mail?', 'pull in the latest emails', 'what came in overnight'). Drives a list → summarise → ask → ingest loop using the email-fetch and email-ingest tools. Human-in-the-loop: nothing enters the graph without an explicit operator decision per message."
---

# Email ingestion

Operator-initiated, human-in-the-loop ingestion of the agent's IMAP inbox into the graph. New messages enter the knowledge graph **only** when the operator says so, and only the ones they pick. There is no background poll, no automatic classifier, no silent writes.

## When to activate

Activate when the operator wants to bring new email into the agent's memory. Typical phrasings:

- "Check my emails."
- "Any new mail?"
- "Pull in the latest emails."
- "What came in overnight?"

Do **not** activate for:

- Searching, reading, or recalling email already in the inbox or the graph — those are `email-search`, `email-read`, `email-graph-query`.
- Drafting or sending — that's `email-composition`.
- Setup, status, or OTP extraction — those are direct tool calls.

## Flow

### 1. List

Call `email-fetch`. It pulls new messages since the stored high-water mark (or a `since_uid` if the operator asked for a wider window) and stages them in-memory for the follow-up ingest. **It writes nothing to the graph.**

If `email-fetch` returns "No new messages", say that and stop. There is nothing to ingest.

### 2. Summarise

Present the listed messages to the operator as a numbered or per-`Message-ID` summary. For each, surface:

- Sender (name + address).
- Subject.
- A one-line gist drawn from the body preview (not the full preview — keep it tight).
- A suggested disposition where the message is obviously one or the other (e.g., "newsletter — looks like discard", "thread reply from a known correspondent — looks like ingest"). The suggestion is a hint, never an action; the operator decides.

Do not classify suspicious vs clean — there is no screener. If a message looks like phishing, surface that observation in plain English so the operator can discard it by eye.

### 3. Ask

Ask the operator to confirm a disposition for **every** message in the list. Either:

- A blanket call ("ingest all", "discard everything"), which you turn into the per-message map.
- An explicit per-message choice ("ingest 1, 2, 4; discard 3").

If the operator answers ambiguously ("the work ones"), restate the messages you think they mean and confirm before proceeding. Never guess.

### 4. Ingest

Call `email-ingest` with a `decisions` array containing one entry per Message-ID returned by `email-fetch`. The tool refuses if any pending Message-ID is missing a decision — that's the safeguard against silent drops, not an error to retry around. If it refuses, go back to the operator and ask about the missing ones.

On success the tool returns a one-line summary (ingested / discarded / thread counts) and advances the inbox high-water mark.

### 5. Confirm

Echo the tool's summary back to the operator in one sentence. If they want to draft a reply to anything that was just ingested, hand off to `email-composition`.

## Edge cases

| Situation | Behaviour |
|-----------|-----------|
| `email-fetch` returns zero new messages | Tell the operator and stop. Do not call `email-ingest`. |
| A listed message has no `Message-ID` header | Surface it by UID and ask the operator how to handle it. `email-ingest` cannot apply a per-message decision to it, so either widen the search or skip it explicitly. |
| Operator wants to ingest some now and review the rest later | Refuse cleanly. The high-water mark advances on every successful ingest, so a partial decision would silently drop the rest. Ask them to commit one way or the other on every message, or to wait. |
| Operator abandons mid-flow | Do not call `email-ingest`. The staged batch stays in memory until the next `email-fetch` overwrites it; the high-water mark does not move. |
| `email-fetch` errors (IMAP down, auth failure) | Surface the error verbatim. Do not retry silently. |

## Out of scope

- **Automatic re-screening or LLM classification.** The screener was removed; the operator is the screener now.
- **Scheduled or recurring ingestion.** There is no cron, no interval, no background trigger. The operator initiates every cycle.
- **Per-thread or per-sender allowlists.** Each message gets a fresh decision.
- **Other channels.** WhatsApp and the public web have their own ingestion paths.

## Observability

Each tool emits a single tagged log line:

- `[email-fetch] tag=email-fetch listed=<n> sinceUid=<uid>` — one per list call.
- `[email-ingest] tag=email-ingest ingested=<n> discarded=<m>` — one per ingest call.

A complete cycle is greppable as the pair `email-fetch` → `email-ingest` for the same session.
