How the SwarmForge Handoff Daemon Delivers Messages Between Agents

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.

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:

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

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 →