Appearance
ahpd 0.15
The good stuff
Agents get a terminal and ask the person, a failed turn can be resumed, and every client action is answered as VS Code expects.
- Agent tools: a shell an agent and a person share, commands in the background, and questions with options.
- Resume a failed turn: Claude, cofold and pi run a failed turn again from where it stopped.
- Machines on another box: Docker on an ssh box, and VMs on libvirt or Proxmox.
- Host tools at
/mcp: list, create and message sessions from any MCP client.- Tunnels the host keeps: Dev Tunnels in the daemon, with a device-code sign-in.
Unreleased
Upgrading
- The model calls have their own gate,
modelApi.httpApino longer serves/v1or the root model paths. A host that calls models through ahpd runsahpd config set modelApi trueafter the update, and restarts. See The model proxy.
Daemon
modelApi: { "port": N }serves the model API on a port of its own, apart from/api.ahpd config set <key> <value>andahpd config unset <key>write one setting of the configuration file. See Configure.- The descriptions of
httpApi,modelApiandmodelProxyare one short sentence each.
Plugins
- An icon is at most 256 KB of image, up from 32 KB, as VS Code bounds an icon's image. An Activepieces piece shows its own logo again, WhatsApp and Resend included.
- An agent record that is saved with new settings is asked again what it offers. A cofold agent given a
baseUrland anapiKeylists that endpoint's models with no restart. - A pi agent's
envtakes what a Claude agent's does: any variable by name, such asOPENROUTER_API_KEY, with text,{ "fromEnv": "NAME" }or{ "$secret": "host:NAME" }as its value. The form no longer lists pi's 38 key variables, and a key pi reads is still kept in the vault.
0.15.0-rc.1 - 2026-11-04
Upgrading
- A new session is now named
ahp-session:/<uuid>, and its provider is onSessionSummary.provider. A client that read the provider from a session URI's scheme reads it from the summary instead. A session that already exists keeps its<provider>:/<id>name. - A terminal's
terminal/datano longer carries the shell's OSC 133 and OSC 7 marks, as the protocol requires of a server. A client that read command boundaries or the directory from the marks readsterminal/commandExecuted,terminal/commandFinishedandterminal/cwdChanged. terminal/commandDetectionAvailablegoes out with the shell's first OSC 133 mark rather than when the terminal opens, andsupportsCommandDetectionis false until then. A shell that prints no marks never announces it.- A tool call part is its
kindand itstoolCall, as the protocol declares one, from every agent. A client that found a call's part by anidbeside them finds it bytoolCall.toolCallId. A system notification part carries noideither. - An ACP session's commands are the children of one read-only
commandsdirectory customization, as the protocol holds a prompt, and no longer customizations of their own. - A pi
!command's call is namedterminaland shown asTerminal, with the command as its intention, its line and its input, as every other backend's is. It was namedshell, shown as the command, with the command as JSON for its input. - A client action the host accepts goes to every client of its channel, takes the next
serverSeqand is kept for replay, as the protocol says. The host echoed some actions to their sender alone. A refusal still goes to its sender alone, and so does terminal input, so keystrokes such as a password typed at a prompt never reach another client. ahpd.restartNeededis a read-only value of the root config, set withroot/configChanged. A client that read it from the root's_metareadsconfig.valuesinstead.- A turn a wake or an async command's end starts carries
origin: { kind: 'systemNotification' }and novscode.chat.delegationentry, so a client draws it as a notice, not as the person's message. - A steering message a turn took in shows in that turn as a
systemNotificationpart whose content is the message, with its id in_meta['ahpd.steer'].steerPartandsteerOfin@ahpd/sdkbuild and read it. - Minting a team key needs
proxy:callas well asuser:write. Removing a team removes its keys. See docs/server/USERS.md. - The built-in
guestrole no longer holdsmcp:call.plugin:applyandplugin:reloadare part ofplugin:write. x-ahpd/invokeOperationchecks the operation's own grant. A caller who holds onlyoperation:invokeno longer runs every operation.--pluginadds to the configured plugins instead of replacing them.- A session file this version cannot read is moved to
<file>.unreadable-<time>at load and logged, so the next write does not replace it. - A release candidate is published under the npm
nexttag.npm iandahpd updatekeep readinglatest. See DEVELOPER.md.
Agent tools
- An agent gets VS Code's shell tools:
bashorpowershell, withread_,write_,list_and_shutdownfor the same shell. Each call shows its terminal, and a person can type into it. They are off by default, behind the root config keyenableCustomTerminalTool, and never on Windows. On Claude the host'sbashreplaces the CLI's own, and on pi the host'sbashandpowershelldo. cofold and ACP get them beside their own. Each tool checks the grant of the terminal method it matches, as the person who sent the turn. - The shell tool takes
mode: "async". The command answers at once with its shell id and shows as a background row on the chat, with its terminal and a stop. When it ends, the row goes and a turn begins with the command, its exit code and the last 60 KB of its output, as a wake does. - A shell opened with no command, under a pseudoterminal and not in a machine, runs VS Code's shell integration, so each command is its own part with its exit code.
nonInteractiveandpreventShellHistoryare terminal options. Each terminal under a pseudoterminal keeps an@xterm/headlessscreen in the server, so the host knows when a program uses the alternate screen. - The
ask_userhost tool asks the person up to four questions, each with options,multiSelectandallowOther, and waits for the answer. A stopped or ended turn cancels a question still waiting. cofold uses it in place of its own. Claude keeps its own question tool.
Sessions and VS Code parity
- A
!command's call names its terminal as soon as the terminal opens, withchat/toolCallContentChanged, so a client watches the output arrive, as VS Code's does. A backend hears the terminal from the second argument ofSession.ran'srun(RunCommand,CommandTerminal), and the handle of a terminal a backend opens saysisPty. See docs/sdk/TERMINALS.md. - A terminal a tool call names opens for as long as its chat lasts. A
!command's terminal and a terminal a backend opens are namedahp-terminal://shell/<chat>/<key>, and what each printed is kept in the session's folder, a!command's when the command ends and a backend's when its terminal exits. A subscribe that no live terminal answers gets the exited terminal back: after a client disposed it, after the backend released it, and after the daemon started again. What is kept goes with its turn on a truncation, with its chat and with its session, and moves with its chat. A backend keeps an output of its own withStart.terminalOutputs, and a transcript read is handed the outputs of the chat it reads. See docs/sdk/TERMINALS.md. - A
!command's turn survives a restart. The host keeps it in the session's folder as a local turn of its chat, anchored to the turn before it, and every read of a transcript puts it back there, so its terminal opens again too. A truncation or a fork at one goes to the backend at the turn before it, a truncation takes the ones it drops, and they go and move with their chat. cofold stores a turn's prompt under the turn's id and pi names its prompt's entry throughonTurnRecorded, so their turns read back under the names the anchors use. Claude writes a turn's prompt under the turn's id, as VS Code's agent host does, or under a uuid it names throughonTurnRecordedwhen the id is not a UUID, so a Claude turn read back after a restart also keeps its sender; the id was taken from an echo of the prompt that the CLI never sends. An ACP chat read back after a restart is its server's replay, which names its turns anew, so only a command before its first turn comes back. See docs/sdk/TERMINALS.md. - A chat read back after a restart shows each turn as it ran. The host records in the session's folder the transcript id each turn began under, its own id as it starts and what
onTurnRecordedsays, and a read puts a turn the record does not hold inside the turn before it, as Claude's CLI writes a steering message it runs after the turn's last answer as a prompt of its own. A turn written down twice, as a retried turn's prompt is, reads back as one turn. A chat from before the record, or on a store with no session folder, reads as its transcript gives it, and so does an ACP chat, whose server's replay names its turns anew. See docs/sdk/AGENT.md. - The Bot chat's
botsetting is a picker of the bots this daemon has, labelled by each bot's name. A backend now answers for a key of its own schema withAgent.completions, so a key whose values the host knows arrives as a picker rather than a text box. See docs/sdk/AGENT.md. - A session's settings come in the order VS Code's agent host composes them: the computer and isolation first, then the agent's own settings and another plugin's, then the branch and the worktree keys. A running session reports the same order. Only the order moved: which value wins a name two of them declare is unchanged.
- A turn that failed on a model call can be resumed with
chat/turnResume, on Claude, cofold and pi. It ends with an error part markedresumable, and a resume runs it again from the end of the turn before. pi keeps the tools the turn already ran, and Claude and cofold run the turn again whole. - Every client action is answered as the protocol says: accepted, said back, or refused with a reason. This covers drafts, read and archived bits, working directories, terminals, reviews, queued and steering messages, stops with no turn running, and customization switches. A dispatch whose apply throws is refused with the error's message. A client's own action comes back to it with its origin, and nothing else carries that origin.
- A queued message sent again as a steer, as VS Code's "Send Immediately" does, runs once in the running turn and not again as a turn of its own.
- A message steered into a running turn stays visible in that turn, live and after a reload, in a Claude session and in a bot chat.
- A confirmation or a question that waits leaves the session's activity as it was. The InputNeeded status says it, as in VS Code.
- A token a client pushes for an MCP server's sign-in is spent only on the sessions that client's connection watches. A client can name one server in
authenticate'starget, and the token then goes to that server in that session alone. See docs/sdk/AUTHENTICATION.md. - A session an automation started keeps its origin after a restart: its summary names the automation, and its first message has origin
automation.
Daemon
- On Bun, a terminal is a pseudoterminal with OSC 133 command detection: Bun's own, through the
terminaloption ofBun.spawn, from Bun 1.3.5 on Linux and macOS and from Bun 1.3.14 on Windows, where it is ConPTY.node-ptyis never loaded on Bun, where it loads but every shell it starts dies at once. Node and Deno keepnode-ptywhere it loads. See docs/sdk/TERMINALS.md. ahpd doctorruns every check on this machine and prints one line for each, with the fix when something is wrong.--jsongives the checks as an array, and the exit code is 1 when a check fails. See docs/server/RUN.md.- A client of the daemon's AHP restarts it with
x-ahpd/restart, with the checks ofahpd restart. It takesconfig:write, and only the host's own connection may send it. The answer is{ "restarting": true, "pid": <pid> }. See docs/server/RUN.md. - The host serves its tools that need no session at
/mcp, to a person's MCP client such as VS Code or Claude Code, with their ahpd token and under their grants. The first tools list, create, message, read, rename and delete sessions. It needsmcp:call, whichmemberholds. See docs/server/MCP.md. x-ahpd-clients://lists the open connections, with the address, the connect time and the traffic in each direction, and itsdropoperation closes one. Reading it needsclient:read, and droppingclient:drop.ahpd startthat gets SIGINT, SIGTERM or SIGHUP before the daemon is up stops the daemon it started.- When
/usr/localis not writable, the install script also writes the PATH line into the shell's login file, so a desktop app findsahpdfrom the next login.--uninstallremoves it again.
Terminals
- The output a shell printed before a mark goes out before the mark's action, so the output between
terminal/commandExecutedandterminal/commandFinishedis the command's. A mark that arrives in two reads is still read and taken out. See docs/sdk/TERMINALS.md. - On pipes, a character whose bytes arrive in two reads reaches the client whole rather than as two replacement characters: each pipe is decoded as one stream. See docs/sdk/TERMINALS.md.
Agents
- Claude: a call that a stopped turn was in stays cancelled when the CLI answers it after the stop. A client that subscribes later reads what a client that watched built, no
chat/toolCallCompletewithout aturnIdis sent, and the stopped session does not say it is thinking again. An edit such a call made is recorded under its own turn. See docs/sdk/CHATS.md. - Claude: an answered
AskUserQuestionreads back with its answers after a restart, in the session and in a subagent's chat. The answers are read from the transcript entry where the CLI writes them, which the SDK's reader leaves out. See packages/agent-claude/README.md. - Claude, ACP, cofold, pi and the bot send only what the protocol declares: no
idon a tool call part or a notice, live or read back from a transcript. Each is driven through a host in its suite, every frame it sends and the chat a late client reads checked with the protocol's own schema. The wire suite reads the chat again after its turn, where it read only the session. - ACP and pi: a client that subscribes after a call ran reads the call a client that watched built, with its line, its confirmation and its result's content, as the protocol's reducer builds it.
- Every agent on the root channel has a
description, whichAgentInforequires: the backend's own, or its display name. - Claude, ACP, cofold and pi: a terminal a call names says
isPtyas the terminal's state does, so a client parses the VT sequences a pseudoterminal writes; it saidisPty: falsewhatever ran the command. An ACP server's own terminal says it too. - cofold: a
!command's call is held as a client that watched it holds it, so a client that subscribes while the command runs reads the running call, and its ready says the command as its line. - pi: a turn names the entry pi wrote its prompt under however it ends, and when its session closes while it runs, as a host that stops closes it. A turn the host was running as it stopped read back without its sender.
- pi: a
!command's call names the terminal it ran in, which a client opens to watch it and read it after. - Claude: a chat the host resumes by an id the SDK holds no conversation under, one nobody spoke to or one whose CLI could not sign in, starts a new conversation under that id, asked of the SDK's
getSessionInfoas its first CLI starts, as VS Code's agent host asks. A transcript under that id in which the SDK finds no session is deleted first, through the SDK'sdeleteSession, since the CLI starts no conversation under an id it holds a transcript for. The CLI ended at startup withNo conversation foundwhen it was told to resume one. A conversation the SDK holds is resumed from any project folder, as its resume finds it. See packages/agent-claude/README.md.
Claude
- A turn that starts on a new Claude CLI, for an agent the message picked, an edited record or another login, sends its prompt to that CLI, as VS Code's agent host keeps its prompt queue across a rebind. The SDK reads a new CLI's input only once its initialize request is out, which a session offering a client's tools writes late, and the prompt went to the reader of the CLI it replaced, whose closed query dropped it: the turn sat at "Thinking" for good.
- A Claude turn that a client starts after announcing its tools is offered those tools, as VS Code's agent host rebuilds its query with them before it sends a turn. The turn waits until the CLI has taken the change, and changes reach the CLI one at a time in the order they came. The turn went out beside the change and ran without the client's tools, and two changes close together could leave the first set in place.
- A steering message the CLI answers after the turn's last answer is answered in that turn, as VS Code's agent host keeps it. The turn stays open until the CLI has answered every prompt it was handed in it, and a queued message waits for that. The answer opened a turn of its own with an empty message, and a queued message that started in between took the answer for its own. See docs/sdk/CHATS.md.
- A steering message the CLI still holds when its turn is stopped is answered in a turn of its own, which opens as that turn ends and carries the message, so a turn sent meanwhile waits for it. A stop of that turn stops the message with it, and one the CLI holds when its turn fails fails with it. What the stopped answer said after the stop opened a turn with an empty message, which its interrupted result then ended as a failure, and a turn begun after the stop took the steering message's answer.
- A Claude turn begun right after a stop runs to its own end. The stopped turn's result ended it as a failure.
- A Claude transcript read back after a restart shows the turns a client saw live. A steering message the CLI took in at a tool call reads back in that turn, and so does one it ran after the turn's last answer, from the host's record of the turns a chat began; a stop reads back as the end of a cancelled turn. Each read back as a turn of its own, and a stop as a turn of the CLI's stop text. See docs/sdk/CHATS.md.
- A
Bashresult too large to send shows the whole output Claude saved in a terminal on the call, as VS Code does. The call keeps Claude's notice as its text, and the terminal has exited, with 0 for a command that worked. The host keeps the output for as long as the chat lasts, and a call read back from the transcript names the terminal again. - An agent runs on several Claude logins, each a connector of the
claude-loginadapter this plugin registers: a sign-in on claude.ai, or a tokenclaude setup-tokenprinted. A record names them inlogins, in the order a turn tries them, and a turn runs on the first ready one. A request that fails with no connection, a 429 or a 5xx before the turn showed anything goes to the next login, as the model proxy hands a call to its next entry. A refused credential ends the turn naming the login, a failure that names a retry time keeps its login out until then, and each failure is logged and checked on the login's row. See packages/agent-claude/README.md. - A Claude session is offered the MCP servers the host offers every session. A stdio server whose folder is not the session's is left out, and the log says so.
- A skill, command, agent or rule names the file it came from, so a client opens the real
SKILL.md. TheCLAUDE.mdfiles and.claude/rules/**/*.mdare listed as rules. One written after a session opened shows at once. - A failed turn ends with the CLI's own words. A login refused for its limit says so on its connector's row, with when it is back.
- A turn stopped while a call waits tells Claude "The turn was stopped" for that call, not that the person refused it.
- A background task's row stays after its turn ends, and a stop removes only that task's row. When a task ends, the turn the CLI opens shows its summary as a notice, live and after a restart.
- A symlink loop in
.claude/rulesno longer stops the daemon: the walk visits each folder once.
Machines
- A box in
ssh.machinessays what it can run: readcomputer://ssh.<name>/serverfor its OS, CPUs, memory, disk, Docker, KVM and libvirt, with the lines that fix what is missing. Nothing is installed on the box. - A profile with
servermakes its machines on that box's Docker. The session's code arrives by bundle or by clone, and the work comes back to the session's branch. No credential of this host is mounted. - A profile with
runtime: "libvirt"orruntime: "proxmox"makes its machines as VMs, on the libvirt of itsserveror on a Proxmox node. A VM can be suspended, and a suspended VM is not metered. See docs/plugins/COMPUTERS.md. - A cofold or pi agent record runs in a machine. The machine gets the record by name, and an ssh box or a joined node gets it without its secrets.
Plugins and connectors
- The new session form and the plugins screen come in the language a client sends as
localeininitializeorreconnect. The host and each first-party plugin shipptandes, and a string with no translation stays in English. A plugin ships its own text withahpd.i18n. See docs/plugins/PLUGINS.md. - An engine's schema may mark a field
x-ahpd-connectorwith an adapter, as an automation's may. The agents channel then refuses a put that names a connector the host does not serve, one of another adapter, or one that is neither the record owner's nor the host's. See docs/plugins/PLUGINS.md. - A tunnel kind's field marked
x-ahpd-connectoris checked too. A tunnel create or update is refused when the field names no host connector of that adapter, or a person's connector. See docs/plugins/PLUGINS.md. - A client draws a field marked
x-ahpd-connectoras a list. The host sends its connectors of the adapter inenum, with their titles inenumLabels, on the tunnel forms, the engine rows and the plugin trigger definitions. An engine or trigger field is also markedenumDynamic, and the newx-ahpd/completionscommand answers the reader's own connectors. See docs/plugins/PLUGINS.md. probe, the check one call with a credential answers, is exported from@ahpd/sdkfor an adapter'scheck, and takes a JSON body for a check that posts.- A Dev Tunnel enabled at boot waits for its connector's token refresh before it calls the service. It started with the vault's token, which has expired when the daemon was down longer than eight hours, and failed with
GitHub API returned 401 (Unauthorized)while its connector's check said it works. - The host keeps a list of tunnels at
x-ahpd-tunnels://, and a plugin adds a kind of tunnel. Dev Tunnels is the first kind, run in the daemon. A connector of thedevtunneladapter signs in with GitHub by a device code. The system tunnel that VS Code and ahpc find stays at the same address. - An adapter may declare a device sign-in: the host answers a code and the provider's page, and waits for the person. A connector whose check the provider refuses is
unauthorizeduntil a check passes or it signs in again. - A plugin shows its state on its page with
showView, from a small set of components with literal values. The Dev Tunnel plugin shows its address, port, relay state and client count. - A plugin that fails after it loads says so on its row.
- A plugin registers the grants it requires with
host.registerGrants, so a role editor lists them. A required grant nothing lists is one line in the plugin's problems. - A connector row shows
<set>for everywriteOnlyvalue, at any depth. examples/canvasis a plugin that serves a canvas from its own HTTP server on a loopback port.
Access and usage
- Nobody gives a grant they do not hold: writing or removing a role or a person is bounded by the caller's own grants, over AHP as on the command line. A person with
role:putcould make themselves admin throughrole://. - A minute limit with
pool: eachcounts each person, as a minute limit withpool: allcounts everybody. x-ahpd/meon a plugin's own connection answers the plugin's grants and itsplugin:<name>pool.
Push
@ahpd/pushsends to a browser through its Web Push service, as it sends to a phone through Expo. A browser registers underpush://devicesand reads the host's key frompush://vapid. See docs/plugins/push.md.
Bots
- A message steered into a bot chat keeps its sender on the turn it starts:
originand_metacome from the steered message rather than falling back to the user.Session.steertakes them as an optionalfromafterattachments, and a backend that takes no sender ignores it. - A message to a bot goes to its back session as a queued message, and a message steered into the open main turn goes to the back turn as a steer.
- A back session can post more than once in one turn, and a turn typed in the back session opens a turn in the main chat. A question or a call the back session stops asking about leaves the main chat at once.
AGENTS.mdis the bot's one copy of its persona.- The Bot chat's picker lists only the bots its reader may read, and a chat with another person's bot is refused.