How handoffd.bb Serializes tmux Access and Avoids Race Conditions in Swarm-Forge

The handoffd.bb daemon prevents tmux race conditions through single-process ownership enforced by a PID file, sequential outbox processing, atomic file moves, and isolated notify! wrapper functions that serialize all tmux interactions.

Swarm-Forge is a Clojure-based tool for managing distributed development sessions, and handoffd.bb serves as its dedicated message delivery system. According to the unclebob/swarm-forge source code, this Babashka daemon achieves thread-safe tmux access through a deliberate architectural design that eliminates concurrent operations at multiple levels.

Single-Process Exclusivity via PID File Locking

The foundation of race condition prevention starts at process initialization. When handoffd.bb launches via (run-daemon!), it immediately claims exclusive ownership:

;; From handoffd.bb – startup sequence
;; Writes .swarmforge/daemon/handoffd.pid

This PID file serves as a system-wide mutex. Subsequent attempts to start the daemon detect the existing process and exit, ensuring only one instance can ever be alive. Since handoffd.bb is the sole component that communicates with tmux, this guarantees no two processes issue tmux commands concurrently.

The daemon also creates a stop flag file and monitors both this file and a stopping-flag atom for graceful shutdown. On termination, it removes its PID file, preventing stale locks from blocking restarts.

Sequential Outbox Processing in poll-once!

Inside poll-once!, the daemon loads all configured roles and builds a deduplicated list of outbox file paths. It then processes these with a single-threaded doseq:

;; Main loop – processes each outbox file in strict order
(doseq [path paths
        :while (not (should-stop?))]
  (process-outbox-file! roles socket path))

Because the daemon is inherently single-threaded, each handoff file completes full delivery before the next iteration begins. There is no parallelism here—no futures, no agents, no core.async channels that could interleave tmux operations.

Atomic Delivery Before tmux Notification

The deliver! function implements a strict two-phase protocol:

  1. File placement – copies the handoff file into each recipient's inbox
  2. Notification – calls notify! (the only tmux interaction)
  3. Cleanup – moves the source file to the sender's sent directory

This ordering ensures tmux commands execute only after filesystem operations succeed. The move to sent-dir uses move-with-collision, which appends a timestamp if the target exists, eliminating filename collisions between concurrent deliveries.

Isolated notify! Wrapper for tmux Commands

All tmux access flows through the dedicated notify! function:

(defn notify! [socket session]
  (let [send-text (sh "tmux" "-S" socket "send-keys" "-t" session "-l" wake-message)
        _ (Thread/sleep 150)
        send-cr   (sh "tmux" "-S" socket "send-keys" "-t" session "C-m")
        _ (Thread/sleep 50)
        send-lf   (sh "tmux" "-S" socket "send-keys" "-t" session "C-j")]
    (when-not (zero? (:exit send-text))
      (throw (ex-info "tmux send text failed" send-text)))
    ;; ... additional error handling ...
    ))

Key characteristics of this serialization strategy:

  • Socket-specific – uses the dedicated .swarmforge/tmux-socket path
  • Sequential commands – three send-keys invocations with deliberate 150ms and 50ms pauses between them
  • Process-isolated – no other code path in the codebase calls sh with tmux

Because notify! always executes within the single-threaded doseq loop, these tmux commands can never interleave with another daemon-initiated notification. The sleeps provide tmux session state stability between keystroke sequences.

Graceful Shutdown and State Consistency

The shutdown protocol protects against race conditions during termination:

  1. stop_handoff_daemon.bb creates the stop file
  2. The daemon detects this via should-stop? and sets stopping-flag
  3. Current notify! operations complete (no forced interruption)
  4. PID file removal signals clean exit to future startup attempts

This prevents the split-brain scenario where a new daemon starts while the old one still holds the tmux socket.

Key Source Files

File Role
swarmforge/scripts/handoffd.bb Core daemon; owns tmux socket, serializes deliveries
swarmforge/scripts/handoff_lib.bb Helper functions (parse-message, etc.)
swarmforge/scripts/swarmforge.bb Launcher that starts handoffd per-project
swarmforge/scripts/stop_handoff_daemon.bb Creates stop file, triggers graceful shutdown
test/swarmforge/handoff_test.clj Validates daemon serialization behavior

Summary

  • PID file locking enforces single-process ownership of tmux access
  • doseq iteration in poll-once! guarantees sequential outbox processing
  • Atomic file moves via move-with-collision prevent delivery conflicts
  • Isolated notify! function centralizes and sequences all tmux commands
  • Graceful shutdown protocol eliminates stale state between daemon restarts

Frequently Asked Questions

Why does handoffd.bb use sleeps between tmux commands?

The Thread/sleep calls (150ms and 50ms) provide tmux session state stability. According to the source code in handoffd.bb, these pauses ensure the wake-message text arrives completely before the carriage return (C-m) and line feed (C-j) execute, preventing partial key sequences during rapid successive notifications.

What happens if handoffd.bb crashes without removing its PID file?

The startup logic in run-daemon! checks for process existence using the stored PID. If the original process no longer exists, the new daemon claims ownership by overwriting the stale PID file. Active process detection prevents false positives from zombie entries.

Can multiple projects run handoffd.bb simultaneously?

Yes. Each project uses an isolated .swarmforge directory with project-specific socket paths and PID files. The serialization guarantees apply per-project; different projects' daemons do not interfere with each other.

Where is the tmux socket path configured?

The socket path defaults to .swarmforge/tmux-socket relative to the project root. This path passes through poll-once! and into notify! as the socket parameter, ensuring all tmux commands target the correct session endpoint.

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 →