Pi Background Processes

Add a background-process extension to pi whose processes can wake the agent, survive session replacement, and never outlive pi.

Background processes for pi: start them, page their logs, let them wake the agent

Where this comes from

Our team runs pi as the coding agent for a monorepo where almost every task needs a long-lived process on the side: a dev server, a test watcher, a local API, or a build that takes minutes. Today the built-in bash tool blocks until the command exits. When the model works around that with nohup ... &, the session either hangs or leaves daemons behind after pi exits. Just as often the model starts a dev server with plain bash, the server prints its banner and goes quiet, and the whole session sits there waiting. We want a proper background-process extension, and we want a process to be able to wake the agent up instead of the agent polling.

What we are asking for

A pi extension at packages/coding-agent/examples/extensions/background-processes/index.ts in this checkout of pi (@earendil-works/pi-coding-agent 0.85.1, Node 22). The file must export default function (pi: ExtensionAPI) and use only the public extension API; do not modify pi core. Keep the layout conventions of the neighbouring examples so it loads with pi -e or through DefaultResourceLoader. Ship a short README.md next to it describing the tools and the wake rules, and a unit test file under packages/coding-agent/test/ that runs offline with npm test -w packages/coding-agent. The existing coding-agent suite must keep passing unchanged; do not edit existing test files. Leave pi's build and test configuration (package manifests, tsconfig*.json, vitest.config.ts and the like) unchanged; the verifier runs the suites with it.

The interface we already agreed on

We have aligned with the harness team on tool names, parameters and result shapes, so please keep these exactly. Every tool returns a JSON object: serialize it as the tool result text and attach the same object as the result details. The one exception is bash on its normal path, described last.

  • bg_run — { command: string; cwd?: string; name?: string; wake?: { ready?: string; error?: string }; cleanup?: { command: string; timeoutSec?: number } }. Starts command through the configured shell and comes back at once with the process record (see below), including a fresh id. The command and everything it spawns can later be stopped as one unit, and nothing about it may hold up pi's own input, output or exit. A relative cwd resolves against the session's working directory, and the record's cwd is the resolved absolute path. wake.ready and wake.error are regular expressions matched against each output line.
  • bg_logs — { id: string; offset?: number; limit?: number }. Returns { id, offset, lines, total }: a page of the process's combined stdout/stderr lines in order, the line offset actually served, and the total line count so far. A page never exceeds 64 KB of text; if limit is omitted, serve the newest lines that fit.
  • bg_list — {}. Returns { processes }, every process record visible to this session, running and finished.
  • bg_kill — { id: string; timeoutSec?: number }. Stops the process and all of its descendants: SIGTERM first, then SIGKILL after timeoutSec (default 5) to whatever is still alive. If the process was started with cleanup, run the cleanup command once nothing of the process tree is left, with the same working directory, bounded by cleanup.timeoutSec (default 30); its output is appended to the same log. Returns the final process record plus cleanup: { ran, exitCode, failed } | null; a failed or timed-out cleanup is reported as failed: true, never swallowed.
  • bg_watch — { id: string; ready?: string | null; error?: string | null; exit?: boolean; output?: boolean }. Changes the wake rules of a running process and returns its record. null removes a pattern; exit: false silences the exit wake; output: false silences the output wake of an auto-backgrounded command (below).
  • bash — the session's bash tool must accept { command: string; timeout?: number; stalledSec?: number } and behave as follows. The command runs in the foreground exactly as the built-in tool runs it, until one of two things happens first:
    • It finishes (exits, hits timeout, or is aborted) before going silent for stalledSec seconds (default 30): return exactly what the built-in tool would have returned: the same result text, the same truncation, the same tool-error texts for a non-zero exit, a timeout and an abort. A timeout shorter than stalledSec therefore wins. No process record is created and nothing wakes the agent.
    • It produces no stdout/stderr bytes for stalledSec seconds while still running: it becomes a managed background process. The tool returns within one second of the threshold, as a normal (not an error) tool result, with the process record as JSON text and details, with backgrounded: true and the stalledSec that applied. Everything the command printed before the switch is the head of its log. From then on the process is exactly like one started with bg_run: it is listed, its logs page, bg_kill and bg_watch apply, and the lifecycle expectations below hold.

A process record contains at least id, name, command, cwd, pid, state (running | exited | killed), exitCode (number or null), startedAt and endedAt (ISO-8601 strings; endedAt is null while running), logLines (total lines so far), and backgrounded (true only for a command that bash moved to the background).

How a process wakes the agent

The wake is a custom message whose customType is "background-process". Its details must contain { id, name, reasons, exitCode, matchedLine }, where reasons is a non-empty array drawn from "ready", "error", "output", "exit", exitCode is a number or null, and matchedLine is the first output line that triggered the wake or null. The message content is free-form text for the model.

This is what we expect to see when we try it:

  • By default only process exit wakes the agent. ready and error wake only when the corresponding pattern was supplied. output applies only to a command that bash moved to the background: the first complete output line the command prints after the switch wakes the agent, with that line as matchedLine.
  • ready and output wake at most once per process. error wakes at most once per delivery window. exit wakes exactly once, when the process leaves the running state, whether it exited on its own or was killed. bg_kill does not suppress it: a killed process delivers its single exit wake (with state: "killed") like any other exit. Nothing wakes the agent for a process after its exit wake has been delivered, and nothing wakes it for a reason that bg_watch silenced.
  • While the agent is busy, the wake is delivered after the current assistant turn has finished executing its tool calls and before the next model request. While the agent is idle, the wake starts a new turn immediately.
  • Several reasons that fire for the same process while the agent is busy may be combined into one message whose reasons lists each of them once, or delivered as separate messages; either way each reason is reported at most once per delivery window. Several processes firing in the same window are reported in the order in which each process first fired.
  • The wake goes to the session that started the process if it is still the active session in this pi process; otherwise to the current active session.

Lifecycle, the part that bit us before

  • Processes belong to the running pi process, not to a session. /reload, /new, /fork, and tree navigation must not stop them, and any session in the same pi process can list, read, and kill them.
  • When the pi process exits, normally or on SIGTERM/SIGINT, every managed process must be stopped with the same SIGTERM-then-SIGKILL sequence, including children the command spawned itself. No managed process may outlive pi. A SIGTERM or SIGINT delivered to the pi process, interactive or headless, must still end that process once its managed processes are stopped: pi exits by the signal or with a non-zero status, and never keeps running.
  • Every process's log and final record must be durable on disk inside the session's storage directory. A later pi process that resumes the same session must be able to bg_list the finished records and bg_logs their output; those records report state as exited or killed with the recorded exitCode, never running.
  • bg_logs shows a running process's output as it arrives, and pi's memory must not grow with the volume of output: a process that prints hundreds of megabytes must not grow pi's heap accordingly, and bg_logs must still page through all of it.