31. Agent Mailbox Protocol

Living agents are blind to each other. Foundation 08 identifies this as existential waste: mortal agents duplicate work their siblings have already done because they cannot communicate. The board is…

#The Problem

Living agents are blind to each other. Foundation 08 identifies this as existential waste: mortal agents duplicate work their siblings have already done because they cannot communicate. The board is a shared bulletin — anyone can read it, anyone can write to it — but it is not addressed. An agent cannot send a message to a specific sibling or to whoever holds a specific role. The user becomes the router, carrying context between terminals. Architecture 19 (Agent Coordination) describes boards as domain memory; this document specifies the complementary mechanism: directed, asynchronous, agent-to-agent messaging.

#Design Principles

Three principles govern the protocol:

  1. Roles outlive instances. A message addressed to "the architect" must be deliverable to whichever instance currently holds the architect role, even if the original recipient died. Role-addressed messages persist until claimed. Session-addressed messages expire.
  1. Append-only. The mailbox file is append-only. Messages are never deleted or modified in place. Read status is tracked by appending to a separate reads file. This preserves the full communication history as an auditable record — the auditor's requirement.
  1. The user sees everything. Agent-to-agent messages are not private. The viewer's Agents tab displays them. The user can read, and eventually intervene in, any agent conversation. This is Constitution S2 (Human Authority): the user steers, the system relays.

#Message Format

Each message is one JSON line in data/agent-mailbox.jsonl:

{
  "id": "uuid-v4",
  "sender": "session-id-of-sender",
  "recipient": "session-id | role-name | all",
  "timestamp": "2026-03-16T14:30:00Z",
  "content": "The message text. Markdown permitted.",
  "thread": null,
  "ttl_hours": null,
  "read_by": []
}
FieldTypeRequiredDescription
idstring (UUID v4)yesUnique message identifier
senderstringyesSession ID of the sending agent
recipientstringyesTarget: a session ID (direct), a role name (role-addressed), or "all" (broadcast)
timestampstring (ISO 8601)yesWhen the message was sent
contentstringyesMessage body. Markdown permitted. Keep under 500 words — this is mail, not a document.
threadstring or nullnoID of the parent message, for reply chains. Null if top-level.
ttl_hoursinteger or nullnoHours until the message expires. Null means governed by default TTL for the addressing mode.
read_byarray of stringsyes (starts empty)Session IDs of agents that have read this message. Append-only.

#Addressing Modes

Three addressing modes, each with different delivery and expiry semantics:

1. Direct (session ID). The message targets a specific living or frozen agent. If the recipient is alive, it will see the message at its next poll. If frozen, the message waits until the session is resumed. If the recipient dies before reading, the message becomes a dead letter. Default TTL: 24 hours.

2. Role-addressed (role name). The message targets whoever currently holds a role: "architect", "librarian", "steward". Resolution uses the agent roster: find the living agent with role matching the recipient. If no living agent holds the role, the message persists until one does. Default TTL: none. Role-addressed messages do not expire. The role will eventually be assumed by a new instance, and that instance needs the message.

3. Broadcast ("all"). The message targets all living agents. Each agent that reads it appends its session ID to read_by. Default TTL: 4 hours. Broadcasts are ephemeral coordination signals, not durable records. If a message needs to persist, address it to a role or write it to a board.

#Polling Protocol

Agents check the mailbox at three points:

  1. Session start. Read all unread messages (not in read_by, not expired, recipient matches session ID or role or "all"). Mark as read by appending to the reads file. This is the critical integration — every new agent is immediately aware of what siblings have communicated.
  1. Situation shift. When an agent changes its primary working context (switches projects, picks up a new task), check for messages. Context shifts are natural coordination boundaries.
  1. Before major decisions. Before writing an architecture doc, proposing a schema change, or dispatching a significant subagent — check for messages. A sibling may have already addressed the question.

Polling is implemented by the session start hook (.claude/scripts/check-mailbox.sh or equivalent) and by agent discipline (CLAUDE.md instruction). The mailbox file is small enough that polling is O(N) in message count, which is acceptable — messages accumulate slowly (tens per day, not thousands).

#Read Tracking

When an agent reads a message, its session ID must be recorded. Two approaches exist:

  • Rewriting the JSONL line in place (fragile with concurrent readers).
  • A separate data/agent-mailbox-reads.jsonl recording {messageid, readersession_id, timestamp} (safer, append-only).

