# How the SwarmForge Handoff Daemon Facilitates Inter-Agent Communication

> Discover how the SwarmForge handoff daemon streamlines inter-agent communication through asynchronous polling, structured file parsing, and automated agent notifications.

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

---

**The SwarmForge handoff daemon enables asynchronous inter-agent communication by polling role-specific outboxes, parsing structured handoff files, delivering them to recipient inboxes, and triggering tmux notifications to wake processing agents.**

The SwarmForge handoff daemon serves as the central message bus in unclebob/swarm-forge, allowing independent AI agents to exchange tasks without a central server. Written in Babashka, this lightweight daemon transforms the filesystem into a reliable, observable message queue that coordinates multi-agent workflows across separate tmux sessions and work-trees.

## Architecture and Core Components

The daemon implements a **file-based message bus** where each agent operates within its own work-tree, communicating through well-defined directory structures rather than network sockets or shared memory.

### Role Definitions and Directory Structure

The system initializes by loading role configurations from `.swarmforge/roles.tsv`, which maps each role to its work-tree path, tmux session name, and receive mode. The `load-roles` function in `swarmforge/scripts/handoffd.bb` (lines 44-56) parses this configuration to establish the communication topology.

Each role maintains three critical directories under `<role-worktree>/.swarmforge/handoffs/`:

- **Outbox** – Where agents write new `.handoff` files destined for other roles
- **Inbox/new** – Where the daemon deposits incoming messages for processing
- **Sent** – Archive location for successfully transmitted handoffs

The `outbox-files` function (lines 46-53) enumerates candidate files using the pattern `*.handoff`, filtering for regular files ready for delivery.

### The Polling Loop

At its core, the daemon runs `poll-once!` (lines 64-73), which executes every `poll-ms` (default 1000ms). This loop:

1. Refreshes the role configuration via `load-roles`
2. Scans each role's outbox for pending files
3. Invokes `deliver!` (lines 23-45) to process each handoff

## Message Flow and Delivery Process

When the SwarmForge handoff daemon discovers a handoff file, it executes a strict delivery pipeline:

**Parse and validate** – The `parse-message` function (lines 58-68) extracts header fields and body content from the handoff file. Required headers include `to` (comma-separated recipient list), `type`, and `priority`.

**Recipient resolution** – The daemon splits the `to` header and determines the target path for each recipient using `target-path` (lines 90-92), which constructs the destination as `.swarmforge/handoffs/inbox/new/<filename>`.

**Metadata enrichment** – `add-delivery-headers` (lines 85-89) injects delivery metadata including `recipient` and `enqueued_at` timestamps, creating an audit trail for each hop.

**Atomic delivery** – The daemon writes the enriched message using `render-message` to the recipient's inbox, ensuring filesystem atomicity for reliable message queuing.

**Archival** – Upon successful delivery, the original file moves to the sender's `sent` directory via `log!`, maintaining a complete transmission history.

## Notification and Wake-Up Mechanism

After depositing a handoff into the recipient's inbox, the daemon must signal the idle agent to begin processing. The `notify!` function (lines 94-101) accomplishes this through tmux interprocess communication.

The daemon sends a targeted message to the recipient's tmux session:

```bash
tmux display-message -t <session-name> "You have new handoff mail..."

```

This notification triggers the recipient's [`ready_for_next.sh`](https://github.com/unclebob/swarm-forge/blob/main/ready_for_next.sh) script, which polls the inbox and begins task execution. This design eliminates the need for persistent network connections or complex service discovery while ensuring immediate task processing for active sessions.

## Error Handling and Reliability

When `parse-message` encounters malformed handoffs—such as missing `to` headers or invalid syntax—the daemon invokes `fail!` (lines 17-22) rather than dropping the message. This function:

- Moves the problematic file to the `failed` directory
- Generates an accompanying `.error` file containing the exception details
- Continues processing remaining messages without daemon termination

This fail-fast approach with persistent error logging allows operators to inspect, repair, and replay failed handoffs without losing task context.

## Practical Usage Examples

### Creating and Sending a Handoff

Agents generate handoffs using the `handoff_lib.bb` utility library:

```clojure
(require '[handoff-lib :as h])

(let [filename (str (h/id-timestamp) ".handoff")
      path (str (h/state-dir) "/outbox/" filename)]
  (spit path (str "type: git_handoff\n"
                  "to: builder\n"
                  "priority: 20\n"
                  "task: compile-project\n"
                  "commit: abcdef1234567890\n")))

```

The daemon automatically detects this file and delivers it to the `builder` role's inbox.

### Running the Daemon

Start the handoff daemon from the project root:

```bash
bb swarmforge/scripts/handoffd.bb .

```

The process runs in the foreground, logging activity to `.swarmforge/daemon/handoffffd.log`. Stop it gracefully using:

```bash
bb swarmforge/scripts/stop_handoff_daemon.bb .

```

### Inspecting Handoff Headers

Use the library CLI to extract specific fields without parsing the file manually:

```bash
bb swarmforge/scripts/handoff_lib.bb header-field mytask.handoff to

# => builder

bb swarmforge/scripts/handoff_lib.bb set-header mytask.handoff status "in-progress"

```

## Summary

- The SwarmForge handoff daemon provides a **file-based message bus** that requires no central broker or network stack, using only filesystem operations and tmux sockets.
- **Delivery guarantees** are enforced through atomic file moves, validation in `parse-message`, and error isolation via the `fail!` mechanism that archives problematic messages rather than dropping them.
- **Agent coordination** happens through tmux notifications sent by `notify!`, waking recipient sessions to process new inbox entries immediately.
- **Operational visibility** comes from enriched headers (`enqueued_at`, `recipient`) and comprehensive logging to the daemon log and sent/failed directories.
- The **modular architecture** separates concerns between the daemon (`handoffd.bb`), utility library (`handoff_lib.bb`), and protocol definitions ([`handoff-protocol.md`](https://github.com/unclebob/swarm-forge/blob/main/handoff-protocol.md)).

## Frequently Asked Questions

### How does the SwarmForge handoff daemon detect new messages?

The daemon runs `poll-once!` in a continuous loop with a default interval of 1000ms (`poll-ms`), scanning each role's outbox directory using the `outbox-files` function. This polling-based approach eliminates the need for filesystem watchers or inotify dependencies, ensuring compatibility across different operating systems and filesystems.

### What happens when a handoff file fails validation?

When `parse-message` encounters invalid syntax or missing required headers like `to`, the daemon calls `fail!` to move the file to the `failed` directory and creates an accompanying `.error` file containing the specific validation failure. This preserves the original message content while preventing the daemon from crashing or retrying indefinitely.

### How do agents receive notifications about new tasks?

After successfully writing a handoff to the recipient's inbox, the daemon executes `notify!` to send a tmux display-message to the recipient's specific session name (as defined in `.swarmforge/roles.tsv`). This message prompts the agent to execute its [`ready_for_next.sh`](https://github.com/unclebob/swarm-forge/blob/main/ready_for_next.sh) script, which checks the inbox for new work.

### Can the daemon handle multiple recipients for a single handoff?

Yes. The `to` header accepts comma-separated role names, and the delivery loop in `deliver!` iterates through each recipient, calling `target-path` and `render-message` for every destination. Each recipient receives an independent copy with specific `recipient` headers added by `add-delivery-headers`, allowing individual tracking of delivery status per role.