How SwarmForge's Handoff Daemon (handoffd) Manages Inter-Agent Communication

SwarmForge's Handoff Daemon (handoffd) orchestrates inter-agent communication through a lightweight, file-based message broker that scans outbox directories, routes handoff messages to recipient inboxes, synchronizes the shared task board, and triggers tmux notifications for real-time agent alerting.

Inter-agent communication is the backbone of any multi-agent system. In SwarmForge, unclebob's open-source framework for AI agent collaboration, this responsibility falls to handoffd—a Bash-compatible Clojure daemon implemented in swarmforge/scripts/handoffd.bb. Unlike complex message queues, handoffd uses a simple filesystem protocol that keeps agents loosely coupled while guaranteeing reliable delivery.


Architecture Overview: Filesystem-Based Message Routing

The daemon follows a polling model with two operational modes:

  • Continuous mode (default): Runs an infinite loop with 1-second sleep intervals between polls
  • One-off mode (--once): Executes a single poll cycle and exits—ideal for debugging or CI pipelines

Both modes converge on the poll-once! function, which implements the core routing logic.


Startup and Role Configuration

Daemon Initialization

When handoffd starts, the configure! function (lines 33-45 in handoffd.bb) establishes the runtime environment:

(defn configure! …)

This builds paths to critical directories and files:

  • State directory: .swarmforge/ (project configuration)
  • Daemon directory: .swarmforge/handoffd/ (PID file, stop signals, logs)
  • Roles file: swarmforge/.swarmforge/roles.tsv (agent definitions)
  • Tmux socket: For sending notifications to agent sessions

The daemon also records its operational mode—whether to run continuously or execute a single poll (once?).

Loading Agent Roles

Agent capabilities and routing information come from load-roles (lines 63-75):

(defn load-roles [] …)

Each line in roles.tsv parses into a map keyed by role name, containing:

  • work-tree: The agent's working directory path
  • tmux-session: Target session for notifications
  • handoff-type: How this role receives handoffs (default: "task")

This role map drives all subsequent routing decisions, from inbox/outbox path resolution to tmux session targeting.


The Main Polling Loop: poll-once!

The poll-once! function (lines 400-417) executes four sequential operations on every iteration:

(defn poll-once! [] …)
  1. Reload roles via load-roles—enabling dynamic reconfiguration without restart
  2. Read tmux socket path for notification delivery
  3. Collect outbox files across all roles using outbox-files
  4. Process each file through process-outbox-file!

This design ensures fresh state on every poll, accommodating role changes and filesystem events without complex locking.


Message Processing: Hold, Deliver, and Notify

File Processing Pipeline

process-outbox-file! (lines 92-99) handles each .handoff file:

(defn process-outbox-file! [roles socket path] …)

The pipeline has three stages:

  1. Parse headers via parse-message to extract routing metadata
  2. Evaluate hold criteria via should-hold?
  3. Route to hold or delivery (hold! or deliver!)

Hold Logic: Approval Workflows

The should-hold? function (lines 92-98) implements a specific governance rule:

(defn should-hold? [roles headers] …)

A handoff is held when all conditions match:

  • Type is git_handoff
  • Origin is the master role (specifier/approver pattern)
  • Single recipient (not broadcast)
  • Lacks an approved header

Held files move to handoffs/pending_approval/ until manually approved.

Delivery Logic: End-to-End Routing

The deliver! function (lines 50-73) executes four coordinated actions:

(defn deliver! [roles socket sender-role path] …)

1. Board Synchronization

update-board! rewrites the shared task board using pack-board!. The board operation depends on handoff type and recipients—common operations include move, done, and archive.

2. Per-Recipient Delivery

For each to header in the message:

  • Look up recipient role: (get roles recipient)
  • Copy handoff to inbox: target-path resolves to .swarmforge/handoffs/inbox/new/
  • Add delivery metadata: add-delivery-headers timestamps and traces the route
  • Trigger notification: notify!

3. Tmux Notification

The notify! function (lines 13-24) sends a wake-up signal:

(defn notify! [socket session] …)

This executes tmux send-keys with:

  • A configurable wake message: "You have new handoff mail …"
  • Carriage return and line-feed to ensure prompt visibility

4. Post-Delivery Housekeeping

  • Move original outbox file to sender's sent-dir
  • Archive sender's board entry via archive-sender!
  • Wake blocked senders if approval unblocked a workflow: maybe-notify-unblocked-sender!