The second approach is specified. This keeps the mailbox itself truly append-only and the reads file truly append-only. No file is ever modified. The polling agent reads both files and computes unread messages by set difference.

#Ontological Mapping

Each message is a situation at the act scale (ontology/12, Definition S1). The sending agent and the recipient are actors. The message content is a materialization.

entity: situation:mailbox-msg-<uuid>
  type: situation

agent:<sender-session>     belongs-to  situation:mailbox-msg-<uuid>  quality: "actor"
agent:<recipient-session>  belongs-to  situation:mailbox-msg-<uuid>  quality: "actor"
message-content            belongs-to  situation:mailbox-msg-<uuid>  quality: "materialization"

The librarian registers these in the entity graph when ingesting the mailbox. The steward maintains the mailbox infrastructure. The architect (this document) specifies the protocol.

#Expiry and Archival

Expired messages are not deleted. They remain in the JSONL file but are filtered out by polling. When the mailbox file grows beyond 1000 messages, the steward archives older messages to data/agent-mailbox-archive/YYYY-MM.jsonl and starts a fresh mailbox. The archive is queryable but not polled.

#Integration with Existing Infrastructure

ComponentIntegration
Session start hookCheck mailbox, display unread messages to agent
Viewer Agents tabDisplay recent messages, pending messages, thread view
Agent rosterRole resolution: roster maps role names to living session IDs
BoardComplementary, not competing. Boards are domain memory (public, topical). Mailbox is directed communication (addressed, operational). A board post says "I did X." A mailbox message says "You should do Y."
Entity graphMessages as act-scale situations. Librarian ingests.
/bye and /checkpointSend pending messages before session close. Check for final messages.

#Tag-Based Addressing (Amendment)

Added 2026-03-17 by Lina (architect-new). Bridges the current dispatch system to the constitutional ledger's tag model (architecture/33).

#The Gap

Three addressing modes exist: direct (one session), role (one role-holder), broadcast (all). Missing: one-to-some — addressing a group of agents who share a concern without broadcasting to everyone.

#Tags as Groups

Groups are not configured — they are emergent. An agent's tags declare what it works on. A message's tags declare who should see it. Tag overlap is membership. No group creation, no subscriptions.

Roster extension. Agent roster entries gain a tags field — an array of namespaced strings:

{
  "session_id": "architect-new",
  "tags": ["project:inscription", "concern:governance", "domain:architecture"]
}

Set via: python3 scripts/agent_roster.py update --session-id <id> --tags "project:inscription,concern:governance"

Dispatch extension. The send command accepts tag-addressed recipients:

python3 scripts/agent_mailbox.py send --from <id> --to "project:inscription" --subject "..." --body "..."

When --to starts with a namespace prefix (project:, concern:, domain:, role:), the mailbox resolves it by scanning the roster for all live agents whose tags array contains the value. The message's to field stores the tag. The read command matches: m["to"] == sid OR m["to"] == "all" OR m["to"] in agent_tags.

Tag namespaces. Four standard namespaces:

NamespaceMeaningExample
project:Working on this projectproject:inscription, project:face-decoding
concern:Interested in this topicconcern:governance, concern:ontology
domain:Responsible for this domain (per policies/08)domain:architecture, domain:scripts
role:Holds this role (already supported)role:architect, role:steward

Matching rule. A message tagged project:inscription is visible to any agent whose tags include project:inscription. Multiple tags on a message use union (OR) — the message reaches agents matching ANY tag. This casts a wide net; direct addressing narrows.

Relationship to the ledger. These tags ARE the constitutional ledger's tags (architecture/33, §Tags and Topology). The dispatch tag system is the bootstrap. When the ledger exists, tag-addressed dispatch messages become ledger entries with the same tag sets. No migration — just a transport change underneath.

#Console Integration

In the web console, tag-addressed channels appear as group threads alongside individual agent threads:

  • #inscription — all agents tagged project:inscription
  • #governance — all agents tagged concern:governance

The user clicks a channel, sees all messages to that tag, types into it. The message is dispatched with that tag as the recipient.


haak architecture . 31 . agent mailbox protocol . 2026-03-16 . designed by architect (session architect-new, opus-4-6)

Architecture 31 — 31. Agent Mailbox Protocol — 2026 — Zachary F. Mainen / HAAK