mail-async

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Mail Async

Mail Async

Filesystem-backed durable messaging for multi-agent orchestration. Every message is a JSON file written atomically into a per-agent mailbox directory. Messages are never lost -- the write-then-rename pattern guarantees that a reader sees either a complete message or no message, never a partial write.
为多Agent编排提供的基于文件系统的持久化消息通信机制。每条消息都是一个JSON文件,以原子方式写入对应Agent的专属邮箱目录。消息永不丢失——先写入再重命名的模式确保读取者要么看到完整的消息,要么看不到任何消息,绝不会出现读取到部分写入内容的情况。

Purpose

用途

Mail is the high-bandwidth, durable communication channel in the Gastown chipset -- the PCIe equivalent. It carries work assignments from the mayor, completion reports from polecats, merge notifications from the refinery, and coordination messages between any pair of agents. Because messages persist on disk, they survive process crashes, agent restarts, and network disconnections.
Mail是Gastown芯片组中的高带宽、持久化通信通道——相当于PCIe总线。它负责传递来自mayor的工作分配、polecat的完成报告、refinery的合并通知,以及任意Agent之间的协调消息。由于消息持久存储在磁盘上,即使进程崩溃、Agent重启或网络断开,消息也不会丢失。

Filesystem Contract

文件系统约定

.chipset/state/mail/{agent-id}/{timestamp}-{from-agent}.json
Each agent has a dedicated mailbox directory. Incoming messages are named with the creation timestamp and sender ID, guaranteeing unique filenames and natural chronological ordering when listing the directory.
Example paths:
.chipset/state/mail/polecat-alpha/2026-03-05T10-30-00Z-mayor-a1b2c.json
.chipset/state/mail/polecat-alpha/2026-03-05T10-31-00Z-witness-d3e4f.json
.chipset/state/mail/mayor-a1b2c/2026-03-05T10-32-00Z-polecat-alpha.json
.chipset/state/mail/{agent-id}/{timestamp}-{from-agent}.json
每个Agent都有一个专属的邮箱目录。传入消息的文件名由创建时间戳和发送方ID组成,确保文件名唯一,且在列出目录时天然按时间顺序排列。
示例路径:
.chipset/state/mail/polecat-alpha/2026-03-05T10-30-00Z-mayor-a1b2c.json
.chipset/state/mail/polecat-alpha/2026-03-05T10-31-00Z-witness-d3e4f.json
.chipset/state/mail/mayor-a1b2c/2026-03-05T10-32-00Z-polecat-alpha.json

Message Format

消息格式

json
{
  "from": "mayor-a1b2c",
  "to": "polecat-alpha",
  "type": "work_assignment",
  "subject": "Bead gt-abc12 assigned to your rig",
  "body": "Implement the auth middleware as specified in the convoy plan. Priority P1.",
  "timestamp": "2026-03-05T10:30:00Z",
  "read": false,
  "priority": "normal"
}
json
{
  "from": "mayor-a1b2c",
  "to": "polecat-alpha",
  "type": "work_assignment",
  "subject": "Bead gt-abc12 assigned to your rig",
  "body": "Implement the auth middleware as specified in the convoy plan. Priority P1.",
  "timestamp": "2026-03-05T10:30:00Z",
  "read": false,
  "priority": "normal"
}

Field Reference

字段说明

FieldTypeRequiredDescription
from
stringyesSender agent ID
to
stringyesRecipient agent ID
type
stringyesMessage type (see Message Types below)
subject
stringyesShort summary for logs and listings
body
stringyesFull message content
timestamp
stringyesISO 8601 creation timestamp
read
booleanyesWhether recipient has processed this message
priority
stringyes
urgent
,
normal
, or
low
字段类型是否必填描述
from
string发送方Agent ID
to
string接收方Agent ID
type
string消息类型(见下方消息类型列表)
subject
string用于日志和列表展示的简短摘要
body
string消息完整内容
timestamp
stringISO 8601格式的创建时间戳
read
boolean接收方是否已处理该消息
priority
string
urgent
(紧急)、
normal
(普通)或
low
(低优先级)

Message Types

消息类型

TypeSenderRecipientPurpose
work_assignment
mayorpolecatNew bead assigned via hook
completion_report
polecatmayorBead work completed
merge_notification
refinerymayorMerge result (success or conflict)
health_escalation
witnessmayorAgent stall detected
coordination
anyanyGeneral-purpose coordination
类型发送方接收方用途
work_assignment
mayorpolecat通过hook分配新的Bead任务
completion_report
polecatmayorBead任务完成报告
merge_notification
refinerymayor合并结果(成功或冲突)
health_escalation
witnessmayor检测到Agent停滞的上报
coordination
任意Agent任意Agent通用协调消息

Sending a Message

发送消息

The send protocol uses atomic writes to guarantee durability.
发送协议使用原子写入来保证持久性。

Protocol

