Loading...
Loading...
Durable asynchronous messaging channel for inter-agent communication. Implements write-once read-many filesystem mail using atomic writes and directory-based mailboxes.
npx skill4agent add tibsfox/gsd-skill-creator mail-async.chipset/state/mail/{agent-id}/{timestamp}-{from-agent}.json.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{
"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 | 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 | |
| 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 |
{ISO-timestamp}-{from-agent}.json.chipset/state/mail/{to}/.msg.tmpasync 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);
}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;
}read === falseconst unread = (await checkMail(agentId)).filter(m => !m.read);readasync 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));
}.chipset/state/mail/{agent-id}/archive/{filename}archive/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;
}work_assignmentcompletion_reporthealth_escalation| 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 |
read