Daemon API
aivoryd is the only process that reads your agent sessions. The desktop app, the phone app and the aivoryd call command are all clients of one JSON-RPC 2.0 API. This page shows you how to connect, how to authenticate, and which methods and events exist.
- JSON-RPC 2.0, one JSON message for each line (newline-delimited JSON)
- Three transports: a local named pipe or Unix socket, a WebSocket on the LAN, and iroh (QUIC) outside the LAN
- One feed:
warroom/subscribereturns a snapshot, thenwarroom/deltanotifications - A JSON Schema describes every method and type: aivory-protocol.schema.json
Quick Start
Start a daemon with demo data and a private endpoint name. Then call it from a second terminal:
aivoryd serve --demo --endpoint docs-try
aivoryd call daemon/info --endpoint docs-try
aivoryd call <METHOD> --params '<JSON>' sends one request and prints every message that comes back. Stop the demo daemon with aivoryd call daemon/shutdown --endpoint docs-try.
Without --demo, aivoryd serve shows the real sessions of your machine (Claude Code, Codex CLI and Gemini CLI). See Daemon for what the daemon stores.
Transports
| Transport | Start | For | Frames |
|---|---|---|---|
| Local pipe or socket | aivoryd serve |
The desktop app, the CLI, local tools | One JSON message per line |
| WebSocket | aivoryd serve --remote <ADDR> |
A paired phone on the same network | One JSON message per text frame |
| iroh | aivoryd serve --iroh |
A paired phone anywhere, with hole punching and a relay | 4-byte length, then one JSON message |
You can run --remote and --iroh together, and with --demo.
Local Pipe or Socket
The endpoint name is 1 to 64 characters of [A-Za-z0-9._-]. The default name is default. Choose another name with --endpoint <NAME>.
| OS | Address |
|---|---|
| Windows | \\.\pipe\aivory-code-<name> |
| Linux and macOS | $XDG_RUNTIME_DIR/aivory/<name>.sock |
Linux and macOS without XDG_RUNTIME_DIR |
<temp folder>/aivory-<user>/<name>.sock |
Open the pipe or socket, write one request as one line, and read lines. The local connection is trusted: it needs no token. It is the only transport that may call the local-only methods.
This Python script talks to a demo daemon on Windows. On Linux and macOS, open an AF_UNIX socket to the path above and use the same lines:
import json
pipe = open(r"\\.\pipe\aivory-code-docs-try", "r+b", buffering=0)
def call(id, method, params=None):
msg = {"jsonrpc": "2.0", "id": id, "method": method}
if params is not None:
msg["params"] = params
pipe.write((json.dumps(msg) + "\n").encode())
# A notification has no id. Skip it until the response with this id arrives.
while True:
line = b""
while not line.endswith(b"\n"):
line += pipe.read(1)
reply = json.loads(line)
if reply.get("id") == id:
return reply
print(call(1, "daemon/info")["result"]["version"])
print(call(2, "autopilot/get"))
WebSocket
aivoryd serve --remote 0.0.0.0:7420
The daemon listens on that address. The pairing QR code names the LAN address of the machine and the same port. Connect with a WebSocket client to ws://<host>:<port>/ and add a query:
| Query | Use |
|---|---|
code=<code>&device=<name>&os=<os> |
First connection of a new device. |
token=<token> |
Every later connection. |
key=<64 hex characters> |
Optional. The Ed25519 public key of the device, so that it can use iroh without a new pairing. |
The server answers 401 Unauthorized for an unknown token or a code that is used or expired, and 400 Bad Request when the request is no WebSocket upgrade. Each text frame is one JSON-RPC message, in both directions. A frame can hold up to 8 MB. The LAN link has no TLS. Use it on networks that you trust, or use iroh.
iroh
aivoryd serve --iroh --relay default
--relay takes a URL, default for the iroh relays, or none for direct paths only. Open a QUIC connection with the ALPN aivory/rpc/1 and one bidirectional stream. Each message is a frame: a 4-byte big-endian length, then one JSON-RPC message (up to 8 MB).
The first frame must be the request link/hello:
| Field | Type | Meaning |
|---|---|---|
key |
string | The Ed25519 public key of the device, 64 hex characters. It must be the key that the QUIC handshake proved. |
device |
string | A name for the device. |
os |
string | The operating system. |
code |
string | The pairing code. Needed on the first connection only. |
needToken |
boolean | true asks for a LAN token too. |
The reply has deviceId, name, newlyPaired, node (the node id of the daemon) and, for a new device or needToken, a token for the WebSocket link. A device that is not allowed gets an error with code -32001 and the daemon closes the stream.
Message Format
Every message is a JSON object.
{"jsonrpc":"2.0","id":1,"method":"autopilot/get"}
| Rule | Detail |
|---|---|
| Request | jsonrpc must be "2.0". method is a string. params is an object, or you leave it out for methods without parameters. null and {} both work. |
| Response | {"jsonrpc":"2.0","id":<id>,"result":...} or {"jsonrpc":"2.0","id":<id>,"error":{"code":...,"message":...}}. |
| Notification from you | A request without id gets no response. |
| Notification from the daemon | {"jsonrpc":"2.0","method":"<name>","params":...} without id. |
| Batches | Not supported. |
| Parallel requests | A connection handles one request at a time. Open a second connection for parallel work. |
| Line length | A line longer than 4 MiB closes a local connection. |
| Slow readers | A connection buffers 1024 messages. A feed drops a client that falls behind. Subscribe again. |
Names in JSON are camelCase. Time values are milliseconds since the Unix epoch.
Pair a Remote Client
A remote client has no token at first. A local client creates a pairing code, and the remote client trades it for a token.
- A local client calls
devices/pair. The result hascode(AIV-and 6 characters),expires(a time in ms, 10 minutes ahead) andurl. - Show
urlas a QR code, or give the code to the remote client. The link has the formaivory://pair?node=<node id>&relay=<relay url>&host=<ip:port>&direct=<ip:port,...>&code=<code>. Each field is optional. - The remote client connects once with the code. On the WebSocket link it adds
?code=<code>&device=<name>. On iroh it sends the code inlink/hello. - On a new pairing, the first message from the daemon is a notification:
{"jsonrpc":"2.0","method":"device/paired","params":{"token":"<64 hex characters>","device":{"id":"d1a2b3c4","name":"Pixel","os":"Android","paired":0,"lastSeen":0,"online":true}}}
- Store the token. The daemon keeps only its SHA-256, so the token comes once. Use
?token=<token>on later WebSocket connections. On iroh, the key of the device proves who it is.
A code works once and ends after 10 minutes. devices/remove with a device id deletes the device and closes its iroh connection. settings/get lists the devices.
A paired device can register a push target with devices/push. The daemon sends the sealed phone push to the gateway that you set with --push-url <URL> (or the setting remote.pushUrl). Without a URL, the daemon sends no push.
Local-only Methods
A paired device gets the error -32004 (NOT_AVAILABLE) for these methods. Only the local pipe or socket may call them:
repos/add, repos/remove, project/new, map/addPart, providers/addAcp, providers/removeAcp, settings/importClaude, daemon/restart, theme/inspect, theme/add, theme/remove, theme/checkUpdates, theme/update, theme/diff, theme/job, registry/add, registry/remove, registry/refresh.
All other methods are open to a paired device. theme/list, theme/get and theme/asset are how a phone loads a theme.
Follow the War Room
warroom/snapshot returns the full view once. For a live view, use the feed:
- Call
warroom/subscribewith{}. The result hasepoch,cursorand asnapshot(the fullWarRoom). - The daemon then sends
warroom/deltanotifications on the same connection. Each hasparams.seq(increasing) andparams.delta. - Apply each delta to your copy of the view and set your cursor to
seq. - After a reconnect, call
warroom/subscribewith{"cursor": <last seq>, "epoch": <epoch>}. If the epoch is the same, the result hasdeltas(everything you missed) and nosnapshot. If the daemon restarted, the epoch changed and you get a freshsnapshot.
{"jsonrpc":"2.0","method":"warroom/delta","params":{"seq":42,"delta":{"type":"tick","now":1792593300000}}}
A delta has a type:
type |
Fields | Change |
|---|---|---|
session |
session (a Pane) |
Add or replace a session with the same id. |
sessionRemoved |
id |
Remove a session. |
lines |
id, subagent?, lines, total |
New transcript lines. Lines of a subagent only count. |
activity |
sample |
A new sample of the pulse chart, or a new value for the same at. |
autopilot |
autopilot |
The autopilot rules. |
counts |
queued, inboxOpen |
The counts of the badges. |
usage |
usage |
The provider usage windows. |
links |
hub, relay |
The state of the hub and relay links. |
codebase |
codebase |
Add or replace a codebase. |
codebaseRemoved |
id |
Remove a codebase. |
tick |
now |
The daemon time. |
board |
board |
The gates (needsYou, working, blocked, idle) and the order of the asks. A client that does not know a delta type keeps its old value. |
The Rust type WarRoom has apply(seq, delta) that implements these rules. The session states are needsYou, error, working, idle, parked and done.
Other Notifications
| Notification | Sent when | Params |
|---|---|---|
warroom/delta |
After warroom/subscribe. |
seq, delta |
device/paired |
The first WebSocket connection of a new device. | token, device |
session/event |
During sessions/resume, before its response. |
request_id, provider, session_id, seq, event |
The ADR for themes names a notification theme/changed. The daemon does not send it yet. Call theme/list again to see changes.
Methods
The schema has 77 methods, and the daemon has three more (sessions/*). Parameters with ? are optional. Result types are named in the schema $defs. The daemon returns -32004 for a method that needs the war-room engine on a daemon that does not run it.
Two methods work only with aivoryd serve --demo: session/resume and session/handoff. A live daemon answers -32004.
Daemon
| Method | Params | Result |
|---|---|---|
daemon/info |
none | name, version, protocol (jsonrpc-2.0/ndjson), contract, pid, demo, host, cwd |
daemon/shutdown |
none | ok |
daemon/restart |
daemon |
ok. Local only. |
daemon/scale |
daemon, maxRunning |
Autopilot |
resources/memory |
none | totalMb, usedMb, agentsMb, samples, top |
organizer/start |
codebase?, composer?, mode? |
id of the new session |
War Room and Autopilot
| Method | Params | Result |
|---|---|---|
warroom/snapshot |
none | WarRoom: epoch, cursor, now, codebases, sessions, activity, autopilot, queued, inboxOpen, usage, hub, relay, board |
warroom/subscribe |
cursor?, epoch? |
epoch, cursor, snapshot?, deltas? |
autopilot/get |
none | Autopilot: on, parkAfterMin, maxRunning, maxPerCodebase, startQueued, order |
autopilot/set |
any of the Autopilot fields |
Autopilot |
Sessions
Here id is a session id from warroom/snapshot.
| Method | Params | Result |
|---|---|---|
session/send |
id, text, subagent?, attachments?, composer? |
ok |
session/answer |
id, ask (the id of the open ask), option?, text? |
ok. A late answer for an ask that is closed gives -32006. |
session/pause |
id |
ok. Interrupts the turn. The session stays live. |
session/stop |
id |
ok. Kills the agent process. |
session/park |
id |
ok. Stops the session and keeps it resumable. |
session/resume |
id |
ok. Demo only. |
session/new |
codebase, composer (mode, provider, model, effort), prompt, part?, title?, task?, attachments? |
id |
session/dispatch |
prompt, codebase?, part?, agent?, model?, mode?, effort? |
session? or task?. With a free autopilot slot the daemon starts a session. Else it queues a task. |
session/mode |
id, mode (plan, edit, auto) |
ok |
session/handoff |
id, provider, model? |
id. Demo only. |
session/fork |
id, at?, prompt?, provider? |
id |
session/history |
id, before?, limit?, subagent? |
HistoryPage: lines, start, total |
session/spans |
id |
SessionTrace: nodes, edges, spans, stats |
session/diff |
id |
SessionDiff: files |
sessions/list |
provider?, cwd?, limit? |
sessions, stats. Native sessions of the CLIs. |
sessions/get |
id, provider? |
A session summary. |
sessions/resume |
id, prompt, provider?, model?, include_raw? |
status, exit_code, events. Sends session/event notifications first. |
The providers of sessions/* are claude, codex and gemini. The war-room methods also name opencode and goose.
Inbox and Tasks
| Method | Params | Result |
|---|---|---|
inbox/list |
filter? (all, answers, errors, reviews, conflicts) |
items, done |
inbox/answer |
id, option?, text? |
The InboxItem with its result |
inbox/snooze |
minutes, id? |
ok |
tasks/list |
codebase? |
tasks |
tasks/create |
codebase, part, title, prompt, playbook (feature, fix, port), when (now, queue, planOnly), priority?, provider?, mode?, composer? |
Task |
tasks/update |
id, column? (planned, working, waiting, review, done), title?, prompt?, part? |
Task. A move to working does not start a session. |
tasks/start |
id, composer? |
Task. A task that is not planned gives -32007. |
Repos and Projects
| Method | Params | Result |
|---|---|---|
repos/list |
none | codebases |
repos/get |
id |
RepoDetail: codebase, parts, files, activity |
repos/map |
id |
MapFile: the .aivory/map.toml of the codebase |
repos/add |
path |
Codebase. Local only. A folder that is no git repository gives -32602. |
repos/remove |
id |
ok. Local only. |
project/new |
name, parent, template? (empty, rust, node, python) |
Codebase. Local only. |
map/addPart |
codebase, name, paths |
MapFile. Local only. |
Settings, Providers and Devices
| Method | Params | Result |
|---|---|---|
settings/get |
none | SettingsView: settings, account, budget, daemons, devices, providers |
settings/set |
key (dotted camelCase, for example display.look), value |
Settings. The key must exist and the value must fit. |
settings/importClaude |
none | ImportReport. Local only. |
providers/addAcp |
name, command, args?, env? |
provider, found, version?. Local only. |
providers/removeAcp |
id |
ok. Local only. |
devices/pair |
none | code, expires, url |
devices/remove |
id |
ok |
devices/push |
token, platform (apns, fcm), pushKey, id? |
ok. Needs --remote or --iroh. |
Code Intelligence and the Ledger
Pass session to read the worktree of a session. The schema has the types of the positions, ranges and results.
| Method | Params | Result |
|---|---|---|
code/file |
codebase, path, lines?, rev?, session? |
CodeFile: text, language, highlights, outline, folding, diagnostics |
code/definition |
codebase, path, line, col, page?, perPage?, session? |
PlacePage |
code/references |
the same | PlacePage |
code/hover |
the same | Hover |
code/symbols |
codebase, query, kind?, page?, perPage?, session? |
SymbolPage |
code/diagnostics |
codebase, path?, turn?, session? |
rows, added, removed, notes |
code/rename |
codebase, path, line, col, newName, session? |
RenamePreview. The daemon never applies it. |
ledger/turns |
session |
TurnList |
ledger/blame |
codebase, path, lines?, session? |
FileBlame: the commit and the turn of each line |
ledger/timeline |
codebase, path, session? |
Timeline |
ledger/revert |
session, turn |
RevertResult. A session in the main checkout gives -32007. |
Coordination
| Method | Params | Result |
|---|---|---|
claims/list |
codebase?, session? |
claims |
radar/list |
codebase? |
hits, sessions |
radar/ping |
to, kind (fyi, question, request, block), text, from?, keys? |
ok |
mail/send |
the same as radar/ping |
ok |
mail/list |
session |
items |
block/override |
session, keys? |
resolved |
Themes and Registries
A job method returns {"job": "<id>"}. Poll theme/job with that id until state is done or failed. A done job has result.
| Method | Params | Result |
|---|---|---|
theme/list |
none | themes: id, name, version, author, license, source, edited, error? |
theme/get |
id, display (desktop, phone, fold, watch) |
ThemeBundle: tokens, layouts (the root node of each screen), parts, strings, fonts, sha256, preview?. Your client resolves use nodes with parts. |
theme/asset |
id, path |
path, bytes, base64 |
theme/inspect |
source |
A job. The result holds the token for theme/add. |
theme/add |
token |
InstalledTheme |
theme/remove |
id |
ok, clearedCurrent |
theme/checkUpdates |
none | A job |
theme/update |
id |
A job |
theme/diff |
id |
A job |
theme/job |
job |
state (pending, done, failed), result?, error? |
registry/list |
none | registries |
registry/add |
name, url |
RegistryEntry |
registry/remove |
name |
ok |
registry/refresh |
name? |
A job |
To draw a theme in your client, read the theme reference and call theme/get with your display class. The settings key display.customTheme holds the id of the theme that the user selected.
Examples
These calls run against aivoryd serve --demo. The commands print one response per line.
Read the Daemon
aivoryd call daemon/info --endpoint docs-try
Request:
{"jsonrpc":"2.0","id":1,"method":"daemon/info"}
Response:
{"id":1,"jsonrpc":"2.0","result":{"contract":"0.1.0","cwd":"C:\\work","demo":true,"host":"CAPTIVA","name":"aivoryd","pid":7972,"protocol":"jsonrpc-2.0/ndjson","version":"0.1.0"}}
contract is the version of the protocol. cwd is shortened here. cwd, host and pid change on each run.
Change the Autopilot
aivoryd call autopilot/set --params '{"maxRunning":10}' --endpoint docs-try
Request and response:
{"jsonrpc":"2.0","id":1,"method":"autopilot/set","params":{"maxRunning":10}}
{"id":1,"jsonrpc":"2.0","result":{"maxPerCodebase":4,"maxRunning":10,"on":true,"order":"priority","parkAfterMin":15,"startQueued":true}}
The result is the whole Autopilot object. Fields that you leave out do not change.
Handle an Error
aivoryd call session/pause --params '{"id":"zzz"}' --endpoint docs-try
Response:
{"error":{"code":-32001,"message":"no session 'zzz'"},"id":1,"jsonrpc":"2.0"}
An unknown method gives -32601:
{"error":{"code":-32601,"message":"unknown method 'nope/x'"},"id":1,"jsonrpc":"2.0"}
Error Codes
| Code | Name | Meaning |
|---|---|---|
-32700 |
PARSE_ERROR |
The line is no valid JSON. id is null. |
-32600 |
INVALID_REQUEST |
jsonrpc is not "2.0", or method is missing. |
-32601 |
METHOD_NOT_FOUND |
The method does not exist. |
-32602 |
INVALID_PARAMS |
The params do not fit the method. |
-32000 |
SERVER_ERROR |
An error inside the daemon. |
-32001 |
SESSION_NOT_FOUND |
No session with this id. On the iroh link, -32001 also means UNAUTHORIZED for link/hello. |
-32002 |
SESSION_AMBIGUOUS |
More than one session matches. data.candidates lists them. |
-32003 |
SESSION_HAS_NO_CWD |
The session has no working folder. |
-32004 |
NOT_AVAILABLE |
This daemon cannot do it: a local-only method from a paired device, or a method that needs the war-room engine or a service that does not run. |
-32005 |
NOT_FOUND |
No inbox item, task, codebase or device with this id. |
-32006 |
STALE_ASK |
The answer is for an ask that is no longer open. |
-32007 |
INVALID_STATE |
The session or task cannot do this in its state. |
An error has code, message and sometimes data.
Versioning
| Item | Where | Value now |
|---|---|---|
| Transport | daemon/info field protocol |
jsonrpc-2.0/ndjson |
| Protocol version | daemon/info field contract, and version in the schema |
0.1.0 |
| Feed epoch | warroom/snapshot field epoch |
Changes when the daemon restarts |
| Theme catalog | catalog in theme.toml |
1 |
The protocol is at version 0.x and may change between releases. Read contract after you connect and compare it with the schema that you built against. Ignore unknown fields in results and unknown type values of a delta. The JSON Schema file is generated from the Rust types and is the source of truth: proto/schema/aivory-protocol.schema.json. The folder proto/fixtures/ of the repository has one real result for each demo method. Protobuf frames are planned. The wire format today is JSON-RPC 2.0.