协议步骤

  1. Construct the message JSON with all required fields
  2. Generate the filename:
    {ISO-timestamp}-{from-agent}.json
    (replace colons with dashes in timestamp for filesystem compatibility)
  3. Serialize with sorted keys for git-friendly output
  4. Write to a temporary file in the recipient's mailbox:
    .chipset/state/mail/{to}/.msg.tmp
  5. Fsync the temporary file to ensure data reaches disk
  6. Rename the temporary file to the final path (atomic on POSIX)
  1. 构造消息:生成包含所有必填字段的JSON消息
  2. 生成文件名
    {ISO时间戳}-{发送方Agent}.json
    (为兼容文件系统,将时间戳中的冒号替换为短横线)
  3. 序列化:按键排序序列化,生成适合Git存储的输出
  4. 写入临时文件:将内容写入接收方邮箱中的临时文件:
    .chipset/state/mail/{to}/.msg.tmp
  5. 同步磁盘:执行Fsync确保数据写入磁盘
  6. 重命名文件:将临时文件重命名为最终路径(在POSIX系统中该操作是原子的)

Pseudocode

伪代码

typescript
async function sendMail(message: MailMessage): Promise<void> {
  const mailDir = join(stateDir, 'mail', message.to);
  await mkdir(mailDir, { recursive: true });

  const safestamp = message.timestamp.replace(/:/g, '-');
  const filename = `${safestamp}-${message.from}.json`;
  const filePath = join(mailDir, filename);

  const content = serializeSorted(message);
  const tmpPath = join(mailDir, '.msg.tmp');

  const fd = await open(tmpPath, 'w');
  try {
    await fd.writeFile(content, 'utf-8');
    await fd.sync();
  } finally {
    await fd.close();
  }
  await rename(tmpPath, filePath);
}
typescript
async function sendMail(message: MailMessage): Promise<void> {
  const mailDir = join(stateDir, 'mail', message.to);
  await mkdir(mailDir, { recursive: true });

  const safestamp = message.timestamp.replace(/:/g, '-');
  const filename = `${safestamp}-${message.from}.json`;
  const filePath = join(mailDir, filename);

  const content = serializeSorted(message);
  const tmpPath = join(mailDir, '.msg.tmp');

  const fd = await open(tmpPath, 'w');
  try {
    await fd.writeFile(content, 'utf-8');
    await fd.sync();
  } finally {
    await fd.close();
  }
  await rename(tmpPath, filePath);
}

Receiving Messages

接收消息

Polling

轮询机制

Agents poll their mailbox directory on each state cycle. Polling returns all JSON files sorted by filename (which is sorted by timestamp due to the naming convention).
typescript
async function checkMail(agentId: string): Promise<MailMessage[]> {
  const mailDir = join(stateDir, 'mail', agentId);
  let files: string[];
  try {
    files = (await readdir(mailDir)).filter(f => f.endsWith('.json')).sort();
  } catch {
    return []; // No mailbox yet
  }

  const messages: MailMessage[] = [];
  for (const file of files) {
    const msg = await readJson<MailMessage>(join(mailDir, file));
    if (msg) messages.push(msg);
  }
  return messages;
}
Agent在每个状态周期内轮询自己的邮箱目录。轮询返回所有JSON文件,并按文件名排序(由于命名规则,文件名顺序对应时间戳顺序)。
typescript
async function checkMail(agentId: string): Promise<MailMessage[]> {
  const mailDir = join(stateDir, 'mail', agentId);
  let files: string[];
  try {
    files = (await readdir(mailDir)).filter(f => f.endsWith('.json')).sort();
  } catch {
    return []; // 邮箱目录尚未创建
  }

  const messages: MailMessage[] = [];
  for (const file of files) {
    const msg = await readJson<MailMessage>(join(mailDir, file));
    if (msg) messages.push(msg);
  }
  return messages;
}

Reading Unread Messages

读取未读消息

Filter by
read === false
to get only unprocessed messages:
typescript
const unread = (await checkMail(agentId)).filter(m => !m.read);
通过过滤
read === false
来获取未处理的消息:
typescript
const unread = (await checkMail(agentId)).filter(m => !m.read);

Marking as Read

标记为已读

Update the
read
field atomically by rewriting the message file:
typescript
async function markRead(agentId: string, filename: string): Promise<void> {
  const filePath = join(stateDir, 'mail', agentId, filename);
  const msg = await readJson<MailMessage>(filePath);
  if (!msg || msg.read) return;

  msg.read = true;
  await atomicWrite(filePath, serializeSorted(msg));
}
通过重写消息文件来原子更新
read
字段:
typescript
async function markRead(agentId: string, filename: string): Promise<void> {
  const filePath = join(stateDir, 'mail', agentId, filename);
  const msg = await readJson<MailMessage>(filePath);
  if (!msg || msg.read) return;

  msg.read = true;
  await atomicWrite(filePath, serializeSorted(msg));
}

Archival

归档机制

Messages older than 24 hours are moved to an archive subdirectory to keep the active mailbox small and fast to scan.
.chipset/state/mail/{agent-id}/archive/{filename}
超过24小时的消息会被移动到归档子目录,以保持活跃邮箱的小巧和快速扫描。
.chipset/state/mail/{agent-id}/archive/{filename}

