mail-async
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMail 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}.jsonEach 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.jsonMessage 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
字段说明
| Field | Type | Required | Description |
|---|---|---|---|
| string | yes | Sender agent ID |
| string | yes | Recipient agent ID |
| string | yes | Message type (see Message Types below) |
| string | yes | Short summary for logs and listings |
| string | yes | Full message content |
| string | yes | ISO 8601 creation timestamp |
| boolean | yes | Whether recipient has processed this message |
| string | yes | |
| 字段 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
| string | 是 | 发送方Agent ID |
| string | 是 | 接收方Agent ID |
| string | 是 | 消息类型(见下方消息类型列表) |
| string | 是 | 用于日志和列表展示的简短摘要 |
| string | 是 | 消息完整内容 |
| string | 是 | ISO 8601格式的创建时间戳 |
| boolean | 是 | 接收方是否已处理该消息 |
| string | 是 | |
Message Types
消息类型
| Type | Sender | Recipient | Purpose |
|---|---|---|---|
| mayor | polecat | New bead assigned via hook |
| polecat | mayor | Bead work completed |
| refinery | mayor | Merge result (success or conflict) |
| witness | mayor | Agent stall detected |
| any | any | General-purpose coordination |
| 类型 | 发送方 | 接收方 | 用途 |
|---|---|---|---|
| mayor | polecat | 通过hook分配新的Bead任务 |
| polecat | mayor | Bead任务完成报告 |
| refinery | mayor | 合并结果(成功或冲突) |
| witness | mayor | 检测到Agent停滞的上报 |
| 任意Agent | 任意Agent | 通用协调消息 |
Sending a Message
发送消息
The send protocol uses atomic writes to guarantee durability.
发送协议使用原子写入来保证持久性。
Protocol
协议步骤
- Construct the message JSON with all required fields
- Generate the filename: (replace colons with dashes in timestamp for filesystem compatibility)
{ISO-timestamp}-{from-agent}.json - Serialize with sorted keys for git-friendly output
- Write to a temporary file in the recipient's mailbox:
.chipset/state/mail/{to}/.msg.tmp - Fsync the temporary file to ensure data reaches disk
- Rename the temporary file to the final path (atomic on POSIX)
- 构造消息:生成包含所有必填字段的JSON消息
- 生成文件名:(为兼容文件系统,将时间戳中的冒号替换为短横线)
{ISO时间戳}-{发送方Agent}.json - 序列化:按键排序序列化,生成适合Git存储的输出
- 写入临时文件:将内容写入接收方邮箱中的临时文件:
.chipset/state/mail/{to}/.msg.tmp - 同步磁盘:执行Fsync确保数据写入磁盘
- 重命名文件:将临时文件重命名为最终路径(在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 to get only unprocessed messages:
read === falsetypescript
const unread = (await checkMail(agentId)).filter(m => !m.read);通过过滤来获取未处理的消息:
read === falsetypescript
const unread = (await checkMail(agentId)).filter(m => !m.read);Marking as Read
标记为已读
Update the field atomically by rewriting the message file:
readtypescript
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));
}通过重写消息文件来原子更新字段:
readtypescript
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
归档协议
- List all files in the agent's mailbox
- For each file, parse the timestamp from the filename
- If the timestamp is more than 24 hours old, move the file to
archive/ - 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;
}- 列出Agent邮箱中的所有文件
- 解析每个文件名中的时间戳
- 如果时间戳超过24小时,将文件移动到目录
archive/ - 归档移动操作使用重命名(原子操作,无数据丢失)
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:
- Hook set (hook-persistence): Mayor assigns work via hook
- Mail sent (mail-async): Mayor sends message with details
work_assignment - GUPP activation: Agent picks up hook, reads mail for context
- Work execution: Agent processes the bead
- Mail sent (mail-async): Agent sends to mayor
completion_report - 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 mail.
health_escalationMail与其他通信通道按定义好的流程集成:
- 设置Hook(hook-persistence):Mayor通过hook分配任务
- 发送邮件(mail-async):Mayor发送消息包含任务详情
work_assignment - 激活GUPP:Agent获取hook,读取邮件获取上下文
- 执行任务:Agent处理Bead任务
- 发送邮件(mail-async):Agent向Mayor发送
completion_report - 清除Hook(hook-persistence):任务项标记为已完成
如果Agent在步骤4中停滞,witness会通过nudge(nudge-sync)检测到,并通过邮件上报给Mayor。
health_escalationError Handling
错误处理
| Condition | Behavior |
|---|---|
| Recipient mailbox doesn't exist | Created automatically on first send |
| Corrupt JSON in mailbox | Logged as warning, skipped during polling |
| Disk full during write | OS error propagated, temp file left for cleanup |
| Concurrent writes to same mailbox | Safe -- each message has a unique filename |
| Agent terminated with unread mail | Mail 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 field changes
read - 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小时后会被归档