pi-daemon1.3

Log 04 · Protocols

A projection, not an authority.

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

PathProtocolForFrames
/ahpAgent Host Protocol 0.8.0, pinned to @microsoft/agent-host-protocolCulture, Paratenic’s upstream connector, applicationsJSON-RPC text frames, no subprotocol
/acp, /acp/<profile>Agent Client Protocol 1, via @agentclientprotocol/sdkeditorsone JSON-RPC message per text frame; agent-client-protocol.v1 confirmed if offered
/chordDurable’s own views and methodsJavaScript clients, /webJSON calls and subscriptions; subprotocol pi-daemon.chord.v1
/weba small page over /chorda browserbearer-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

Three adapters over one committed state Right, inside the profile's worker: the session's Durable store and what reads from it — agent events, transcript pages and views — and the Durable calls that change it. Middle, in the server: the AHP projection, which turns those reads into AHP state with the official reducers and keeps only a few AHP-specific records in the catalog; the ACP adapter; and the Chord router. They meet over one IPC link. Left, their clients: Culture and applications, editors, and JavaScript clients and the /web page. Reads flow leftward as state; client actions flow rightward and become Durable calls: submit, abort, configure, fork, answer. Cultureapplications editorsZed and others JS clientsand /web AHP projectionofficial reducersreplay window ACP adaptersession updates Chord routercalls · subscriptions agent eventswatchEvents() transcript pagescommitted entries viewswatch() · task graph Durable callssubmit · abortconfigure · fork session.sqlite SERVER WORKER state actions updates prompt · cancel items · ops calls commits AHP-only records, in the catalog: chat URI index, disposed chats, drafts, read and archive marks, default chat IPC
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

What an AHP client can subscribe to The root channel ahp-root:// lists the profiles as agents and the live terminals. Each session is ahp-session:/<id>: its chats, model and thinking config, MCP servers and other customizations, tools, needed input and changesets. Its chats are ahp-chat:/<id>/1, the root conversation, and the forks, side chats and subagents, under the URI a client chose or ahp-chat:/<id>/<conversation>. A chat's changeset is ahp-changeset:/<id>/<conversation>. Terminals and resource watches have channels whose URIs the client chooses; files are file: URIs within the profiles' cwd roots. ahp-root://agents = profiles · terminals ahp-session:/<id>one per session ahp-terminal:/…client-chosen · a live shell ahp-resource-watch:/…file changes, filtered ahp-chat:/<id>/1the root conversation ahp-chat:/<id>/<n>forks · side chats · subagents ahp-changeset:/<id>/<n>a chat’s file changes file:///…within the profiles’ cwd roots session stateconfig · customizationstools · inputNeeded listSessions resourceRead · resourceWrite · resourceList …
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

One turn over AHP Culture subscribes to the root chat and gets a snapshot of its newest 50 turns, with any turn in progress. It dispatches chat/turnStarted with its own turn id; the projection submits the input to the worker with request id ahp:<clientId>:<turnId>, and once Durable has placed it, every subscriber gets the turnStarted echo with the client's origin. The worker's agent events become streamed AHP actions: response parts, text and reasoning deltas, tool calls with streaming arguments and running output, and usage. When the agent calls ask_user, subscribers get chat/inputRequested; the first client to dispatch chat/inputCompleted answers it. The turn ends with chat/turnComplete. subscribe ahp-chat:/s1/11 snapshot: newest 50 turnsand the activeTurn2 chat/turnStarted · turnId t73 submit · ahp:culture:t74 pi.user placed5 turnStarted echowith the client’s origin6 agent events, per commit7 event batches8 responsePart · delta · reasoningtoolCall… · usage9 the agent calls ask_user chat/inputRequested10 chat/inputCompleted · accept11 answer the question12 chat/turnComplete13 CultureAHP clientCultureAHP client /ahpprojection/ahpprojection workerprofile processworkerprofile process session.sqliteDurablesession.sqliteDurable
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

Sequence numbers across a restart Two incarnations of the daemon on a time line. Each numbers its actions from a floor derived from the wall clock when it started, in milliseconds times 1000, so the second incarnation's numbers start above every number the first could have issued. The last 4,096 root actions are kept in memory. A client that reconnects with a last-seen number inside that window gets the missing root actions replayed. Older than the window, or from before the restart, it gets fresh snapshots. Session and chat channels are never replayed: they are reported missing and the client subscribes again. INCARNATION AINCARNATION Bfloor = start ms × 1000floor = start ms × 1000replay window: last 4,096 root actionsrestartlast seen inside the windowreplay the missing root actionslast seen older than the windowfresh snapshotslast seen before the restartbelow B’s floor: fresh snapshotsSession and chat channels are never replayed: they come back as missing, and the client subscribes again.
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.

06What it implements

A declared subset of AHP 0.8

AreaCommands and actions
connectioninitialize (0.8.0 only), ping, reconnect, subscribe, unsubscribe
cataloglistSessions most recently active first, kept live by root/sessionAdded, …Removed and …SummaryChanged
creationresolveSessionConfig (cwd, model, thinking, title), sessionConfigCompletions for cwd, idempotent createSession, disposeSession
turnschat/turnStarted and the streamed answer, chat/turnCancelled, fetchTurns
queuechat/pendingMessageSet (steering and queued), …Removed, chat/queuedMessagesReordered
configsession/configChanged for model and thinking; session/titleChanged; titles from the first prompt
inputagent questions as chat/inputRequested and inputNeeded; chat/inputCompleted, first response wins
chatscreateChat as fork, side chat or empty, with an initialMessage; disposeChat; subagent chats read-only
filesresource* commands within the cwd roots, createResourceWatch
mediaimage 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 callsstreamed arguments and output; chat/toolCallConfirmed and …ResultConfirmed for the tools a profile lists
session stateread and archive marks, default chat, drafts, active clients, activity, lifecycle with creationFailed
MCPservers as customizations with live state, serverTools, stop and start requests
changesetsper-chat changesets, invokeChangesetOperation diff and revert
terminalscreateTerminal, input, resize, claims, disposeTerminal
refusedchat/truncated (history is append-only: fork instead); working-directory changes (a session has one cwd)
everything elsenot advertised; commands answer MethodNotFound, channels NotFound

Every command, field and rejection in the operations reference

07Three spellings

One Durable call, three protocols

DurableAHPACPChord
submit inputchat/turnStartedsession/promptconversation.submit
steer · follow upchat/pendingMessageSet—whenBusy: steer · followUp
abortchat/turnCancelledsession/cancelconversation.abort
configuresession/configChanged—conversation.configure
forkcreateChat fork · side chat—conversation.fork
compact/compact as a turn—conversation.compact
answer a questionchat/inputCompletedsession/request_permissionsession.answerQuestion
changed fileschangesets, invokeChangesetOperation—session.changes · diff · revert
agent eventsstreamed chat/* actionssession/updateconversation.watchEvents
historysnapshots and fetchTurnssession/loadconversation.watch

08ACP

Editors drive the root conversation

  • 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.