Help us shape the future of Go! Take the 2026 Go Developer Survey

courier

package
v0.2.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package courier delivers agents-board commands into the recipient agent's own session — beside its keyboard, never through it (design 050, polling-stream).

A command (msg.BoardKindCmd) is a board post addressed to one pane. The courier hears it on the board, works out how that pane's agent takes messages from outside, and hands it over:

  • claude-inbox: one JSON line on the claude session's own inbox socket (Claude Code cross-session messaging). An idle claude starts a turn; a busy one reads it between tool calls.
  • codex-queue: `codex queue --thread <id> --message <text>`. The running codex TUI runs it at its next idle point.
  • rysh-native: a prompt on the pane's inbox subject, for a pane whose agent IS rysh (no foreground program).

THE ONE RULE: there is no fallback to typing into the pane. The pane's PTY belongs to the human, and the input-stream path that types into it erases whatever the human was writing (design 050 §1). A command the courier cannot deliver is reported on the board as undelivered, where the human sees it.

Index

Constants

View Source
const (
	// MetaClaudeInbox is a claude's inbox socket path, stamped by a SessionStart
	// hook fleetctl installs (`$CLAUDE_CODE_MESSAGING_SOCKET`). Preferred over
	// guessing from the pid because claude can bind elsewhere on a collision.
	MetaClaudeInbox = "claude.inbox"
	// MetaCodexSession is the codex thread id the daemon discovers (design 029).
	MetaCodexSession = "codex.session_id"
	// MetaFleetComm is the fleet's comm mode (design 050): "input-stream" panes
	// are typed into by ANSA and never receive courier deliveries.
	MetaFleetComm = "fleet.comm"
	// MetaFleetAgent is what fleetctl launched in the pane: claude|codex|rysh.
	MetaFleetAgent = "fleet.agent"
)

Pane meta keys the courier reads.

View Source
const (
	RouteClaudeInbox = "claude-inbox"
	RouteCodexQueue  = "codex-queue"
	RouteRyshNative  = "rysh-native"
)

Route names.

View Source
const (
	CommQueue       = "queue"
	CommGrid        = "grid"
	CommPolling     = "polling-stream" // = grid
	CommInputStream = "input-stream"
)

Fleet comm modes (design 050/051), as stamped in pane meta fleet.comm.

  • queue: the default since design 051. No central push: every agent runs its OWN listener (`rysh board listen`) that watches the board and takes the prompts addressed to it. The courier stays out of these panes.
  • grid: point-to-point. The daemon's courier writes each prompt straight into the recipient's inbox. "polling-stream" is its pre-051 name.
  • input-stream: ANSA types into the pane. Neither courier nor listener.
View Source
const Identity = "courier"

Persona/PaneID the courier posts under. Not a pane uuid on purpose: the board store files a post whose PaneID does not own the thread as a reply, so the notice lands under the command, and no agent's inbox counts it as an answer.

View Source
const UndeliveredKind = "undelivered"

UndeliveredKind is the kind of the board post the courier files on a command's thread when it gives up.

Variables

View Source
var DefaultBackoff = []time.Duration{time.Second, 2 * time.Second, 5 * time.Second, 15 * time.Second, 60 * time.Second}

DefaultBackoff is design 050 §5.2's schedule.

View Source
var ErrPermanent = errors.New("permanent")

ErrPermanent wraps a planning failure that waiting will not fix.

View Source
var ListenerBackoff = []time.Duration{
	time.Second, 2 * time.Second, 5 * time.Second, 15 * time.Second,
	30 * time.Second, time.Minute, 2 * time.Minute, 5 * time.Minute,
}

ListenerBackoff is longer than the courier's: a queue listener's agent may simply be restarting, and the prompt is safe on the board meanwhile. After it runs out the listener files `undelivered`; a restarted listener still catches the prompt up, because it remains open on the board.

Functions

func CatchUp

func CatchUp(r *board.QueryReply, me, boardID string, delivered map[string]bool) []*msg.MsgBoardPost

CatchUp returns the prompts still OPEN for me on a board — never acked or answered by me, and not already delivered by an earlier listener — oldest first, so a listener that starts late hands over the backlog in order.

func CodexBin

func CodexBin() (string, error)

CodexBin is the codex executable: $RYSH_CODEX_BIN, else `codex` on PATH.

func Compose

func Compose(boardID string, p *msg.MsgBoardPost, bin, session string) string

Compose is the text the agent receives: the command, who sent it on which board, and how to answer. The answer instruction is what turns a delivered message into a closed loop — the sender learns it was done from the ack.

The ack instruction is COMPLETE — binary, session, board and identity are all spelled out — because the agent's own shell cannot be trusted to know them. Found live (design 050 P2): codex runs commands in an environment that does not carry $RYSH_PANE/$RYSH_SESSION, and spent a minute rediscovering who and where it was before it could ack. bin is the daemon's own absolute executable: a bare "rysh" resolves through the agent's PATH, which may hold an older build with no `board` verb.

func DeliveredKey

func DeliveredKey(boardID string, p *msg.MsgBoardPost) string

DeliveredKey identifies one prompt on one board: (board, sender, cmd id).

func Dispatch

func Dispatch(ctx context.Context, pub *msg.NATSPublisher, r Route, t Target, text string) error

Dispatch performs one delivery over a planned route. rysh-native is the only route that touches a pane subject, and it is the pane's prompt INBOX — the same non-PTY door fleetctl's native kickoff uses — never rawinput.

func IsSocket

func IsSocket(path string) bool

IsSocket reports whether path is a live-looking unix socket.

func ListenerFor

func ListenerFor(ev board.Event, me, boardID string) bool

ListenerFor reports whether a board event is a prompt for this listener's agent on this listener's board. Everything else — status posts, prompts for other agents, other boards — is somebody else's, and passes.

func LockPane

func LockPane(path string) (f *os.File, ok bool, err error)

LockPane takes the per-pane listener lock, so one agent has exactly one listener. ok=false means another listener holds it. The returned file must stay open for as long as the lock is wanted; the OS drops it on exit.

func SendClaudeInbox

func SendClaudeInbox(ctx context.Context, socket, text string) error

SendClaudeInbox writes one user message to a claude session's inbox socket.

The wire format is Claude Code's own (cross-session messaging): one JSON line, {"type":"user","message":{"role":"user","content":"..."}}. The optional {"type":"auth"} line is omitted — it is optional on macOS and Linux, and its token is only exported to the session's own children, which the daemon is not. Fleet panes run with crossSessionInbound=accept, so the message is delivered rather than held behind an approval dialog in the composer.

Claude closes a connection that sends no complete line within 30s, so the message is fully built before dialling, as its docs ask.

func SendCodexQueue

func SendCodexQueue(ctx context.Context, bin, thread, text string) error

SendCodexQueue queues a user turn on a running codex thread: `codex queue --thread <id> --message <text>`. The interactive TUI attached to that thread runs it at its next idle point. Success is codex's own receipt ("Queued message … for thread …"), not merely exit 0.

func SocketDirs

func SocketDirs() []string

SocketDirs are where Claude Code binds inbox sockets, in the order it tries them: the shared dir, then the per-user fallback it uses when it cannot accept the shared one.

Types

type Courier

type Courier struct {
	// contains filtered or unexported fields
}

Courier holds one FIFO queue per recipient: commands to one agent are delivered in the order they were posted, one at a time, so a burst cannot trip the recipient's inbox limits and a slow delivery to one agent never delays another.

func New

func New(d Deps) *Courier

New builds a courier. Close stops it.

func (*Courier) Close

func (c *Courier) Close()

Close stops every queue and waits for in-flight deliveries to return.

func (*Courier) Drain

func (c *Courier) Drain(timeout time.Duration) bool

Drain waits until every command taken so far has finished — delivered, reported undelivered or skipped — or until timeout. It does not stop the courier; Close does.

func (*Courier) Handle

func (c *Courier) Handle(ev board.Event)

Handle takes one board event. Everything but a well-formed command is ignored: status posts are never work.

type DeliveredLog

type DeliveredLog struct {
	// contains filtered or unexported fields
}

DeliveredLog is the listener's record of what it has handed over, one key per line, append-only. It is per agent pane, in the listener's state dir.

func NewDeliveredLog

func NewDeliveredLog(path string) *DeliveredLog

func (*DeliveredLog) Load

func (d *DeliveredLog) Load() map[string]bool

Load reads every recorded key. A missing file is an empty record.

func (*DeliveredLog) Record

func (d *DeliveredLog) Record(key string)

Record appends one key. Best effort: a failure costs a possible duplicate after a restart, never a lost prompt.

type Deps

type Deps struct {
	// Snapshot reads the recipient pane now.
	Snapshot func(paneID string) (Target, error)
	// Deliver performs one delivery over a planned route.
	Deliver func(ctx context.Context, r Route, t Target, text string) error
	// Notify publishes a courier post (undelivered) on a board.
	Notify func(boardID string, p *msg.MsgBoardPost)
	// IsSocket defaults to IsSocket.
	IsSocket func(string) bool
	// Bin and Session are named in the answer instruction (Compose).
	Bin     string
	Session string
	// AsListener marks a per-agent queue listener (design 051) rather than the
	// daemon's central courier. The courier skips queue panes, because their
	// own listener takes their prompts; a listener delivers to them.
	AsListener bool
	// Identity is who files undelivered notices; defaults to Identity.
	Identity string
	// Delivered, when set, is told about every successful delivery — the
	// listener records it so a restart does not deliver the same order twice.
	Delivered func(boardID string, p *msg.MsgBoardPost, r Route)
	// Backoff is the wait before each retry; len(Backoff)+1 attempts in all.
	Backoff []time.Duration
	// Log defaults to slog.Default().
	Log *slog.Logger
}

Deps is everything the courier needs from the daemon. Injected so the queue, retry and refusal logic is testable without a daemon, a socket or codex.

type Route

type Route struct {
	Adapter string
	Address string // socket path | codex thread id | pane id
}

Route is a planned delivery: which adapter, and its address.

func Plan

func Plan(t Target, isSocket func(string) bool) (Route, error)

Plan picks the route for a target. Pure apart from isSocket, which tests replace. The order is evidence first: an explicit stamp beats a pid guess, and a program name is never trusted on its own (claude's executable is named after its version, e.g. "2.1.296").

type Target

type Target struct {
	PaneID  string
	Program string // foreground executable, "" at the shell prompt
	PGID    int    // foreground process group, 0 when Program is ""
	Meta    map[string]string
}

Target is what the courier knows about a recipient pane at delivery time.

func SnapshotVia

func SnapshotVia(pub *msg.NATSPublisher, paneID string) (Target, error)

SnapshotVia reads a pane's foreground program, pgid and meta over the bus — the one look the courier and a listener both take before every delivery.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL