pi-dock0.5.2
Run named, resident Pi agents in the background with durable sessions: spawn them, send prompts, wait for results, and stop and wake them with their memory intact.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-dock
Keep named Pi agents running in the background. pi-dock spawn starts an agent that outlives
the shell that created it; you hand it prompts, wait for or read its results, power it off, and
wake it later with its memory intact.
Each agent is one detached process hosting one Pi session, identified by a name you choose:
- Delegate and walk away: send a long task, close the terminal, read the result later.
- Several agents side by side: residents in different directories or with different models, working while you do something else.
- Memory across prompts: a stopped agent keeps its session; the next prompt continues it.
- Teams over pi-link: a docked agent can join pi-link and talk to your other terminals.
[!WARNING] Trusted directories only. An agent loads
<cwd>/.piconfig and extensions without Pi's project-trust prompt. Spawn agents only in directories you trust.No limits. There is no turn, time or cost cap. An agent runs until you stop it or it crashes, and work that extensions deliver while it is idle (e.g. pi-link messages) is unbounded too. Stop agents you no longer need.
Contents
- Prerequisites
- Install
- Quick start
- Commands
- How it works
- Recipes
- For AI agents
- Data locations
- Status and limitations
Prerequisites
- Node.js 22.19 or later (required by the Pi SDK).
- Pi set up with credentials for at least one model. pi-dock runs agents with its own copy of the
Pi SDK (
@earendil-works/pi-coding-agent1.4.0) but uses your Pi agent directory (~/.pi/agent, orPI_CODING_AGENT_DIR) for credentials, settings, models and extensions.pi-dock modelslists only the models whose credentials are configured. - The
picommand is needed to set up credentials and to install pi-dock for Pi (below). - Tested on Windows. Linux, macOS and Android (Termux) are little tested yet; see Status and limitations.
Install
pi-dock is a Pi package that contains only the pi-dock skill (no extensions) and the CLI. With pi-dock 0.4.0 or later on npm, this is all Pi's AI agents need (if npm still offers only 0.0.1, a placeholder for the name, install from a checkout as described below):
pi install npm:pi-dock # the skill, plus the CLI it runs
pi remove npm:pi-dock # undo
Pi installs the package and its dependencies under ~/.pi/agent/npm but does not put the
pi-dock command on PATH, so the skill tells the agent to run the package's bin/pi-dock.mjs
with Node. For a pi-dock command on PATH, for you, your scripts and the recipes below, also
install it globally:
npm i -g pi-dock # optional
npm uninstall -g pi-dock # undo
npm audit flags undici (GHSA-4qv8-2p9j-2685, GHSA-f72v-6w45-q472, GHSA-xjj6-f33q-929c; fixed
in 8.12.2) through the Pi SDK, which pins undici 8.10.2. pi-dock can't override it for you:
npm ignores overrides in an installed package, and npm audit fix --force would downgrade
the SDK to 0.75.3. Exploiting them takes a malicious HTTP or SSE response on an affected
undici API path; Pi 1.4.0 pins the same vulnerable undici version. The warning goes away once
pi-dock moves to an SDK that ships a fixed undici.
For development, install it from a checkout of this directory:
cd path/to/pi-dock
npm ci # dependencies, exactly as locked
npm install -g . # puts the pi-dock command on PATH
pi-dock --help
pi install . # Pi loads the local package in place; nothing is copied
npm install -g . links the global command to the checkout, so keep the checkout and its
node_modules in place. Undo with npm uninstall -g pi-dock and pi remove path/to/pi-dock.
Quick start
pi-dock models haiku
pi-dock spawn --name w1 --model anthropic/claude-haiku-4-5
pi-dock send w1 --wait "Summarize README.md in three bullets"
pi-dock logs w1 --tail 5
pi-dock show w1
pi-dock stop w1
models haikulists the matchingprovider/idrefs you can use, with context, max-out, thinking and images columns.spawncreatesw1in the current directory and printsw1 idle anthropic/claude-haiku-4-5. The agent normally waits idle, though an extension may already have started work (the state then readsrunningorcompacting); spawn never takes a prompt.send --waitqueues the prompt, prints its id (p+ 12 hex) on stderr, and prints the run's final text on stdout once it is done.logsshows what happened;showprints the agent's name, state, model, thinking, flags, system prompt size, tools, cwd, session file and creation time.stoppowers the agent off and printsw1 stopped.pi-dock send w1 "…"orpi-dock start w1wakes it again, with the conversation intact.
Every command exits 0 on success and 1 on error, with the reason on stderr. Options and
arguments are strict: anything unknown fails with the command's usage line. The exception is
compact, which takes all remaining arguments as instructions: pi-dock compact w1 --help
compacts w1 with the instructions --help. For usage, run the top-level pi-dock --help.
Commands
| Command | What it does |
|---|---|
spawn --name <name> [--model <provider/id>] [--thinking <level>] [--system-prompt <file>] [--tools <a,b> | --no-tools] [--x key[=value]]... |
Create an idle agent in the current directory |
send <name> [--wait] [--file <path>] [--] [text...] |
Queue a prompt and print its id; --wait also waits for the result |
wait <name> <id> |
Wait for a prompt's run and print its final text |
start <name> |
Wake a stopped or failed agent |
stop <name>... |
Power agents off and confirm their processes exited |
ls |
List agents: name state model age |
show <name> |
Print one agent's details, one key and value per line |
logs <name> [--tail <n>] [--raw] [--follow] |
Print the agent's event log |
set <name> [--model <provider/id>] [--thinking <level>] [--system-prompt <file>] [--tools <a,b> | --no-tools] [--x key[=value]]... |
Change a powered-off (stopped or failed) agent's configuration |
compact <name> [instructions] |
Compact an idle agent's session |
models [filter] |
List the models you can use |
skill |
Print the operating guide for AI agents |
pi-dock --help shows usage and the essentials. The full operating contract (every outcome,
message and rule) is in pi-dock skill.
How it works
Resident agents
An agent is resident: its process never exits because work finished, only on stop or a crash.
ls and show report its state: idle, running or compacting while it is on; stopped
after a stop; failed after a crash; not-responding when its process did not answer in time,
which does not tell whether it is alive. Neither wakes it.
stop is a power-off, not a delete. The name, configuration, log and session survive, and
start, send and compact wake the agent with its memory. pi-dock has no command that
deletes an agent; see Deleting an agent to do it by hand.
pi-dock stop w1 w2 w3 stops a team. It checks the whole list first, so an invalid, unknown or
repeated name stops nothing. Then it stops the agents one after another and prints one line per
name (w1 stopped, w2 already stopped); a failure on one goes to stderr, the others are still
stopped, and the exit code is 1. There is no --all and no pattern.
send never creates an agent, so a typo in the name fails instead of starting a new one.
Getting results
pi-dock send <name> --wait "<prompt>"blocks until the run ends: stdout carries only the final text, and the prompt id goes to stderr.- Without
--wait,sendprints the id at once;pi-dock wait <name> <id>waits for it later, and reports a run that already ended at once.waithas no timeout and never wakes the agent; Ctrl-C only stops waiting. - When the run fails, the prompt is dropped by a shutdown, or the agent stops or crashes,
waitexits 1 with the reason. pi-dock logs <name>shows lifecycle and assistant-turn events (queued,run,turn,text,done, …); tool calls and the full conversation are in the session file.--followkeeps watching, and--rawprints the stored NDJSON.
Long prompts are easier with --file prompt.md. Prompts run one at a time, in order.
Models and thinking
--model provider/idtakes a ref frompi-dock models. Without it, Pi's default model is used. An unknown model (the error suggests matches) or one without credentials fails before anything is created.--thinking off|minimal|low|medium|high|xhigh|maxis applied on every wake. Without it, Pi restores the session's saved level or uses its configured default; levels the model does not support are clamped by Pi.pi-dock setchanges model, thinking, system prompt, tools and extension flags, only while the agent is powered off (stopped or failed); the next wake applies them. Runstop,setandstartone after another, with no other command on that agent in between:setchecks that the agent is off and then rewrites its manifest, so astartin between runs the old configuration until the next restart (nothing is lost), and twosets at once can each undo the other's change.
System prompt and tools
Each agent can have its own role and tool set, instead of a per-folder .pi/SYSTEM.md or
defaultTools:
pi-dock spawn --name reader --system-prompt ./reviewer.md --tools read,grep
pi-dock spawn --name thinker --system-prompt ./thinker.md --no-tools
--system-prompt <file>replaces Pi's base prompt, aspi --system-promptdoes, and wins over.pi/SYSTEM.mdin the agent's directory. What Pi adds to the base prompt still applies:APPEND_SYSTEM.md,AGENTS.mdcontext and the working directory. Skills follow Pi's usual rule: they are listed only when the agent's tools includereadorbash. The path is relative to where you run the command.spawnandsetrefuse a file that cannot be read or is blank before they start or change anything. The runner reads the file again; if it has become unreadable or blank in between, startup fails with the reason in the log, and no manifest is written, so no agent exists.- The agent keeps a copy of the content, not the path: editing or deleting the file changes
nothing. To change the prompt,
stop,set --system-prompt <file>,start. --tools a,benables exactly these tools, built-in or from extensions, and replaces Pi's defaults (read, bash, edit, write), so list every tool the agent needs. Names are exact:*,+nameand-nameare refused.--no-toolsenables none. The list applies on every wake, whatever tools the session used before.- An unknown name, or the tool of an extension that is not installed, fails startup before a
manifest is written, so a spawn leaves no agent, only its log. The handshake
failure shows
tools not found: …in the last log line.setchecks only the syntax, so after asetthe next wake fails the same way. - It is not a sandbox. Extensions whose tools are not listed stay loaded and keep running:
pi-link stays connected without its
link_*tools, so namelink_sendand the others if the agent should reply. pi-dock never adds tools to the list. - pi-dock does not load Pi's built-in MCP and codemode extensions, and
--toolsinstalls nothing:codemodeormcp__…names fail unless an extension provides them. A list withoutmcp__entries leaves the MCP tools extensions register reachable indirectly (through codemode or tool search). A list that names anymcp__tool keeps only the MCP tools it names, and--no-toolsremoves them all. showprintssystem-prompt <N> bytesand the tools (noneafter--no-tools,-when unset), never the prompt itself.setreplaces either one; going back to Pi's defaults is not possible yet.
Extension flags and pi-link
--x key[=value] passes a flag to the extensions the agent loads, the same as --key value on
the pi command line. pi-dock does not interpret them, and they do nothing without the
extension that defines them. set --x replaces the whole list. A flag that no loaded extension
defines, or a value flag given without its value, is not applied: each boot logs it as a
diagnostic event, and the agent runs without it.
With pi-link installed in Pi, this spawns an agent that joins the link and requests the link
name reviewer (pi-link may suffix a taken name, e.g. reviewer-2):
pi-dock spawn --name reviewer --x link --x link-name=reviewer
Messages from other terminals then reach the agent as they reach any pi-link terminal. Their turns appear in the agent's log, and they fall under the "no limits" warning above.
Compaction
pi-dock compact <name> [instructions] compacts the agent's session. The agent must be idle
(no run, no queued prompts); a stopped agent is woken first and stays on. It waits without a
timeout for the result and exits 1 with the reason when compaction fails (e.g.
Nothing to compact (session too small)). The log records the outcome of every compaction
(compacted, compact_failed or compact_cancelled), including those an extension or Pi
itself starts.
Recovery
agent <name> is not responding: a request to the agent timed out. Find the latestspawned pid=<pid>line inpi-dock logs <name>, terminate that process, then runpi-dock start <name>.agent <name> did not exit within 5s; terminate PID <pid> externally:stopasked the agent to exit and it did not, e.g. because an extension'ssession_shutdownhandler, which the agent waits for as Pi does on quit, never finished. Check that the PID's command line containsrunner.mjs --name <name>(PIDs are reused), terminate that process, and confirm it has exited before starting the agent again or deleting its files: while it shuts down,pi-dock lscan showfailedandpi-dock stopcan printalready stoppedoralready failed.handshake failed for <name>: a spawned or woken agent's runner exited with an error, or the agent did not answer within 20 s. The printed diagnostics include the last log line, if any.lock: ~/.pi/dock/<name>.sock.lock exists(Unix, after a failed handshake): a runner holds this directory while it takes the agent's socket, and removes it once it listens. If it remains, a runner is acquiring the socket right now, or one was killed while it did or could not remove it; other runners never remove it, and until it is removed the agent cannot start. Make sure no runner of that agent is left (no process whose command line containsrunner.mjs --name <name>), then remove the directory (rmdir) and runpi-dock start <name>.
[!CAUTION] PIDs are reused by the operating system. Before terminating one, make sure it is still that agent's process: its command line contains
runner.mjs --name <name>.
Recipes
pi-dock has no limits and no notion of a team being finished; both belong to the application.
These two small Node scripts cover them. Node is already required by pi-dock and behaves the same
on Windows and Unix, where a shell script would not. Both call pi-dock from your PATH.
Watchdog for unattended runs
Before spawning anything, write the roster: the exact names the run will use. Never stop by pattern; a pattern can match agents that are not part of the run.
printf '%s\n' lead coder reviewer > roster.txt
In PowerShell: Set-Content roster.txt lead, coder, reviewer.
watchdog.mjs waits for an absolute deadline, then stops every agent in the roster and records
each result in watchdog.log:
// node watchdog.mjs <ISO deadline> <roster file>
import { spawnSync } from 'node:child_process';
import { appendFileSync, readFileSync } from 'node:fs';
const [deadlineText, rosterFile] = process.argv.slice(2);
const deadline = Date.parse(deadlineText);
const roster = readFileSync(rosterFile, 'utf8').split(/\s+/).filter(Boolean);
const badName = roster.find((name) => !/^[a-z0-9._-]+$/.test(name));
if (Number.isNaN(deadline) || roster.length === 0 || badName !== undefined) {
throw new Error('usage: node watchdog.mjs <ISO deadline> <roster file of agent names>');
}
const record = (text) => appendFileSync('watchdog.log', `${new Date().toISOString()} ${text}\n`);
record(`armed until ${new Date(deadline).toISOString()} for ${roster.join(' ')}`);
// Poll the wall clock: one long timer cannot exceed ~24.8 days and may not count host sleep.
while (Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, Math.min(60_000, deadline - Date.now())));
}
// One stop per name, so a name that was never spawned cannot keep the others running.
let failed = false;
for (const name of roster) {
const stop = spawnSync(`pi-dock stop ${name}`, { shell: true, encoding: 'utf8' });
const output = stop.error?.message ?? `${stop.stdout}${stop.stderr}`.trim();
record(`${name}: exit ${stop.status}: ${output}`);
failed ||= stop.status !== 0;
}
process.exitCode = failed ? 1 : 0;
Start it as its own process before launching the team, so it does not depend on the orchestrator:
nohup node watchdog.mjs 2026-10-02T18:00:00Z roster.txt >/dev/null 2>&1 &
In PowerShell:
Start-Process node -WindowStyle Hidden -ArgumentList watchdog.mjs, 2026-10-02T18:00:00Z, roster.txt
The watchdog never kills a process: PIDs are reused, so a failed stop is for a person to resolve
with Recovery, using watchdog.log. Know its limits:
- A stop can be slow (up to about 8 s for an agent that misbehaves) or fail.
- On the same machine it cannot keep the deadline if the host sleeps, hibernates or crashes, and it is itself a process that can be killed.
- A run that finishes early can stop its own agents; at the deadline the watchdog then records
already stopped.
Observer for team completion
done and idle do not mean a team has finished: pi-link messages can start more work at any
time. Agree on a final marker with the lead instead, fresh for every mission, e.g. "when, and
only when, the result is complete, end your reply with the line TEAM-DONE-m42". observe.mjs
polls the lead's raw log every 15 s until a text event ends with that line. A marker only
mentioned mid-reply does not count. Because the marker is unique to the mission, the observer
can start, or restart, at any time, even after the lead has finished:
// node observe.mjs <lead> <marker>
import { execSync } from 'node:child_process';
const [lead, marker] = process.argv.slice(2);
if (!/^[a-z0-9._-]+$/.test(lead ?? '') || !marker) {
throw new Error('usage: node observe.mjs <lead agent> <marker>');
}
// The whole log is read each time, so it is not capped (Node's default is 1 MiB of output).
const read = () => execSync(`pi-dock logs ${lead} --raw`, { encoding: 'utf8', maxBuffer: Infinity })
.split('\n').filter(Boolean).map((line) => JSON.parse(line));
for (;;) {
const events = read();
const final = events.find((event) => event.event === 'text'
&& event.text.trimEnd().split(/\r?\n/).at(-1).trim() === marker);
if (final) {
console.log(final.text);
break;
}
const external = events.filter((event) => event.event === 'external').length;
console.error(`${new Date().toISOString()} waiting; ${external} extension messages logged`);
await new Promise((resolve) => setTimeout(resolve, 15_000));
}
The marker is the team's claim, not proof: check the artifact before you trust it (here
npm test stands for your own check), then stop the roster.
node observe.mjs lead TEAM-DONE-m42 && npm test && pi-dock stop $(cat roster.txt)
In Windows PowerShell 5.1, which has no &&, check $? as well as the exit code: a command
that cannot run at all (e.g. npm not found) leaves $LASTEXITCODE at the previous 0.
node observe.mjs lead TEAM-DONE-m42
if ($? -and $LASTEXITCODE -eq 0) {
npm test
if ($? -and $LASTEXITCODE -eq 0) { pi-dock stop (Get-Content roster.txt) }
}
The external count is only a sign of activity: it counts extension messages (for pi-link,
external type=link) that entered the lead's context, not messages the lead acted on. The
whole log is read on every poll, which costs memory for a very long-lived lead.
For AI agents
pi-dock skill prints the pi-dock operating guide: workflow, prompt ids and wait outcomes,
states and waking, changing agents, reading logs, recovery and the warnings. --help ends by
telling AI agents to read it.
Installed as a Pi package (see Install), the same guide is the pi-dock skill, so Pi
agents load it on their own when a task involves pi-dock. For other AI tools, have them run
pi-dock skill first.
Data locations
| What | Where |
|---|---|
| Manifest (identity and configuration) | ~/.pi/dock/<name>.json |
| Event log (NDJSON) | ~/.pi/dock/<name>.log |
| Control pipe | \\.\pipe\pi-dock-<name> on Windows, ~/.pi/dock/<name>.sock elsewhere |
| Session | Pi's session directory, <agent dir>/sessions/--<cwd>--/; pi-dock show <name> prints the exact file |
Names are lowercase a-z0-9 segments joined by single ., _ or -, at most 64 characters,
and not a Windows device name such as con or nul.
Opening an agent's session in Pi
The session is an ordinary Pi session, so Pi opens it by the path pi-dock show <name> prints:
pi --fork <session file> # a copy, as a new session in the current directory
pi --session <session file> # the agent's own conversation; stop the agent first
--forkleaves the agent untouched and needs no stop: use it to read the conversation or continue it separately.--sessioncontinues the agent's own session, in the agent's directory. Stop the agent first and don't wake it until you quit Pi: while both run, they append to the same file without seeing each other's entries.pi --continueorpi --resumein the agent's directory can pick its session too.- What you add in Pi becomes part of the agent's memory: the next wake continues from there.
Pi runs the session with its own prompt, tools and extensions; the agent's next wake applies
its manifest again, and what the manifest does not set (e.g. thinking without
--thinking) follows the session, as on any wake.
Deleting an agent
To remove an agent by hand:
pi-dock show w1 # note the session file, if you also want the conversation gone
pi-dock stop w1 # must print "stopped", "already stopped" or "already failed"
rm ~/.pi/dock/w1.json ~/.pi/dock/w1.log
rm <session file> # optional
Stop it first: without its manifest pi-dock no longer knows the agent, so it cannot stop a
runner that is still on. If stop reported did not exit within 5s, follow
Recovery first and confirm the runner process has exited. Remove the log too: a
new agent with the same name would otherwise append to the old one. On Unix, also remove
~/.pi/dock/w1.sock if it is there.
Status and limitations
- Version 0.5.2.
- Windows is verified: the regression suite on every change, and the paid end-to-end smoke test against a real model. Unix (sockets, signals) is little tested: on Linux (WSL) only the regression's socket acquisition cases pass, and the suite does not yet run through; the smoke test has not been run on Linux, macOS or Android (Termux).
- Lockfile:
package-lock.jsonreproduces this checkout's dependencies. It does not pin the transitive dependencies of anyone who installs pi-dock some other way.