Courier Relays: Agent Communication via Local Sockets
Overview
This document describes an enhancement to Courier that adds relay support — local Unix socket channels that enable real-time bidirectional communication between a leader Claude session and worker agent sessions.
Relays extend Courier's existing send-message tool with a new transport. Instead of building a separate tool for agent coordination, Courier gains the ability to route messages to local agents the same way it routes messages to humans via Telegram. The leader doesn't think about transports — it just sends a message to a recipient.
Messages on relays are ephemeral — short, human-readable text meant to signal state changes and prompt action. Durable context (task details, progress logs, design decisions) belongs in folio notes, not relay messages.
Problem
When a leader session spawns worker agents (via tmux), there's no reactive communication channel between them. The leader has to poll files to check on progress. Workers have no way to signal the leader when they're blocked or done. This gap was identified when the Ops session had to be manually prompted to check on worker state.
Design
Concepts
Relay: A named Unix socket that connects exactly two Courier instances — a listener and a connector. Messages pass through in real-time with no buffering or persistence.
Leader: The Courier instance that creates and listens on relays. This is typically the Ops session or any session that spawns workers.
Worker: A Courier instance launched with --relay <name>, which connects to an existing relay socket. The worker sees the relay as a message recipient.
Message Flow
Leader Session Worker Session
┌──────────────┐ ┌──────────────┐
│ Claude │ │ Claude │
│ │ │ │
│ send-message│ │ send-message│
│ to: "agent" │ │ to: "leader"│
│ │ │ │
│ channel │ │ channel │
│ notify! ◄───┼────────────────────────┼──── notify! │
└──────┬───────┘ └──────┬───────┘
│ stdio │ stdio
┌──────┴───────┐ Unix socket ┌──────┴───────┐
│ Courier │◄══════════════════════►│ Courier │
│ (leader) │ ~/.courier/relays/ │ (worker) │
│ │ agent-name.sock │ │
│ Telegram ◄──┤ │ no Telegram │
│ Relays │ │ 1 relay │
└──────────────┘ └──────────────┘Unified send-message
The existing send-message tool gains relay awareness. Courier maintains a recipient registry:
- Telegram recipients: identified by chat_id (existing behavior)
- Relay recipients: identified by relay name
The tool's to: parameter accepts either:
send-message to: "331005009" text: "..." → Telegram (chat ID) send-message to: "sigil-agent" text: "..." → relay socket send-message to: "leader" text: "..." → relay (worker mode)
When a message arrives on a relay (from either direction), it's delivered as a channel notification to the Claude session, just like Telegram messages:
<channel source="courier" sender="sigil-agent" type="relay">
Done with step 2, see folio notes for details
</channel>Leader Usage
The leader's Courier exposes a create-relay tool:
create-relay name: "sigil-agent"
This creates a Unix socket at ~/.courier/relays/sigil-agent.sock and starts listening for a connection. The relay name becomes a valid recipient for send-message.
The leader can create multiple relays — one per worker agent it spawns.
A list-relays tool shows active relays and connection status:
list-relays → sigil-agent: connected (uptime 2h) sc-agent: waiting for connection
A close-relay tool tears down a relay when the worker is done:
close-relay name: "sigil-agent"
Worker Usage
A worker's Courier is launched with:
courier --relay sigil-agent
This connects to ~/.courier/relays/sigil-agent.sock. The worker's Courier:
- Registers "leader" as a recipient (the other end of the relay)
- Does NOT start Telegram polling (no token needed)
- Exposes the same
send-messagetool, but with "leader" as the only non-local recipient
The worker sends messages to the leader:
send-message to: "leader" text: "Blocked on test failures, need guidance"
The leader receives this as a channel notification immediately.
Socket Protocol
Messages on the socket are newline-delimited JSON:
{"sender": "sigil-agent", "text": "Step 2 complete, see folio"}
{"sender": "leader", "text": "Move on to step 3"}Simple, no framing complexity. Each side reads lines and delivers them as channel notifications to their Claude session.
Socket Path Convention
All relay sockets live in ~/.courier/relays/. The name maps directly to the filename:
~/.courier/relays/sigil-agent.sock ~/.courier/relays/sc-agent.sock
Workers only need the relay name, not the full path.
Updated Courier Instructions
The instructions string sent to Claude in the MCP handshake is updated based on mode:
Leader mode (Telegram + relays):
Messages from Telegram arrive as channel notifications with type="telegram". Messages from worker agents arrive with type="relay" and the agent's name as the sender. Use send-message with a chat_id to reply via Telegram, or with a relay name to message a worker agent. Use create-relay to set up a communication channel before spawning a worker agent. Use list-relays to see active connections.
Worker mode (relay only):
You are a worker agent connected to a leader session via relay. Messages from the leader arrive as channel notifications with type="relay". Use send-message to: "leader" to communicate back to the leader session. Keep messages short — use folio notes for detailed context.
Configuration
Leader (existing courier.yaml, extended)
allowed-senders:
- "331005009"
relays:
socket-dir: ~/.courier/relaysWorker (CLI args only, no config file needed)
courier --relay sigil-agent --log logs/courier.log --log-level trace
No .env-courier needed for workers (no Telegram token). The relay name is all the config a worker needs.
Spawning Workers
The leader session handles spawning — it knows the project context, worktree needs, and MCP config. A typical flow:
- Leader calls
create-relay name: "sigil-agent" - Leader creates a tmux session with Claude, passing MCP config inline: ``
bash tmux new-session -d -s sigil-agent \ claude --mcp-config '{"mcpServers":{"courier":{"command":"courier","args":["--relay","sigil-agent","--log","logs/courier.log"]}}}' \ --project-dir ~/Projects/Code/sigil/sigil \ ...`Or by writing a.mcp.json` in the project directory. - Worker starts, courier connects to the relay socket
- Leader sees "sigil-agent: connected" in
list-relays - Leader sends initial task:
send-message to: "sigil-agent" text: "..."
The spawning logic can be systematized as a shared skill installed across repos — a skill that knows how to set up tmux sessions, configure MCP, and start Claude with the right flags and permissions.
Resilience
Worker crash/disconnect
When a worker's courier disconnects (process dies, tmux session killed):
- Leader's courier detects the closed socket
- Delivers a channel notification: ``
xml <channel source="courier" sender="sigil-agent" type="relay-disconnect"> Agent disconnected </channel>`` - Leader can decide whether to respawn or clean up
Leader restart
If the leader's courier restarts:
- Relay sockets are recreated by the leader
- Workers using
--relayretry connection with backoff - Short disruption, but recoverable
System restart
Relay sockets are ephemeral (Unix sockets disappear on reboot). After a restart:
- Leader recreates relays as needed
- Workers are re-spawned (using folio task notes with in-progress status to determine what needs to resume)
- Workers reconnect via
--relayand continue claude --continueresumes the previous conversation in the worker's project directory
Implementation Plan
Phase 1: Socket Infrastructure
- [ ] Unix socket creation/listening in courier
- [ ] Socket connection (client mode) via
--relayCLI arg - [ ] Newline-delimited JSON message protocol over socket
- [ ] Deliver incoming socket messages as channel notifications
- [ ]
~/.courier/relays/directory convention
Phase 2: Leader Tools
- [ ]
create-relaytool: create socket, start listening - [ ]
close-relaytool: tear down socket, notify if connected - [ ]
list-relaystool: show active relays and connection status - [ ] Update
send-messageto route by recipient type (chat_id vs relay name) - [ ] Disconnect detection and notification
Phase 3: Worker Mode
- [ ]
--relay <name>CLI flag: connect to existing socket - [ ] Register "leader" as default recipient
- [ ] Skip Telegram setup when in worker mode
- [ ] Updated instructions string for worker context
- [ ] Connection retry with backoff
Phase 4: Resilience
- [ ] Reconnection on socket drop (worker side)
- [ ] Relay recreation on leader restart
- [ ] Integration with folio task notes for resumption after system restart
Dependencies
No new dependencies. Courier already has everything it needs:
- sigil-async: goroutines for socket I/O alongside stdin polling
- sigil-json: message serialization
- sigil-log: structured logging
Unix socket support may need a small addition to sigil-stdlib or sigil-lib if not already exposed.
What This Replaces
This design replaces the earlier Foreman proposal. The key insight: Foreman kept narrowing down to "just message passing between leader and workers" — which is exactly what Courier already does, just with a different transport.
Agent orchestration (deciding what to spawn, managing worktrees, tracking tasks) stays in the leader session's instructions and skills. Courier provides the communication fabric. Folio provides shared knowledge.
Open Questions
- Multiple relay connections: Should a relay support more than two endpoints? Current design is 1:1 (leader <> worker). Broadcast would require a different topology.
- Shared skill for spawning: The pattern of "create relay, set up tmux, write MCP config, start Claude" could be a shared skill installed across repos. Design TBD.
- Unix socket API in Sigil: Need to verify that sigil-lib exposes Unix domain socket creation/connection, or add it.