pi-daemon speaks three protocols over one listener. None of them owns any state: each is a view of what Durable committed, and each client action becomes a Durable call with the client’s own identity as its request id.
01Surfaces
Three ways in, one bearer
Path
Protocol
For
Frames
/ahp
Agent Host Protocol 0.8.0, pinned to @microsoft/agent-host-protocol
Agent Client Protocol 1, via @agentclientprotocol/sdk
editors
one JSON-RPC message per text frame; agent-client-protocol.v1 confirmed if offered
/chord
Durable’s own views and methods
JavaScript clients, /web
JSON calls and subscriptions; subprotocol pi-daemon.chord.v1
/web
a small page over /chord
a browser
bearer-to-cookie login
Every request and WebSocket upgrade except /healthz and /readyz must carry Authorization: Bearer with the instance’s token; a wrong or missing one gets 401 before any route sees it. initialize on /ahp refuses any protocol version but 0.8.0, so upgrade Culture and pi-daemon together.
02Projection
Why this AHP host is small
Fig. 4.1State flows out of the store; actions flow into it as Durable calls. Nothing in between is the source of truth.
An AHP host is usually the authority for its sessions: it must persist and sequence every action, fence stale backends and reconcile what its runtimes did while it was away. That is the job Paratenic does for Pi sessions that cannot make themselves durable. pi-daemon’s sessions already are, so its AHP layer only projects: session and chat state is rebuilt from committed Durable entries through the official AHP reducers, and can be rebuilt at any time. There is no persisted AHP log. After a restart, clients get fresh snapshots, which the protocol explicitly allows.
03Channels
What there is to subscribe to
Fig. 4.2Each profile is an agent; each Durable conversation of a session is a chat; the root conversation is the default chat.
An MCP server appears as a customization with the URI pi-daemon-mcp:/<name>, so its command, URL and environment are never shown to clients.
04A turn
From turnStarted to turnComplete
Fig. 4.3The client’s turn id stays the turn’s id in history; sending the same turn again is rejected, never run twice.
Late subscribers get the running turn as the snapshot’s activeTurn with the response so far, and the rest live.
History pages backwards: the snapshot carries turnsNextCursor, and fetchTurns sends the next older turns as chat/turnsLoaded.
Busy chats reject a second chat/turnStarted; send it as a steering or queued message instead.
A broken stream — the worker restarted — disconnects the chat’s subscribers, who reconnect for fresh snapshots.
05Reconnect
Replay when it is cheap, snapshots otherwise
Fig. 4.4A remembered sequence number can never be mistaken for a new incarnation’s: each starts above the last.
A client that falls more than 16 MiB behind on unsent frames is disconnected rather than buffered without bound; it catches up with reconnect.
chat/turnStarted and the streamed answer, chat/turnCancelled, fetchTurns
queue
chat/pendingMessageSet (steering and queued), …Removed, chat/queuedMessagesReordered
config
session/configChanged for model and thinking; session/titleChanged; titles from the first prompt
input
agent questions as chat/inputRequested and inputNeeded; chat/inputCompleted, first response wins
chats
createChat as fork, side chat or empty, with an initialMessage; disposeChat; subagent chats read-only
files
resource* commands within the cwd roots, createResourceWatch
media
image and audio attachments on messages; tool-result images and media tools share come back as resources from the session’s media store; usage carries live context-window telemetry
completions
/ commands, prompt templates and /skill: commands; ! shell commands
tool calls
streamed arguments and output; chat/toolCallConfirmed and …ResultConfirmed for the tools a profile lists
session state
read and archive marks, default chat, drafts, active clients, activity, lifecycle with creationFailed
MCP
servers as customizations with live state, serverTools, stop and start requests
changesets
per-chat changesets, invokeChangesetOperation diff and revert
Which profile. The path /acp/<profile>, else _meta["pi-daemon/profile"], else the only profile. MCP servers come from the profile; ones the editor offers are ignored.
New and loaded sessions.session/new creates a catalog session in a cwd under the profile’s roots. session/load replays the newest 1,024 turns of the root transcript as updates, then answers.
Prompts.session/prompt streams text, thinking and tool calls and answers end_turn or cancelled. _meta["pi-daemon/requestId"] makes a retry the same submission. One prompt runs per session per connection, at most 16 per connection.
Questions. An ask_user during a prompt becomes session/request_permission: each choice is an option, and a free-text question offers Yes and No.
Disconnects. Closing the connection stops its streams. A running turn carries on in its worker.
09Chord and /web
Durable’s own shapes, remoted
Chord 1.1.0 ships no WebSocket remoting, so /chord is a thin equivalent: calls routed to the session’s worker, and subscriptions that stream Durable agent events, or a view’s value followed by its Chord delta operations so a client keeps a replica current.
// a call, and its answer
{ "id": 7, "method": "conversation.submit",
"params": { "sessionId": "s1", "requestId": "msg-42", "content": "Summarise today", "wait": true } }
{ "id": 7, "result": { … the settled submission … } }
// a subscription: items until it ends
{ "id": 8, "method": "conversation.watchEvents", "params": { "sessionId": "s1" } }
{ "sub": 8, "items": [ … ] }
{ "sub": 8, "end": … }
Methodssessions.list, conversation.watchEvents, conversation.watch, session.watchTaskGraph, conversation.submit, abort, compact, configure (model and thinking only), fork, reset, and the session.*Question calls.
For a full clientsessions.create (idempotent per request id), sessions.dispose, sessions.watch (the catalog, live), session.rename, session.mark, session.models, session.commands and session.completions (/ commands and @ files), conversation.command (! and built-in / commands), session.usage (spend and context use), conversation.draft, setDraft and watchDraft (the chat draft AHP clients share) and session.media: the same sources AHP uses. pi-client is built on exactly these.
Bounds64 calls in flight and 64 subscriptions per connection. ChordClient in the package is a Node client of all of it.
/webA session list, a live conversation, a composer that sends, steers and aborts, and model and thinking choice. Culture is the rich browser client; this is a window onto the same views.
Its loginThe page posts the bearer once and gets an HttpOnly, SameSite=Strict cookie, Secure over TLS, held in memory for 12 hours. It admits only same-origin /chord upgrades.