Archival Protocol

归档协议

  1. List all files in the agent's mailbox
  2. For each file, parse the timestamp from the filename
  3. If the timestamp is more than 24 hours old, move the file to
    archive/
  4. Archive moves use rename (atomic, no data loss)
typescript
async function archiveOldMail(agentId: string): Promise<number> {
  const mailDir = join(stateDir, 'mail', agentId);
  const archiveDir = join(mailDir, 'archive');
  const cutoff = Date.now() - 24 * 60 * 60 * 1000;
  let archived = 0;

  const files = (await readdir(mailDir)).filter(f => f.endsWith('.json'));
  for (const file of files) {
    const tsStr = file.split('-').slice(0, 3).join('-').replace(/T/, 'T');
    const fileTime = new Date(tsStr).getTime();
    if (fileTime < cutoff) {
      await mkdir(archiveDir, { recursive: true });
      await rename(join(mailDir, file), join(archiveDir, file));
      archived++;
    }
  }
  return archived;
}
  1. 列出Agent邮箱中的所有文件
  2. 解析每个文件名中的时间戳
  3. 如果时间戳超过24小时,将文件移动到
    archive/
    目录
  4. 归档移动操作使用重命名(原子操作,无数据丢失)
typescript
async function archiveOldMail(agentId: string): Promise<number> {
  const mailDir = join(stateDir, 'mail', agentId);
  const archiveDir = join(mailDir, 'archive');
  const cutoff = Date.now() - 24 * 60 * 60 * 1000;
  let archived = 0;

  const files = (await readdir(mailDir)).filter(f => f.endsWith('.json'));
  for (const file of files) {
    const tsStr = file.split('-').slice(0, 3).join('-').replace(/T/, 'T');
    const fileTime = new Date(tsStr).getTime();
    if (fileTime < cutoff) {
      await mkdir(archiveDir, { recursive: true });
      await rename(join(mailDir, file), join(archiveDir, file));
      archived++;
    }
  }
  return archived;
}

Cross-Channel Integration

跨通道集成

Mail integrates with other communication channels in a defined flow:
  1. Hook set (hook-persistence): Mayor assigns work via hook
  2. Mail sent (mail-async): Mayor sends
    work_assignment
    message with details
  3. GUPP activation: Agent picks up hook, reads mail for context
  4. Work execution: Agent processes the bead
  5. Mail sent (mail-async): Agent sends
    completion_report
    to mayor
  6. Hook cleared (hook-persistence): Work item retired
If an agent stalls during step 4, the witness detects via nudge (nudge-sync) and escalates to the mayor via
health_escalation
mail.
Mail与其他通信通道按定义好的流程集成:
  1. 设置Hook(hook-persistence):Mayor通过hook分配任务
  2. 发送邮件(mail-async):Mayor发送
    work_assignment
    消息包含任务详情
  3. 激活GUPP:Agent获取hook,读取邮件获取上下文
  4. 执行任务:Agent处理Bead任务
  5. 发送邮件(mail-async):Agent向Mayor发送
    completion_report
  6. 清除Hook(hook-persistence):任务项标记为已完成
如果Agent在步骤4中停滞,witness会通过nudge(nudge-sync)检测到,并通过
health_escalation
邮件上报给Mayor。

Error Handling

错误处理

ConditionBehavior
Recipient mailbox doesn't existCreated automatically on first send
Corrupt JSON in mailboxLogged as warning, skipped during polling
Disk full during writeOS error propagated, temp file left for cleanup
Concurrent writes to same mailboxSafe -- each message has a unique filename
Agent terminated with unread mailMail persists; available if agent restarts
情况处理行为
接收方邮箱不存在首次发送时自动创建
邮箱中存在损坏的JSON文件记录警告日志,轮询时跳过该文件
写入时磁盘已满抛出操作系统错误,保留临时文件以便清理
同时向同一邮箱写入消息安全——每条消息都有唯一的文件名
Agent终止时存在未读消息消息持久化存储;Agent重启后仍可读取

Constraints

约束条件

  • Filesystem only: No sockets, no tmux, no network. All communication through JSON files
  • Write-once: Message content is immutable after creation. Only the
    read
    field changes
  • No guaranteed delivery order: Recipients poll; order depends on filesystem listing
  • No acknowledgment protocol: Sender does not get confirmation of receipt. Use mail replies for coordination
  • 24-hour retention: Active mailbox keeps messages for 24 hours before archival
  • 仅依赖文件系统:不使用套接字、tmux或网络。所有通信通过JSON文件完成
  • 一次写入:消息内容创建后不可修改。仅
    read
    字段可变更
  • 不保证投递顺序:接收方通过轮询获取消息;顺序取决于文件系统的列表顺序
  • 无确认协议:发送方不会收到已读确认。如需确认,使用邮件回复进行协调
  • 24小时保留期:活跃邮箱中的消息在24小时后会被归档