# How the SwarmForge Handoff Daemon Delivers Messages Between Agents

> Discover how the SwarmForge handoff daemon delivers messages between agents by polling files, routing to inboxes, and waking sleeping agents via tmux.

- Repository: [Robert C. Martin/swarm-forge](https://github.com/unclebob/swarm-forge)
- Tags: how-to-guide
- Published: 2026-08-08

---

**The SwarmForge handoff daemon delivers messages between agents by polling for `*.handoff` files in sender outboxes, routing them to recipient inboxes based on the `to` header, and triggering tmux notifications to wake sleeping agents.**

The `unclebob/swarm-forge` repository implements an asynchronous, file-based message bus for coordinating AI agents through tmux sessions. At the heart of this system sits the **handoff daemon** (`swarmforge/scripts/handoffd.bb`), a Babashka script that continuously monitors agent outboxes to ensure reliable message delivery without network servers.

## Loading Agent Roles from roles.tsv

At startup, the daemon initializes its routing table by calling `load-roles`, which parses `<project-root>/.swarmforge/roles.tsv`. Each line defines an agent’s **role name**, **work-tree path**, **tmux session name**, display settings, agent type, and receive mode. The daemon stores this data in a map keyed by role name for O(1) lookups during delivery.

According to the source code at lines 44–56, the roles file uses a tab-separated format that maps directly to the internal configuration structure used throughout the delivery pipeline.

## Polling Outboxes for New Hand-offs

Every second (controlled by the `poll-ms` variable), the daemon executes `poll-once!`. This function iterates over every configured role and gathers **outbox files** using `outbox-files`, which searches for `*.handoff` files created when a sender runs [`swarm_handoff.sh`](https://github.com/unclebob/swarm-forge/blob/main/swarm_handoff.sh).

The polling loop (lines 46–53) maintains a lightweight watch on the filesystem, ensuring the daemon detects new hand-offs without blocking or consuming excessive resources.

## Parsing Hand-off Headers and Body

When the daemon discovers a hand-off file, `parse-message` reads the content and splits it into a **header block** and an optional **body**. Header lines are parsed into a key-value map, while everything after the first blank line is treated as the message payload.

As implemented in lines 58–68, the parser validates that required fields like `to` are present, throwing an exception if the header structure is malformed.

## Determining Recipients and Validating Routes

The daemon extracts the comma-separated list of recipients from the `to` header. For each recipient, it performs a lookup in the roles map loaded at startup. If a recipient role is unknown, the daemon immediately throws an exception and routes the message to the failure handler.

This validation step ensures that misaddressed messages fail fast rather than lingering in dead-letter queues.

## Delivering to Inboxes with Metadata Enrichment

Before writing to the recipient’s inbox, the daemon calls `add-delivery-headers` (lines 85–89) to inject two metadata fields:
- **`recipient`** – the specific role name receiving this copy
- **`enqueued_at`** – the current ISO-8601 timestamp

The `target-path` function constructs the absolute destination: `<worktree-path>/.swarmforge/handoffs/inbox/new/<filename>`. If the file does not already exist, `render-message` writes the fully formatted message—headers first, followed by a blank line, then the body—to the recipient’s **new** inbox directory (lines 70–84).

## Notifying Agents via tmux

After successfully writing the file, the daemon invokes `notify!` to wake the receiving agent. This function sends three tmux commands to the recipient’s session socket:

```bash
tmux -S <socket> send-keys -t <session> -l "You have new handoff mail. If idle, run ready_for_next.sh."
tmux -S <socket> send-keys -t <session> C-m
tmux -S <socket> send-keys -t <session> C-j

```

As shown in lines 94–105, this sequence displays a wake-up notice in the tmux pane and triggers the agent’s [`ready_for_next.sh`](https://github.com/unclebob/swarm-forge/blob/main/ready_for_next.sh) script to process the new work.

## Archiving and Error Handling

Once all recipients receive their copies, the daemon moves the original outbox file to the sender’s **sent** directory using `move-with-collision`. If a filename collision occurs, the function automatically appends a timestamp to prevent overwrites.

Any delivery failure—whether from missing headers, unknown recipients, or write errors—is caught and logged. The `fail!` function moves the problematic file to a **failed** folder and writes a companion `.error` file describing the cause, ensuring administrators can diagnose issues without losing message context.

## Summary

- The **handoff daemon** (`handoffd.bb`) loads agent configurations from `.swarmforge/roles.tsv` at startup to build a routing map.
- It polls outboxes every second for `*.handoff` files created by [`swarm_handoff.sh`](https://github.com/unclebob/swarm-forge/blob/main/swarm_handoff.sh).
- Messages are parsed into headers and bodies, with the `to` header determining routing.
- Delivery includes metadata injection (`recipient`, `enqueued_at`) and atomic writes to `inbox/new/`.
- **tmux notifications** wake receiving agents immediately after file delivery.
- Successful messages are archived to the sender’s **sent** folder; failures are quarantined in **failed** with error logs.

## Frequently Asked Questions

### What file format does SwarmForge use for inter-agent messages?

SwarmForge uses plain-text `*.handoff` files with a header block followed by an optional body. Headers are key-value pairs (e.g., `to: worker-1,worker-2`, `type: git_handoff`), separated from the body by a blank line. This format is specified in [`swarmforge/handoff-protocol.md`](https://github.com/unclebob/swarm-forge/blob/main/swarmforge/handoff-protocol.md) and parsed by the `parse-message` function in the daemon.

### How does the daemon handle messages addressed to unknown recipients?

If the `to` header contains a role name not present in the roles map, the daemon throws an exception during the routing phase. The `fail!` mechanism then moves the hand-off file to the sender’s **failed** directory with a descriptive `.error` file, preventing silent message loss.

### What happens if the tmux notification fails but the file was written?

The daemon considers delivery complete once the file is written to the inbox and the `notify!` function executes. If tmux commands fail (e.g., session not found), the error is logged, but the message remains in the recipient’s inbox. The recipient will still process the message when [`ready_for_next.sh`](https://github.com/unclebob/swarm-forge/blob/main/ready_for_next.sh) runs during its next idle check.

### Can multiple agents receive the same hand-off message?

Yes. The `to` header accepts comma-separated role names. The daemon iterates over each recipient, delivering an independent copy to every inbox listed. Each copy receives unique `recipient` and `enqueued_at` headers, enabling agents to identify which copy belongs to them.