Graceful Shutdown Mechanisms

The daemon monitors two stop signals through should-stop?:

  • External stop file: handoffd/stop (filesystem trigger)
  • Internal flag: stopping-flag (set by JVM shutdown hook)

When either signal is detected, poll-once! returns false, breaking the main loop. The daemon then:

  1. Removes the PID file
  2. Writes final log entry
  3. Exits cleanly

This dual-channel design allows both programmatic control (touching the stop file) and OS-level termination handling.


Handoff Protocol Format

All messages follow the header-body format defined in handoff-protocol.md:

id: 12345
from: developer
to: build
type: git_handoff
task_id: 42
priority: high

Fix broken test suite

Standard headers:

Header Purpose
id Unique message identifier
from Originating role
to Target recipient(s), space-separated for multicast
type Handoff category (git_handoff, task, etc.)
task_id Reference to board item
priority Scheduling hint
approved Presence indicates governance clearance

The daemon's parse-message and render-message functions preserve header ordering for well-known fields while permitting arbitrary extension headers.


Practical Usage Examples

Start the Daemon (Production)


# From project root

./swarmforge/scripts/handoffd.bb /path/to/project

Runs continuously, polling every 1000ms and routing handoffs as agents produce them.

Debug with Single Poll

./swarmforge/scripts/handoffd.bb --once /path/to/project

Useful for CI validation or troubleshooting routing logic without daemon persistence.

Manual Handoff Creation (Testing)

cat > .swarmforge/handoffs/outbox/myagent.myrole/12345.handoff <<EOF
id: 12345
from: developer
to: build
type: git_handoff
task_id: 42
priority: high

Fix broken test suite
EOF

The daemon will detect this file, move it to build's inbox, update the board, and notify the build tmux session.

Force Approval Hold

cat > .swarmforge/handoffs/outbox/master.specifier/99999.handoff <<EOF
id: 99999
from: master
to: deploy
type: git_handoff
task_id: 100

Production deployment request
EOF

# Note: no 'approved' header

This lands in pending_approval/ until edited to include approved: true.


Key Implementation Files

File Responsibility
swarmforge/scripts/handoffd.bb Core daemon: configuration, polling loop, delivery orchestration
swarmforge/scripts/handoff_lib.bb Shared utilities for handoff parsing and manipulation
swarmforge/handoff-protocol.md Formal specification of message format and semantics
swarmforge/scripts/pack_board.sh Board manipulation CLI invoked by update-board!
swarmforge/scripts/ready_for_next_task.sh Agent-side inbox processor for task initiation
swarmforge/constitution/articles/handoffs.prompt AI agent instructions for handoff creation

Summary

SwarmForge's inter-agent communication relies on handoffd's elegant filesystem-based architecture:

  • Continuous polling of role outboxes with 1-second granularity
  • Dynamic role loading from roles.tsv enables hot reconfiguration
  • Approval workflow support via hold logic for git_handoff from master
  • Atomic file operations guarantee delivery despite crashes
  • Tmux integration provides real-time agent notification without network dependencies
  • Dual shutdown channels support both graceful and forced termination

This design prioritizes simplicity, inspectability, and operational reliability over throughput—appropriate for human-in-the-loop agent workflows where visibility trumps raw performance.


Frequently Asked Questions

What triggers a handoff to be held for approval?

A handoff enters pending_approval/ when it is a git_handoff type originating from the master role, addressed to a single recipient, and missing an approved header. This three-condition check in should-hold? implements SwarmForge's specifier-approver governance pattern for sensitive operations like production deployments.

How does handoffd notify agents of new messages?

The daemon invokes notify! after successfully copying a handoff to the recipient's inbox. This function executes tmux send-keys against the recipient's configured tmux session, writing a wake message followed by newline characters. Agents see "You have new handoff mail …" appear at their prompt without requiring network listeners or socket connections.

Can handoffd run without tmux?

Yes, but notifications fail silently. The notify! function wraps tmux calls; if no session is configured or tmux is unavailable, delivery still completes (file copied, board updated) but the agent receives no real-time alert. The agent's next poll of their inbox—typically via ready_for_next_task.sh—will discover the message.

What happens if the daemon crashes during message delivery?

The atomic move operations in deliver! provide crash safety. A handoff is only removed from the outbox after successful inbox copy and board update. If interrupted, the outbox file remains and will be reprocessed on daemon restart. Duplicate delivery is prevented through idempotent file moves and board operations keyed by id headers.

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 →