# How the SwarmForge Handoff Daemon Delivers Messages Between Agents

> Discover how the SwarmForge handoff daemon uses file polling and tmux to deliver messages between agents. Learn about efficient agent communication in this technical guide.

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

---

**The SwarmForge handoff daemon uses a lightweight file-based polling mechanism to move hand-off files from sender outboxes to recipient inboxes and wakes tmux sessions to notify agents of pending work.**

The SwarmForge handoff daemon (`handoffd.bb`) implements an asynchronous, file-based message bus that enables agent communication without requiring network servers. This Clojure-based daemon continuously monitors outbox directories, parses hand-off files, routes them to specified recipients, and triggers agent activation through tmux notifications. Understanding how the handoff daemon deliver messages between agents reveals the core architecture of SwarmForge's distributed task coordination.

## Role Configuration and Initialization

At startup, the daemon loads agent definitions from `<project-root>/.swarmforge/roles.tsv` using the `load-roles` function. Each line in this tab-separated file defines a role, its work-tree path, tmux session name, display configuration, agent type, and receive mode. The data is stored in a map keyed by role name for O(1) lookups during delivery.

```clojure
;; Conceptual structure from handoffd.bb#L44-L56
(def roles (load-roles))  ;; Returns {"worker-1" {:worktree "..." :session "..."} ...}

```

This configuration enables the daemon to resolve physical file paths and tmux session identifiers dynamically without hardcoding agent locations.

## Polling and Outbox Detection

Every second (controlled by `poll-ms`), the daemon executes `poll-once!`, which iterates over every registered role and gathers pending messages using `outbox-files`. These files are regular `*.handoff` files created when a sender agent executes [`swarm_handoff.sh`](https://github.com/unclebob/swarm-forge/blob/main/swarm_handoff.sh).

```clojure
;; Simplified polling loop from handoffd.bb#L46-L53
(doseq [role (vals roles)]
  (let [files (outbox-files role)]
    (doseq [path files]
      (deliver! roles socket role path))))

```

The daemon scans each agent's outbox directory (`<worktree-path>/.swarmforge/handoffs/outbox/`) for new hand-off files, creating a queue of pending deliveries to process.

## Parsing and Routing Messages

Each outbox file is processed by `parse-message`, which splits the file into a header block and optional body. Header lines are converted into a key-value map, while the body content is preserved separately.

```text
type: git_handoff
to: worker-1,worker-2
priority: 50
task: build-docker-image
commit: a1b2c3d4

Optional body content here...

```

The daemon examines the `to` header—a comma-separated list of recipient role names—and fetches corresponding role information from the roles map. If a specified recipient does not exist in the configuration, the daemon throws an exception and routes the message to the failure handler.

## Adding Delivery Metadata

Before writing to recipient inboxes, the `add-delivery-headers` function injects provenance metadata:

- **`recipient`**: The target role name
- **`enqueued_at`**: Current ISO-8601 timestamp

This metadata, added at lines 85-89 of `handoffd.bb`, enables downstream agents to track message latency and routing history.

```clojure
;; From handoffd.bb#L85-L89
(defn add-delivery-headers [headers recipient]
  (-> headers
      (assoc "recipient" recipient)
      (assoc "enqueued_at" (iso-8601-now))))

```

## Writing to Recipient Inboxes

The `target-path` function constructs the absolute destination path:

```text
<worktree-path>/.swarmforge/handoffs/inbox/new/<filename>.handoff

```

The `render-message` function serializes the enriched headers followed by a blank line and the original body. The daemon performs an idempotency check—if the file already exists in the target inbox, it skips writing to prevent duplicates.

## Notifying Agents via Tmux

After successful file placement, the `notify!` function (lines 94-105) sends three tmux commands to the recipient's session:

```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

```

This sequence displays a wake-up message in the agent's tmux pane and simulates Enter keypresses (carriage return and line feed), ensuring the notification appears even if the terminal is idle. Agents typically respond by executing [`ready_for_next.sh`](https://github.com/unclebob/swarm-forge/blob/main/ready_for_next.sh) to process the new hand-off.

## Archiving and Error Handling

Once all recipients receive the message, the original outbox file moves to the sender's `sent` directory using `move-with-collision`. If a filename collision occurs, the daemon appends a timestamp to ensure uniqueness.

Error handling follows a strict fail-safe protocol:

- Missing `to` headers trigger immediate failure
- Unknown recipients raise exceptions
- Write failures are caught and logged

Any error moves the offending file to the `failed` directory with a companion `.error` file describing the cause, implemented in the `fail!` function.

## Summary

- **The handoff daemon** (`swarmforge/scripts/handoffd.bb`) provides asynchronous message delivery using filesystem operations rather than network sockets.
- **Configuration** loads from `.swarmforge/roles.tsv` to map logical agent names to physical worktrees and tmux sessions.
- **Delivery flow** moves files from `outbox` to `inbox/new`, adds metadata headers (`recipient`, `enqueued_at`), and triggers agents via tmux `send-keys` commands.
- **Reliability** is ensured through collision-safe archiving, idempotent writes, and comprehensive error handling with automatic failure directory routing.

## Frequently Asked Questions

### How does the handoff daemon handle multiple recipients for a single message?

The daemon splits the `to` header on commas and iterates through each recipient. For every valid recipient, it creates an independent copy of the message in that agent's inbox with recipient-specific metadata. Only after all recipients receive their copies does the daemon move the original file to the sender's `sent` directory.

### What happens if a recipient's tmux session is not running when a message arrives?

The tmux `send-keys` command will fail silently if the session does not exist, but the message remains safely in the recipient's inbox. When the agent starts and runs [`ready_for_next.sh`](https://github.com/unclebob/swarm-forge/blob/main/ready_for_next.sh), it will discover and process pending hand-off files regardless of whether the original notification succeeded.

### Can agents communicate across different machines using this daemon?

No. The handoff daemon operates on local filesystem paths and unix sockets for tmux communication. It is designed for single-machine coordination where all agents share a filesystem namespace, though the repository structure could theoretically be extended for networked filesystems.

### What distinguishes the "new" inbox directory from other inbox subdirectories?

The `inbox/new` directory holds messages that have not yet been acknowledged by the receiving agent. When [`ready_for_next.sh`](https://github.com/unclebob/swarm-forge/blob/main/ready_for_next.sh) processes a hand-off, it typically moves the file from `inbox/new` to `inbox/cur` (current) or `inbox/processing`, creating a simple state machine for message lifecycle management without requiring database transactions.