name: email-manual
description: >
Operational guide for the email tool — LingTai email protocol within
your .lingtai/ network. Covers send/check/read/dismiss/reply/reply_all/
search/delete/archive/contacts, reply discipline (always reply on the
channel the message arrived on), addressing (bare paths like human,
mimo-1), self-send for persistent notes that survive molt, time
capsules (delayed self-send via delay), scheduled email (recurring
alarms via schedule={action: "create", ...}), the unread digest
notification contract, and addon ownership. This is for INTERNAL email
only — for real internet email via IMAP/SMTP, see the mcp-manual
skill (the lingtai-imap addon owns that surface).
version: 1.0.0
tags: [capabilities, email, communication]
Email Manual — the internal email tool
LingTai email protocol between agents in your
.lingtai/network. Not the internet. No IMAP, no SMTP, no DNS. Messages are JSON files written undermailbox/inbox/of the recipient agent andmailbox/sent/of the sender.
1. What is internal email
The email tool moves messages as files between agents that share a .lingtai/ directory tree:
- Sending writes a
message.jsoninto the recipient'smailbox/inbox/<uuid>/and the sender'smailbox/sent/<uuid>/. - Delivery is handled by a per-recipient daemon thread (
_mailman) — synchronous from the sender's perspective oncedelay=0. - Read state lives in the recipient's
mailbox/read.json(a set of message IDs). - The kernel mirrors current unread mail into
.notification/email.json, which surfaces as a notification block read vianotification(action="check")— that's how you find out new mail arrived.
There is no concept of an SMTP server, an MX record, or an external address. If a request involves @gmail.com, @outlook.com, IMAP folders, or anything that needs to leave the machine, the right tool is the lingtai-imap MCP addon — see the mcp-manual skill, not this one.
Internal Email vs IMAP
| Feature | Internal Email (this skill) | IMAP (see mcp-manual) |
|---|---|---|
| What | LingTai email protocol within .lingtai/ network |
Real email via IMAP/SMTP (Gmail, Outlook, etc.) |
| Address format | Bare path (e.g. human, mimo-1) |
@ address (e.g. alice@gmail.com) |
| Tool | email (intrinsic) |
imap (MCP server, lingtai-imap addon) |
| Reply policy | Always reply on the same channel | Requires confirmation for unknown senders |
| Persistence | Survives molt, lives in working directory | External mailbox, managed by IMAP server |
| Use case | Agent-to-agent communication, self-send, time capsules | Real-world email integration |
This skill covers INTERNAL email only. For IMAP/SMTP email, see the mcp-manual skill.
2. Addressing
Addresses are bare directory names inside .lingtai/. No @, no domains, no slashes.
| Address | Meaning |
|---|---|
human |
The human's mailbox at .lingtai/human/ |
mimo-1 |
An agent whose working directory is .lingtai/mimo-1/ |
<your-own-name> |
Self — creates an inbox entry that survives molt (§6) |
Multiple recipients: pass address as a string or a list, plus optional cc / bcc.
email(action="send", address=["mimo-1", "scribe"], cc=["human"],
subject="status", message="ready")
To discover who exists: glob .lingtai/*/.agent.json from a shell. Use the agent_name field of each as the address. Do not invent addresses — bounces write a system.bounce event into .notification/system.json and silently absorb your message.
When displaying a sender to the human or another agent, prefer sender_nickname if set, else sender_name (both are in the inbound message's identity block). Bare addresses are routable but ugly.
3. Reply discipline — the one rule you cannot break
Reply on the channel the message arrived on.
If a message arrived via email, reply with email(action="reply", ...). Do not pivot to pigeon, IM, or a fresh send. If you must change channels (e.g. the original sender is dead), explain that pivot in the reply body before sending it elsewhere.
Prefer reply and reply_all over send even when you know the addresses:
replypreserves the thread linkage (the originalidlands in the new message'sin_reply_to), so a futuresearchorcheckshows the conversation as related.reply_allmirrors the original recipient set automatically, so you don't drop someone who wascc'd.sendis for new conversations.
Doing it the other way scatters conversations across orphaned threads and is the single most common confusion source in human-facing audits.
4. Sender display name resolution
Inbound mail carries an identity block:
"identity": {
"sender_name": "mimo-1",
"sender_nickname": "MiMo",
"via": "lingtai" | "claude-code" | ...
}
When you mention the sender in a reply body or in a summary you give the human, use sender_nickname if it is set and non-empty; otherwise fall back to sender_name. The address itself (from) is for routing, not for prose.
5. Actions — full surface
The email tool dispatches by action. All actions take optional mode for output verbosity (see §11).
| Action | Purpose | Required args |
|---|---|---|
send |
Start a new thread to one or more recipients | address, subject, message |
check |
List inbox (newest-first), with optional filter={...} and n=N |
— |
read |
Fetch full body + attachments and mark read | email_id (list of IDs) |
dismiss |
Mark read without fetching body — for digest-preview-only mail | email_id (list of IDs) |
reply |
Reply to sender only; preserves thread linkage | email_id, message |
reply_all |
Reply to sender + all original recipients minus self | email_id, message |
search |
Search across inbox/sent/archive by query + filter |
query (and/or filter) |
archive |
Move from inbox to archive folder (keeps thread, removes from view) | email_id |
delete |
Permanently delete | email_id |
contacts |
List your address book | — |
add_contact |
Add or upsert by address |
address, name, optional note |
remove_contact |
Remove by address |
address |
edit_contact |
Update fields | address, plus the fields to change |
read vs dismiss — when to use which
The unread digest in .notification/email.json already contains a preview of up to 200 characters per unread message. If that preview is enough to act on, call dismiss — same effect on read state, no body returned, no token cost. Only call read when you actually need the full body or attachments.
The kernel removes the unread-mail notification once count hits 0, so failing to dismiss/read keeps the digest reminding you on every heartbeat.
check filter
check accepts a structured filter for narrowing the inbox without round-tripping:
email(action="check", filter={
"unread_only": True,
"from": "mimo-1",
"subject": "status",
"contains": "blocker",
"after": "2026-05-18T00:00:00Z",
"has_attachments": False,
"sort": "newest",
"truncate": 500, # body preview length per entry
}, n=20)
Use this aggressively. Pulling 100 messages with check and then post-filtering in your head is wasteful.
6. Self-send — persistent notes that survive molt
Mail sent to your own address lands in your own inbox. It is marked self-sent, but otherwise behaves like any other unread message — meaning:
- It survives a molt (because it lives in
mailbox/inbox/, not in chat history). - It surfaces in the unread digest until you
readordismissit. - It can be
searched by the future you.
Use this for: TODOs you want to remember after a memory rotation, breadcrumbs about decisions, "hand-off to self" notes during a long task.
email(action="send", address="<your-own-name>",
subject="resume here", message="Picked the Helmholtz approach; see paper/drafts/2026-05-18.md")
7. Time capsule — delayed self-send
Add delay=<seconds> to defer delivery. The outbox entry is written immediately; the _mailman daemon thread sleeps until the deadline, then dispatches.
# Remind self in 1 hour
email(action="send", address="<your-own-name>", delay=3600,
subject="check on long task", message="Did `daemon(check, id=...)` finish?")
Combined with self-send, this gives you cheap alarms without standing up a cron. The notification is delivered exactly once. For recurring reminders, use schedule (§8).
Use delayed self-send as a future nudge, not delayed tool execution. The message should tell the future you what to inspect and why, then let that future turn decide with current context whether to run bash(action="poll"), daemon(check), a channel read, or nothing at all. This is the preferred escape hatch when a repeated-call _advisory tells you that you may be polling the same thing: write one concrete reminder, then yield/idle instead of immediately calling the same tool again.
8. Scheduled email — recurring alarms
For repeating messages, use the schedule family — a nested action on the email tool:
# Every 30 min, up to 10 times
email(schedule={
"action": "create",
"interval": 1800,
"count": 10,
}, address="<your-own-name>",
subject="watering reminder", message="check the build")
# List active/inactive schedules
email(schedule={"action": "list"})
# Cancel one
email(schedule={"action": "cancel", "schedule_id": "<id>"})
# Reactivate (schedules go `inactive` on agent startup by default)
email(schedule={"action": "reactivate", "schedule_id": "<id>"})
Schedule lifecycle: active → inactive (on cancel) or completed (count hit zero). The scheduler reconciles on startup by flipping all active schedules to inactive so nothing fires unexpectedly after a restart — you must reactivate deliberately.
9. Privacy — internal IDs
The mailbox UUID (email_id) is local to your working directory. Never paste a raw mailbox ID into a message to another agent or to the human — it has no meaning outside your tree and reveals nothing useful. Refer to messages by subject + from + approximate time. If you must show the human exactly which mail you're acting on, use the subject line and sender.
The exception: when you call email(action="read"/"dismiss"/"reply", email_id=[...]), you pass IDs you read out of your own digest. That's internal plumbing, fine.
10. Addon ownership — what this skill does NOT cover
This skill is the manual for the kernel-intrinsic email tool only. Adjacent surfaces live elsewhere:
| Want to … | Use |
|---|---|
| Send/receive real internet email (Gmail, etc.) | mcp-manual → lingtai-imap MCP addon |
| Send Telegram / Feishu / WeChat messages | mcp-manual → respective MCP addon |
| Send a notification-style ping to another agent | This skill — it IS the notification channel |
| Schedule a one-off wake-up of your own loop | This skill, delay + self-send (§7) |
| Schedule recurring agent-side work | This skill, schedule={action: "create"} (§8) |
The IMAP, Telegram, Feishu, and WeChat MCP addons each ship their own SKILL.md and mcp-manual entry. They are separate processes, separate auth surfaces, and separate failure modes. Do not try to use the email tool to reach an external address — you will write a file into a non-existent .lingtai/foo@bar.com/ and the message will bounce silently.
11. Mode (output verbosity)
Every email action accepts an optional mode (set by mode_field) controlling how much detail is rendered in the tool result. Defaults are usually fine. Bump it when you are debugging delivery or read-state issues; lower it when you are iterating in a tight loop and only need confirmation.
12. Quick reference — common recipes
# Triaging unread without reading bodies
email(action="check", filter={"unread_only": True}, n=20)
# Read one message in full
email(action="read", email_id=["<id-from-digest>"])
# Acknowledge a digest-preview-sufficient message
email(action="dismiss", email_id=["<id>"])
# Thread-preserving reply
email(action="reply", email_id=["<id>"], message="ack, looking now")
# Self-note that survives molt
email(action="send", address="<self>", subject="resume", message="next: ...")
# 5-minute timer
email(action="send", address="<self>", delay=300,
subject="ding", message="check the deploy")
# Recurring nudge every 10 min, 6 times
email(schedule={"action": "create", "interval": 600, "count": 6},
address="<self>", subject="stretch", message="stand up")
# Find related mail
email(action="search", query="helmholtz",
filter={"after": "2026-05-01T00:00:00Z"})
# Address book
email(action="add_contact", address="mimo-1", name="MiMo (vision)",
note="reachable for image-analysis requests")
Found a bug or issue? If you encounter any problems with this skill, load the
lingtai-issue-reportskill and follow its instructions to report it.
Cleanup / Footprint
Internal email persists under the agent mailbox: inbox/archive/sent message
files, attachments, contacts, and schedule metadata. Mail is also memory: do not
blindly delete it. Prefer email(archive), email(delete), or schedule cancel
verbs over rm, and never delete mail that is the only copy of a decision,
handoff, or attachment the human may expect you to retain.
Footprint check (read-only, records the audit):
python3 - <<'PY'
import json, time
from pathlib import Path
agent = Path.cwd()
roots = [p for p in [agent / "mailbox", agent / "mail", agent / "email"] if p.exists()]
def size(p):
return p.stat().st_size if p.is_file() else sum(f.stat().st_size for f in p.rglob("*") if f.is_file())
rows = [(p, size(p)) for r in roots for p in ([r] if r.is_file() else r.iterdir())]
total = sum(s for _, s in rows)
print(f"internal email roots: {[str(r) for r in roots]}")
print(f"top-level entries: {len(rows)}; bytes: {total}")
for p, s in sorted(rows, key=lambda r: r[1], reverse=True)[:20]: print(f"{s:>12} {p}")
log = agent / "logs" / "cleanup.jsonl"; log.parent.mkdir(parents=True, exist_ok=True)
log.open("a", encoding="utf-8").write(json.dumps({"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "tool": "email", "dry_run": True, "candidates": len(rows), "bytes": total, "human_approved": False, "summary": "internal email footprint audit"}) + "\n")
PY
Recommended cadence: when large attachments are exchanged, before exporting or
archiving a project, and quarterly for long-lived agents. Cleanup requires a
dry-run report plus explicit user consent; after deletion/archive, append an
apply record to logs/cleanup.jsonl.