How the SwarmForge Handoff Daemon Facilitates Inter-Agent Communication

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:

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

This notification triggers the recipient's 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:

(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:

bb swarmforge/scripts/handoffd.bb .

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

bb swarmforge/scripts/stop_handoff_daemon.bb .

Inspecting Handoff Headers

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

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).

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

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →