Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Daemon protocol

The daemon listens on a Unix socket (see Command line for its path) readable only by the current user. Clients speak JSON-RPC 2.0, one JSON object per line, in both directions. The Rust types live in the volee-proto crate.

Try it by hand:

echo '{"jsonrpc":"2.0","id":1,"method":"daemon.status","params":{"protocol":1}}' \
  | nc -U ~/.local/state/volee/volee.sock

Methods

MethodParametersResult
daemon.statusprotocol (the client’s protocol version, or 0)version, protocol, pid, start time, last event, agent count, harnesses, forge, multiplexers, repos (the repositories the configuration names)
daemon.shutdownnonenone; the daemon stops after answering
events.sinceafter (sequence number, optional)events (sequence number and event), last_seq
events.subscribeafter (optional), interactive (optional, false by default)the same batch, then events.appended notifications
discovery.refreshtimeout_ms (optional: 3,000 by default, 10,000 at most)listed: whether a listing that started after the request was applied in time
agents.forgetagentnone
agents.joinname, harness, cwd, optional missionagent, rejoined
agents.activityagent, activitynone
agents.previewagentscreen: terminal output that draws the agent’s screen, with its escape sequences; exact: whether it is the screen itself, row for row, or the replay of a session’s output; size (cols, rows): the size of the terminal it was drawn in, when the daemon knows it
agents.diffagent, optional fulltext: a summary by file, or the full patch; truncated
agents.attachagentprogram, args, cwd, env_remove, hint: the command that opens the agent in the client’s terminal
agents.openagent, cols, rowssocket: a volee host to connect to, for a client that shows the agent inside its own screen; view: whether it is a view on a session that runs elsewhere
agents.stopagentnone
agents.adoptagentnone; the agent’s conversation continues in a volee host
agents.tidyagent, optional dry_runoutcome: removed or would-remove (worktree, branch), or kept (reasons)
messages.sendfrom, to, kind, body, optional refs, in_reply_to, thread, options (the answers a question to the human offers: two to six short lines)message, thread
messages.readmessage, bynone
hooks.ingestagent, event, payloadstdout: what the hook prints, if anything
hooks.waitagent, timeout_mstext: messages that wake the agent, or null
agents.contextagent, percentnothing: the share of its context window the agent’s conversation uses, from its status line (ADR 0035); an event context-used when it changed
agents.spawnrepo, branch, task, role, optional base, parent, mission, model, name, harness (claude-code by default, or opencode)agent, worktree
agents.leadcwd (an absolute path), optional task, name, modelagent, name: a lead, in a volee host, without a worktree
agents.runhost: the id of a volee host the client started, in the daemon’s hosts directory; optional nameagent, name; the program of the host is now an agent
threads.summarizethreadsummary, messages, model; refused unless [messages] summaries is on
missions.createtitle, kind (change or analysis), goal, repos, optional requested_bymission, thread, coordinator
missions.abortmission, optional reasonnone
missions.request_approvalmission, by (its coordinator), done, proof, leftquestion: the report, posted as a question to the human that offers the approving answer and no
missions.closemission, optional report, forcenone; the daemon then tidies the mission’s workers and stops its coordinator. The human’s own gesture: a request with by (an agent) is refused

Running a program

agents.run makes the program of a volee host an agent (ADR 0029). The client starts the host itself (volee host run, detached, under the daemon’s hosts directory), so that the program gets the client’s environment and directory, then gives the daemon the host’s id. The daemon reads the host’s record and registers an agent in that host, launched by the human, with the role independent, the name asked for (else the host’s own name, else the name of the program’s directory) and, as its harness, the name of the program. It refuses an id that names no file of its hosts directory (-32602), a host it does not find (-32001), and a program that already ended or a host that already is an agent (-32002); the client then ends the program it started.

The agent has no session, so nothing that needs one applies to it: its states come from its screen, no message is handed to it and nothing resumes it. agents.preview, agents.open, agents.attach and agents.stop act on its host, as for any agent in a host; once its program ended, agents.open and agents.attach are refused with words that say nothing resumes it, and the agent leaves the registry a minute later. A session or a process that a harness lists inside the host, the program itself or a process below it, is not registered a second time.

Tidying

agents.tidy follows ADR 0011: it removes nothing without delivery evidence and never deletes a branch. delivery::tidy_check decides from the registry: the agent was launched by volee, works in a worktree under <repository>/.claude/worktrees/, is not running, and its delivery evidence, checked since its last change, shows a clean tree and landed work (merged, merged pull request, or report). Otherwise the result is kept with every reason, each with a reason tag (busy, uncommitted-changes, not-delivered, not-launched-by-volee, not-a-volee-worktree, evidence-not-checked, evidence-stale, kept-by-harness, refused-by-git) and a sentence for people when displayed.

When the evidence allows it, the daemon asks the harness to delete the session (with the evidence), checks the verdict again, unlocks the worktree if the harness left it locked, and lets git remove it; git still refuses changes and commits that no branch keeps, and never forces. It then records the workspace without its worktree and forgets the agent; its events stay in the log. With dry_run, the answer is would-remove or kept, and nothing is touched.

Launching

agents.spawn creates the branch and its worktree with git, writes the agent’s hook settings and MCP configuration (readable only by you), records the agent, then starts the session in a new volee host (ADR 0019), from the repository’s main checkout with --worktree (ADR 0013). With harness: "opencode", it starts opencode --session <id> --prompt <task> in the worktree instead, for a lead or a worker; any other role there, and any other harness, is refused (-32602). A failed start marks the agent failed and keeps the worktree; the same request sent again reuses that worktree while nothing was done in it, and forgets the failed agent (ADR 0016). It refuses coordinators (missions launch them) and, for a mission, repositories the mission does not list. missions.create refuses (-32003) when the daemon has no usable Claude Code (missing or too old, as daemon.status reports), before it records anything. Otherwise it records the mission and launches the coordinator; when that launch fails, it tells the human in the mission’s thread and coordinator is null.

Joining

agents.join brings an agent of another harness into volee (ADR 0014); volee mcp --join sends it. The daemon finds the agent that joined with the same name, harness and cwd (an absolute path) and was not forgotten, or registers one (launched_by: outside, harness other with that name, role independent), records it idle, and answers whether it knew it (rejoined). With mission, which must be open, the agent is a member of that mission, and its coordinator is told; joining again with another mission, or without one, records a membership-changed event. The agent stays alive while the connection that joined stays open: when the last such connection closes, or when the daemon starts, it is stopped. Invalid names or harnesses answer -32602, a mission that is over -32002, and claude-code is refused (-32002): those sessions are found without joining.

agents.activity records a joined agent’s last action in one line (its MCP server sends one per tool call). It refuses agents that did not join.

Acting on agents

For an agent in a volee host (ADR 0019), agents.preview returns a snapshot of the host’s screen (exact, with the size of the host’s terminal: a snapshot redraws its screen in a terminal of that size only), agents.attach returns the volee host attach command for the client to run (Ctrl+Q comes back), and agents.stop asks the host to end the program. For a Claude Code background session that volee does not host, agents.attach returns the harness’s own command (claude attach <id>): nothing is stopped or resumed (ADR 0027). agents.adopt is another thing: it first stops the session, then resumes its conversation in a new host. It refuses an agent that already runs in a host and one that runs in a terminal of its own, which must be quit there first (ADR 0023).

agents.open is for a client that draws the agent inside its own screen, as the interface does. It answers with the socket of a volee host, which the client connects to with the host protocol: the host the agent runs in (view false), or a view (view true), a host the daemon starts on the session’s own door, at cols by rows: claude attach for a Claude Code background session, running or finished, and the multiplexer’s own command for a pane it can show alone (tuios pane-view). A view ends its program once no client has held its input for 15 seconds, and ends with the daemon; the session it shows is not touched. Without a socket, the agent runs in a pane only its multiplexer can show: agents.attach says how to bring it forward. It refuses (-32002) an agent in a terminal the daemon cannot reach, and says what makes it reachable.

For other agents, agents.preview, agents.attach and agents.stop act on the agent’s current harness session, as the last listing showed it; an agent volee just launched is reachable before any listing shows it. They refuse (-32002) an agent whose session runs in a terminal of its own, which only that terminal can show or control, and answer -32001 when the harness no longer knows the session. agents.stop also refuses agents volee did not launch (ADR 0011). What agents.preview returns for them is the replay of the session’s output, or the end of the setup log of an agent that never had a session: exact is false, and a client may repair it as a replay (ADR 0028).

agents.attach returns a command for the client to run in its own terminal, with the environment variables in env_remove removed; the harness keeps the agent running when the person comes back.

An agent whose session runs in a multiplexer pane the daemon found (ADR 0015) is not refused: agents.preview returns what the pane shows, each row placed with a cursor move (exact, without a size: it draws the same cells in any terminal large enough), and agents.attach returns an empty program and a focus object (multiplexer, session, steps, attach). The client runs each step, a program and its arguments, in order, in its own environment and without giving up its terminal, so that the multiplexer acts on the person’s own client. The first step focuses the pane in its session; when a later one fails, attach is the command that shows that session in a terminal (such as tuios attach work), for the client to suggest. daemon.status lists the multiplexers the daemon uses in multiplexers, with whether each answers now.

agents.diff compares the agent’s worktree (committed and uncommitted changes, and the files it created) with the merge base of the branch it started from. The text is at most 512 KiB, cut at a line break when longer, with truncated set.

Missions

missions.create opens the mission’s thread and records the mission in the planning status. From then on the daemon runs the supervisor after every batch of events that concerns the mission, and once when it starts. Its decisions are ordinary events (worker-assessed, mission-updated) and messages from volee in the mission’s thread, so every client sees them.

agents.spawn refuses (-32002) a branch of a repository where an agent already works, and missions.create a title under which a mission is already open: each refusal names the agent to write to instead (ADR 0033). When requested_by names an agent, it must be a lead, and the mission’s coordinator is registered with that lead as its parent. agents.lead refuses a lead under the name of one that runs in the same directory.

missions.request_approval is refused (-32002) unless by is the mission’s coordinator and the mission is reporting: every worker delivered, and no report waits for an answer. It posts the report as a question to the human and moves the mission to awaiting-approval. The human’s answer is an ordinary message in reply to that question (messages.send); the supervisor reads it and moves the mission to approved, or back to reporting.

No request merges: the daemon does, when a mission becomes approved (ADR 0032). It reads the workers’ evidence from git and the forge, merges the work that has not landed (a pull request on the forge, a branch into its base branch in the main checkout of a repository without a remote), reads the evidence again, and closes the mission once every merge shows in it. Otherwise it moves the mission back to reporting and posts an alert in the mission’s thread, to the human, the coordinator and the lead, that names each worker whose work was not merged, with the reason. A mission that is approved when the daemon starts is carried out then.

missions.close is the human closing a mission themselves, without a merge. It refuses (-32002) while a worker has not delivered, unless force is set, and it refuses a request made in an agent’s name (by): no agent closes a mission. Without report, the report the coordinator presented is kept, else its latest report in the mission’s thread. After a close, by the human or by the daemon, the daemon tidies the workers whose work landed, stops the coordinator, and posts one message in the mission’s thread that says what is merged, what it tidied and what it kept.

Events and replicas

Clients do not ask the daemon for “the list of agents”. They read events (events.since) and apply them to their own Registry from volee-core, exactly as the daemon does. A subscription (events.subscribe) answers with the backlog and then pushes every new event as a notification:

{"jsonrpc":"2.0","method":"events.appended","params":{"seq":42,"event":{"type":"state-changed","at":1793993747000,"agent":"ag_01jc5…","state":{"state":"working"},"detail":null}}}

Notifications arrive in log order without gaps. A subscriber that falls more than 4,096 events behind receives events.lagged (with the number of events it skipped) and is dropped; it can resubscribe with after set to the last sequence number it applied.

A client a person watches, such as the interface, subscribes with interactive set to true. While at least one interactive subscription is open, the daemon lists the harness’s sessions every two to ten seconds (two after a listing that changed something, longer while nothing changes) instead of every thirty, and a new one triggers a listing at once, so the client opens on fresh data (discovery). The subscription stops counting when it ends or its connection closes. Programs, such as an agent’s MCP server, leave it false: agents volee launched report through their hooks in real time anyway.

A one-off command that wants the sessions as they are now, such as volee ls, sends discovery.refresh before events.since. The daemon lists at once and answers when a listing that started after the request is applied, or when timeout_ms runs out; the listing then still completes in the background. Requests made while a listing runs share the next one. listed is false without a harness, after a failed listing, or after the timeout: the events then show the last listing, as before.

Rust clients get all of this from volee_proto::replica: subscribe returns the backlog and a feed of the events that follow, which drops repeats and picks up again after a lag on request (subscribe_interactive does the same for a client a person watches), and Replica keeps a Registry up to date from a feed. The MCP server reads a Replica; the interface follows an interactive feed, because it also needs the events themselves.

Errors

Standard JSON-RPC codes, plus volee’s own:

CodeMeaning
-32700the line is not JSON
-32600not a valid request
-32601unknown method
-32602invalid parameters
-32603internal error
-32001not found: agent, message, thread or mission
-32002refused by a rule (routing, cleanup, a mission that cannot close)
-32003a required program (harness, git) is unavailable
-32004protocol versions differ

Versioning

The protocol version is 1. Adding a method or an optional field does not change it; changing or removing one does, and daemon.status tells a client which version the daemon speaks.