Skip to content
ColliePWA

10/Documentation

The durable wire (experimental)

What travels between Collie and a durable agent, for a builder: the routes, hello and the epoch, the life of an ask and the request ids, the socket and token transports, the limits and the version rule, with a link to WIRE.md for the exact rules

Experimental. The wire may change, or the feature may be removed, in a later minor release. Versions and the telling hello says how a change reaches you.

The durable wire is how Collie talks to a durable agent. This page explains it for a builder who writes a durable agent without the SDK, or wants to know what the SDK does. The exact rules are in WIRE.md, and where this page and that file differ, WIRE.md is right. The durable SDK speaks all of it for you.

What travels

Collie dials and the durable agent listens, and every exchange is one HTTP request with a JSON body.

The durable agent listens on a private Unix socket, or behind TLS at a URL (Transports). Collie polls it: there is no push, no WebSocket and no stream. Every path starts with /session/v1, which is a fixed name, not the version. Every call but the first hello carries the version in the header X-Collie-Session. A request body is one JSON object of at most 64 KiB, and an empty POST body reads as {}.

method and pathwhat it doesreply limit
GET /helloThe durable agent's name, version range, epoch, readiness and caps16 KiB
GET /snapshotThe layout: workspaces, tabs, panes with their state, turn and open asks, and starters. Carries an ETag, and answers 304 when nothing changed4 MiB
GET /pane/:id/events?after=N&epoch=EThe pane's events after a cursor8 MiB
POST /pane/:id/answerAn answer to an ask16 KiB
POST /pane/:id/sendText for the pane, to steer the running turn or wait for the next16 KiB
POST /pane/:id/abortStop the running turn16 KiB
POST /pane/:id/actionA tap on one of the pane's belt buttons16 KiB
POST /renameA new title for a workspace or a tab16 KiB
POST /closeClose a pane, a tab or a workspace16 KiB
POST /starter/:idRun a starter. The reply names the new pane16 KiB
GET /conversations?after=C&limit=NThe durable agent's conversations, open and closed, newest first1 MiB
GET /conversation/:id/items?after=C&limit=NOne conversation's items, oldest first8 MiB
POST /reopenShow a closed conversation again16 KiB

Every error is a JSON body with a code, such as 409 stale or 404 no-such-pane. Any other path answers 404 not-found. A reply over its limit is not read, and a POST whose reply is cut has an unknown outcome (Request ids).

Hello and epoch

hello says who the durable agent is, and the epoch says which start of it Collie is talking to.

field of hellowhat it holds
serviceExactly collie-durable. Without it Collie reads the durable agent as some other server and calls nothing else
protocolThe range of wire versions the durable agent speaks, { min, max }. Today { min: 1, max: 1 }
host.nameThe durable agent's own name, display text only, at most 64 bytes
epochNew and random on every start of the durable agent, at least 128 bits, never reused
readyFalse while the durable agent lays out its panes after a start
capsWhat the durable agent can do, each a boolean. An absent cap is false

Collie first calls hello without the version header, picks the highest version both sides speak, and sends it on every later call. While ready is false, every route but hello answers 503 starting, and the phone shows the durable agent as starting. A durable agent sets ready only once every conversation is shown again and every open ask raised again, so a restart never shows half a layout.

The snapshot's ETag is "<epoch>-<rev>". rev rises on every change a reader could see, so a 304 costs the durable agent next to nothing. A new epoch tells Collie the durable agent restarted. Collie keeps showing the panes it has until it reads the new snapshot. It then reads each pane with its old cursor and gets the pane's whole current view in one reply.

One failed read after a good one keeps the last snapshot for that poll. Two failures in a row show the durable agent as unreachable. A socket path that fails the privacy walk is unreachable at once.

Asks and answers

An ask is a question card, and it is open from the durable agent's ask event until its ask-closed.

statewho sets itwhat the phone shows
OpenThe durable agent raises the ask. The pane reads waiting while any ask is openA card under "Needs your answer", with the durable agent's name, and the time left when the ask has expiresAt
ChangedThe durable agent changes the ask in place, and its revision risesThe card redraws. A tap on the old form gets 409 stale, and the card says "This ask changed. Read it again, then tap an answer."
TakenThe phone answers, and the durable agent closes the ask with by: "phone"The card goes, and the tool runs or is denied as the option says
Decided in the durable agentThe durable agent closes the ask itself, decidedBy: "program". A stop of the turn countsA line in Chat that starts "Decided in the durable agent", with the ask's title
Timed outThe clock passes expiresAt, decidedBy: "timeout"A line in Chat that starts "Timed out before an answer", with the ask's title

A tap that comes after the decision gets 409 decided with the first outcome, and nothing runs. The card then says who decided first: "Decided in the durable agent.", "Timed out before an answer.", or, after another tap, "Already answered. This card goes away on the next update." An ask the durable agent does not know any more answers 200 too-late. The card then says "Not taken. The question may show again in a moment.", because after a restart the durable agent may raise it again under the same id.

rulewhy
The ask id is a hash of the pane id and the tool call idThe same tool call raised after a restart has the same id, so an answer sent across the restart lands
A permission card is built from the tool's name and its arguments, never from the model's wordsA model that wants a yes would describe the call kindly
A durable agent behind TLS should offer no allow-alwaysThe token is the only gate, so one answer from whoever holds it would let every later call through
The phone offers no allow-always on a card whose words were cut or cleaned, or that names no toolThe person cannot see what the grant covers
expiresAt is ISO 8601 with a zonePast it the ask is decided by timeout, and a snapshot no longer lists it as open
The durable agent keeps the outcome of at least its last 500 decided asks per paneA late tap gets decided and the first outcome, not a plain too-late

An ask is one of four kinds: permission, choice, text or form. Each has its own answer shape, listed with the SDK's Questions.

Request ids

Every act from the phone has a request id, so it runs at most once.

The phone mints one request id per intent, and Collie writes its own receipt before it passes the id on. The durable agent keeps at least the last 500 ids of its epoch. A repeat of a finished id gets the first reply again with replayed: true and runs nothing. The same id with another payload gets 409 request-id-reused.

Collie never retries a POST by itself. A POST that times out, loses its connection or gets a 5xx has an unknown outcome. For an answer, the card then says "Not sure the answer arrived. Try again sends it once more.", and Try again sends the same request id, so the durable agent's memory answers a repeat.

A retry that finds the ask closed or the turn moved cannot tell whether the first try landed. Collie then answers the phone "maybe applied", the card says "May already be applied. Check the chat.", and the act is never sent a third time.

Transports

A durable agent uses one of two transports: a private socket on Collie's machine, or TLS with a token.

The bytes are the same on both: the routes, the bodies, the version header, the ETag and the error codes. Only who may reach the durable agent differs.

over the socketthe rule
PathAbsolute, at most 103 bytes. Both sides read it from COLLIE_DURABLE_SOCKET
ModesThe socket is mode 0600, in a folder with mode 0700. The durable agent removes a stale socket file before it listens
The path walkBefore each connect, Collie checks every part of the path with lstat. No part is a symlink. The socket's folder is yours with mode 0700. No folder above it is writable by group or other, except a sticky one such as /tmp. The socket is yours, with no group or other permission
A failed walkCollie does not connect, and shows the durable agent as unreachable with socket-not-private and the part that failed
TokenNone. The private socket is the gate
Who can actAny process of your user. The durable agent may refuse its own descendants with 403 caller-refused
Timeouts1000 ms for hello, snapshot and events. 3000 ms for an answer, send, stop, action, rename or close. 30000 ms for a starter
How often Collie asksEvery 1 s while a phone watches, else every 3 s. A durable agent that is down is asked once per poll
SystemsLinux and macOS. Windows has no socket transport
over TLSthe rule
URLhttps://, or http://127.0.0.1:<port> or http://localhost:<port> for a local test. At most 2048 bytes, no user name or password, no query, no fragment. Collie adds /session/v1 to it
TokenAt least 32 bytes, printable ASCII with no space. Collie sends Authorization: Bearer <token> on every request, hello included
The durable agent's checkThe token first, before the version, hello and ready, compared in constant time. A missing or wrong token gets a bare 401 { "code": "unauthorized" } and nothing else
RedirectsCollie follows none. A 3xx is passed up as a status, so the token never reaches another host
ETagA proxy that compresses a reply may weaken the tag to W/"<epoch>-<rev>". Both sides compare it with the W/ stripped
A refusalA 401 or 403 shows the durable agent as unreachable with token-refused
Timeouts5000 ms for every read and write, because a durable agent that slept may wake cold. 30000 ms for a starter
How often Collie asksEvery 2 s while a phone watches, else every 10 s. A durable agent that is down or refused is asked at most once every 10 s

Each timeout is a deadline for the whole exchange, from connect to the last byte. Over TLS every poll is a request your host may bill, a 304 included.

Limits

Collie cuts or refuses anything past these limits, so a durable agent that stays inside them loses nothing.

whatlimit
Workspaces per snapshot64
Tabs per snapshot256
Panes per snapshot256
Open asks per pane50, the oldest first
Options of a permission6
Options of a choice, and of a choice field8
Fields of a form6
Starters per snapshot50
Belt buttons per pane4, a label of 16 characters
An id1 to 128 bytes, no control character, no whitespace, none of /, ? or #
A request body64 KiB, else 413 too-large
Replieshello 16 KiB, snapshot 4 MiB, events 8 MiB, conversations 1 MiB, items 8 MiB, a POST 16 KiB
Events per reply1000. A reader that fell behind gets the current view, at most the newest 2000 items
Text of a user, reply, thinking or notice item64 KiB
Text of a markdown or code item32 KiB
Text the phone sends32 KiB
A link itemhttps:// only, at most 2048 characters
A history page100 conversations, or 1000 items
A history cursor512 bytes of printable ASCII, no space

A string over its limit is cut, and its controls and invisible characters are removed. A list over its limit keeps its first entries. A history page over its limit is not read at all.

Versions and the telling hello

Today there is one wire version, 1, and Collie speaks 1..1.

When version 2 ships, Collie will accept version 1 for one minor release after it, and the minor after that drops it. A reader ignores a field, an event type, an item kind or a code it does not know. A field added inside version 1, such as expiresAt or decidedBy, is optional. A new required field, a new meaning for an old field, or a removed field needs a new version.

An ask of a kind the phone does not know shows as one it cannot answer from the phone, with no buttons.

When the two ranges do not meet, Collie reads nothing more from the durable agent. Once per epoch of the durable agent it sends one more hello, this time with its own version, so the durable agent can log the mismatch on its side. The durable agent answers 409 incompatible with a refusal that names both ranges. This is the telling hello.

Collie writes one line in its log for each range it refuses, and collie doctor shows both ranges. The durable agent's home on the phone says the same sentence, for example "The durable agent speaks wire v2, this Collie speaks v1. Update Collie, or the agent's SDK." Update the older side.

Where the exact rules are

WIRE.md is the contract, with every shape, code and rule.

The decisions behind it are records in the repository:

recordwhat it decides
ADR 0099A durable agent is a multiplexer with no screen
ADR 0100An ask travels as data and is answered by id
ADR 0101Send and stop name the turn the phone saw
ADR 0102Starters are rows on the New page
ADR 0103The two transports
ADR 0104A durable agent is a session, and the exit
ADR 0105The SDK is an npm package

Edit this page on GitHub

The rest of the documentation