Messaging
Swarm agents communicate through a SQLite-backed mailbox system. Messages are stored durably in a shared database, delivered to recipients on their next prompt build, and can trigger real-time interrupts for urgent communications.
Design
The messaging system uses SQLite in WAL (Write-Ahead Logging) mode as the message store. This choice (ADR-002) provides:
- Durability — Messages survive process crashes
- Concurrent access — WAL mode allows multiple readers with a single writer
- No external dependencies — No message broker or network service required
- Simplicity — A single file at
.swarm/messages.db
Message Structure
The Message struct represents a single message:
| Field | Type | Description |
|---|---|---|
id | i64 | Auto-incrementing primary key |
thread_id | Option<i64> | ID of the root message in the thread (for grouping) |
reply_to | Option<i64> | ID of the message this is a direct reply to |
sender | String | Name of the sending agent (or "operator" for CLI messages) |
recipient | String | Name of the receiving agent |
msg_type | MessageType | Discriminator: Message, Task, Status, or Nudge |
urgency | Urgency | Normal or Urgent |
body | String | The message content |
created_at | i64 | Epoch nanoseconds when the message was created |
delivered_at | Option<i64> | Epoch nanoseconds when consumed; NULL while pending |
MessageType
| Variant | Usage |
|---|---|
Message | General inter-agent communication |
Task | Task assignment or delegation |
Status | Status updates between agents |
Nudge | Liveness nudge from the monitoring system |
Urgency
| Variant | Behavior |
|---|---|
Normal | Delivered on the recipient's next prompt build |
Urgent | Triggers an interrupt via the router, causing the recipient to restart its session |
Mailbox Operations
The Mailbox struct provides per-agent messaging operations:
| Operation | Description |
|---|---|
send(recipient, body, msg_type, urgency) | Send a message to another agent (self-send rejected) |
reply(original_id, body, msg_type, urgency) | Reply to an existing message, inheriting thread context |
broadcast(recipients, body, msg_type, urgency) | Send to multiple agents in a single transaction |
consume() | Atomically read and mark all pending messages as delivered |
thread(thread_id) | Retrieve all messages in a conversation thread |
outbox(limit) | Get recently sent messages |
Free functions are also available for use outside the Mailbox context:
send_message()— Send a message using a raw connectionbroadcast_message()— Broadcast to multiple recipientsconsume_messages()— Consume pending messages for an agent
Message Router
The router module runs an async polling loop that watches for urgent messages:
Router Loop (every 100ms):
1. poll_urgent(conn) → Vec<UrgentMessage>
2. For each urgent message:
a. Skip if already signalled (deduplication via HashSet)
b. Send InterruptSignal to recipient's mpsc channel
c. Add to signalled set
3. Sleep 100ms
4. Exit on shutdown signal
When the router sends an InterruptSignal, the agent's runner receives an UrgentMessage event, which triggers the interrupt flow:
Running→Interrupting(withCancelSessionside effect)- The backend session is gracefully cancelled
- On session exit →
BuildingPrompt(the new prompt will include the urgent message)
Message Threading
Messages can be organized into threads using thread_id and reply_to:
- When you send a new message,
thread_idandreply_toareNULL - When you reply to a message, the reply inherits the original's
thread_id(or uses the original'sidas the thread root) - The
thread()method retrieves all messages sharing the samethread_id
Database Configuration
The SQLite database is configured with these PRAGMAs:
| PRAGMA | Value | Purpose |
|---|---|---|
journal_mode | WAL | Concurrent reads, single writer |
busy_timeout | 5000 ms | Wait up to 5 seconds on lock contention |
Periodic maintenance tasks run in the background:
| Task | Interval | Action |
|---|---|---|
| WAL checkpoint | 60 seconds | PRAGMA wal_checkpoint(TRUNCATE) — reclaims WAL file space |
| Message prune | 300 seconds | Delete old delivered messages, keeping the most recent 1000 |
Message Flow in Practice
- Agent A calls the mailbox tool to send a message to Agent B
- The message is
INSERTed into themessagestable withdelivered_at = NULL - If the message is urgent, the router detects it within 100ms and sends an
InterruptSignalto Agent B - Agent B's runner cancels its current session and rebuilds the prompt
- On the next prompt build,
consume()marks all pending messages as delivered and includes them in the system prompt - Agent B reads the messages in its prompt context and responds accordingly
Related
- ADR-002: SQLite Messaging — Design rationale
- Message Schema — Full database schema
- Agent Lifecycle — How interrupts affect the state machine