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 pathtmux-session: Target session for notificationshandoff-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! [] …)
- Reload roles via
load-roles—enabling dynamic reconfiguration without restart - Read tmux socket path for notification delivery
- Collect outbox files across all roles using
outbox-files - 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:
- Parse headers via
parse-messageto extract routing metadata - Evaluate hold criteria via
should-hold? - Route to hold or delivery (
hold!ordeliver!)
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
masterrole (specifier/approver pattern) - Single recipient (not broadcast)
- Lacks an
approvedheader
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-pathresolves to.swarmforge/handoffs/inbox/new/ - Add delivery metadata:
add-delivery-headerstimestamps 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:
- Removes the PID file
- Writes final log entry
- 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.tsvenables hot reconfiguration - Approval workflow support via hold logic for
git_handofffrom 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →