Optional extensions and a theme for Pi: a richer footer, usage-limit awareness for the agent, activity and timing indicators, background shell jobs, background subagents on the model of your choice, bounded shell execution, Kagi subscription search, free US government and legal search, local voice dictation, and clearer tool rows.
Requires Pi 1.4.0 or newer, Node 22.18 or newer, npm and Git.
pi-extras follows Pi's latest release; after updating pi-extras, update Pi too.
The shell-job extension currently targets macOS and Linux. A job runs in the
shell of Pi's bash tool, with its shellPath and shellCommandPrefix settings.
Windows process-group behavior has not been validated.
pi install git:github.com/jaedync/pi-extrasRestart Pi after installation. Use pi config to select extensions. Installing
adds all twenty-three extensions (quota hibernation, computer use and auto-update stay off until you opt in); it makes quiet available but does not select it. It also adds the skills be-ambitious, code-slop-rules, formal-verification, implicit-state-machines, ui-redline, untangle-codebase and walkthrough-video and the prompt templates /higher and /wider.
Choose the theme using /settings. Use only one custom footer at a time.
Phase Spinner wraps an existing editor where possible; other editor extensions
can still conflict.
| Component | Behavior |
|---|---|
| Status Plus | Usage/cost grid, context and cache indicators, per-provider limits in a fixed order, $0.00 rows for free local providers, optional linked subagent usage |
| Usage Guard | usage tool, /usage command, one-shot warnings at a session budget (a warning point, not a stop) or, when enabled, near a limit |
| Cache Compaction | Prefix-sharing summaries that reuse the session's warm prompt cache, with safe fallback to Pi's default compaction |
| Rate-limit Recovery | /rate-limit-recovery, bounded backoff for short rate limits and connection failures, opt-in main-session hibernation for provider cooldowns; subagents fail fast on quotas with reset guidance |
| Phase Spinner | Descriptive status with a per-mode spinner in the editor's top divider, with a per-step stopwatch, alongside tokens/sec, time to first token and total elapsed time; live thinking above queued messages and a π end line when a prompt finishes |
| Tab Status | Pi's state in the terminal tab: an iTerm2 status dot and detail, and tab progress in iTerm2, Ghostty, WezTerm and Windows Terminal |
| Shell Jobs | shell_job_start, shell_job, /jobs, bounded logs and completion notifications; jobs are named after their titles |
| Subagents | subagent, message and stop_subagent tools, /subagents: background child agents on your scoped models that report back, talk to main and to each other, each with a live row above the editor |
| Bash Default Timeout | Adds a 120-second timeout only when a bash or powershell call omitted one |
| Idle GC | Once the agent has been idle for 30 seconds, one full garbage collection gives the memory of the last run back to the system |
| Loop Stall | Logs each time Pi stops answering for 500 ms or more, with what held it: main-thread CPU, GC, slow file writes and disk pressure |
| Defer Tools | Opt-in: requests to a local Meridian mark the tools you name defer_loading, so their declarations leave the prompt until the model searches for them |
| Herdr Tab Name | In a Herdr tab, keeps the session name and the tab label the same: /name renames the tab, and a session with no name takes the tab's label |
| Kagi Search | Adds kagi_search without replacing existing search/fetch tools |
| Gov Search | gov_search tool: free search of the Firecrawl Government & Legal Index (US statutes, regulations, city codes, court opinions, agency guidance), with filters for jurisdiction, court and citation; no key needed |
| Voice | Hold or tap ctrl+space to dictate into the editor, transcribed on this machine |
| Computer Use | Opt-in, macOS: a computer_use tool that operates Mac apps through OpenAI's Computer Use, installed by the ChatGPT app |
| Tool Display | Every tool row as a colored header band with live progress and a popup with the whole call, other extensions' tools included; chained bash commands broken into steps; every finished row stays visible, ctrl+o expands them all |
| Pull Link | pull_link tool: give it a link to read posts with their threads, replies and images (X, Bluesky, Threads, Mastodon, Reddit, Hacker News, LinkedIn), GitHub pages, images, any web page, and videos with transcript, thumbnail, comments and frames on request (YouTube, TikTok, Instagram, Vimeo and more); the links in your messages are read before the model starts (/autopull) |
| Copy Blocks | Code blocks and quotes in replies drawn on a background of their own with a copy label: one click copies the exact text; /copy-block does it from the keyboard |
| Release Notes | What changed in pi-extras, shown one time in the first new session after an update; /pi-extras changelog shows it again |
| Auto-update | Opt-in: Pi and your unpinned packages update by themselves. Each new set is tested before use, and running sessions move to it when they are idle; /pi-extras update |
| Quiet | Low-contrast theme with restrained accent colors |
| Be Ambitious | Skill: the model plans the deeper version of a task that a field-leading expert would do (raw material, expert instruments, a hard target, a check from outside the model), shows it and waits for your approval, then does the work at that depth. It also looks at the goal behind the task and at methods from other fields. The model loads it for open tasks by itself; /skill:be-ambitious [task] starts it. From Felix Rieseberg's How to be more ambitious |
| Code Slop Rules | Skill: the model turns the sloppy code patterns that a repository really has (swallowed errors, .catch(() => {}), as any, as never, as unknown as, non-null !, ts-ignore and eslint-disable comments, "for now" comments, fixed sleeps, empty default branches, except: pass, # type: ignore, # noqa) into ast-grep rules with rule tests. slop.mjs measure counts each catalog rule's sites in source and tests, scans fix commits before and after the fix (with their Found-by trailers), and skips what a configured linter already enforces; slop.mjs adopt copies the chosen rules into the repository; slop.mjs ratchet fails when a rule's site count goes above its recorded baseline, so old sites do not block work but new ones do. pi-extras runs its own ratchet in npm test (npm run lint:slop) |
| Formal Verification | Skill: the model checks the part of a codebase where a bug costs the most and example tests reach the least (races, retries, at-most-once delivery, reload and restart hand-over). It writes a Quint model with each action cited to code, and TLC checks every order of events within stated bounds. Each property must fail on a mutant that copies a code change. A conformance test replays model traces against the real code. fv.mjs installs Quint and a Java runtime at user scope, runs every check of the record, writes the traces, and lists covered code that changed after the last sync. Findings stay findings. /skill:formal-verification [area]. From the goals of Lauren Tan's MIT pstack skill create-formal-verification-skill |
| Implicit State Machines | Skill: the model finds the implicit state machines in a codebase: state kept in loose flags, mode values, or nullable fields and timestamps that imply phases, with transitions spread over many handlers. Read-only scouts read the source in batches in parallel. Then strong evaluators, one for each area, check each finding in the code and sort it: needs design thought, quick fix, enum candidate, or fine as it is. The report is a summary list and a detail document. It changes no code; /skill:implicit-state-machines |
| UI Redline | Skill: the model finds and fixes the signs of generated-looking UI in a web screen (hue gradients, glows, gradient text, emoji icons, tracked caps, hype copy, off-grid spacing, low contrast, looping animation, missing focus states). redline audit renders the page in Chromium at desktop and phone size, measures computed styles, layout and keyboard focus against 41 rules, gives a slop score (signs of generated UI) and a craft score (contrast, spacing, sizes, focus), and saves screenshots with numbered redline boxes; redline compare fails when a high finding is left or a rule got worse; redline scan finds the same signs in Tailwind and CSS source. The model writes a design brief from the product, waits for your answer, fixes the screen in passes, measures again and gets a review from a fresh reviewer. A .redline.json turns rules off or changes them for a brand. Runs locally after one node skills/ui-redline/scripts/redline.mjs setup |
| Untangle Codebase | Skill: the model refactors a codebase, or one area of it, so that pieces that change together sit together and pieces that do not are not tied. It lists every hidden link (copies, shared globals, conventions callers must know), needless link (internals used by callers, stable code that imports volatile code), surplus piece (pass-through wrappers, dead code) and special case with file:line, redesigns from first principles, and fixes in small tested commits on a new branch that you review. untangle.mjs graph finds import cycles, imports of more volatile code and files that change together in git with no import; untangle.mjs special-cases counts swallowed errors, retries and sleeps, disabled tests and turned-off checks as the progress number; untangle.mjs skeleton prints the code without function bodies. Optional passes remove comments that restate the code and tests that cannot catch a bug (each removal with a mutation probe). Fresh reviewers check each round until a round finds no new high issue. Only /skill:untangle-codebase [area] starts it. From Maze's untangle-codebase |
| Walkthrough Video | Skill: the model makes a launch film or a walkthrough of a web app. It captures the real app frame by frame and logs each action, plans a storyboard with the product's own look, then builds the shots, camera moves, cursor, captions, sound and an optional local voiceover (Kokoro). It renders with HyperFrames and checks the result with 12 measured gates (framing, text size, captions over controls, dead time, camera churn, smoothness, voice fit, blank frames, sound level and more) before delivery. Runs locally after one node skills/walkthrough-video/scripts/wv.mjs setup; an opt-in wv setup --music builds a sampled instrument kit (about 730 MB) from pinned sources. The tool never loads a script or a package from the network at render time |
| Prompts | /higher and /wider [task]: before work starts, the model plans the outcome behind the task, or the other fields that can improve it. Then it shows a new prompt and waits for your approval |
pi update --extensions
# Or update Pi and packages together:
pi update --all
pi remove git:github.com/jaedync/pi-extrasAn unpinned Git install follows upstream commits. Pi can detect available package
updates. This package does not update itself unless you turn on
Auto-update, and it adds no background service.
Only the Pull Link media tools update themselves (see
Video tools); PI_OFFLINE=1 stops that. The Pull Link media
tools install on the first video link. Voice installs when Pi finds a
microphone. To pin a published tag, append it to the Git source, for example
pi install git:github.com/jaedync/pi-extras@v0.24.14.
Pinned installs require an explicit installation of a newer tag.
After an update, restart Pi or use /reload; stop active jobs before removing
the package. Removing it does not remove your credentials or change other packages.
Auto-update is off by default. When it is on, pi-extras keeps Pi and your unpinned packages up to date. It tests each new set of versions before a session uses it. Linux and macOS only (macOS is not fully tested).
To turn it on, run this command in a shell:
node ~/.pi/agent/git/github.com/jaedync/pi-extras/lib/auto-update/cli.ts enableUse the path of your pi-extras install. enable prints each file that it
changes. Then start Pi again.
To turn it off, run the same command with disable. disable puts back
~/.local/bin/pi and the original packages entries in settings.json.
It also works when the store is gone: enable keeps a copy of the
original entries in ~/.pi/agent/pi-extras-update-undo.json.
After disable, Pi and your packages are the versions that you had before
enable. These can be older than the versions in the store. To get the
newest versions, run pi update --all.
- Linux or macOS, npm and Git.
~/.local/bin/pimust be the link thatnpm install -gmakes for Pi (npm prefix~/.local). If it is a different file,enablestops and changes nothing.
- Every 5 minutes, one check runs on the machine, not one for each session.
It finds the newest Pi on npm, the newest
v*release tag of each Git package, and the newest version of each npm package. - It installs each new version into its own folder in the store
(
~/.local/share/pi-extras/updates). It never changes an installed folder. - It starts the new set in a test Pi that uses no account and no network. If an extension does not load or fails at startup, the set is not used.
- When a set with several new versions fails, the check tests each new
version alone, and then the set without each failed version. Thus it
finds the version that caused the failure, also when two new versions
need each other (for example a new Pi and a pi-extras release that needs
it). That version is marked bad and is not tried again until you run
/pi-extras update retry. The other new versions switch in the same check. - A single new version that fails gets one more test before it is marked bad.
- If each new version passes alone, the check tests the full set one more time. If it passes, the set is used. If it fails again, all its new versions are marked bad.
- If a test gives no result (no reply in 90 seconds, for example on a busy machine, Pi did not start, a signal from outside such as the out-of-memory killer stopped the test Pi, or the check stopped at its 10-minute limit), the versions in that test are suspect. After 2 such checks in a row, the checks skip them for 24 hours and then try them again. A suspect version is not marked bad: no fault was found.
- When you remove a package from
settings.json, the next check stops updating it without a test. - If the test passes, it changes the
pi-currentlink in the store and thepackagesentries insettings.jsonto the new folders. - Each running session moves to the new set when it is idle for 30 seconds.
For package changes, the session reloads. For a new Pi, a terminal
session restarts with
pi --session <file>. An RPC session, or a Pi that the launcher did not start, shows "Pi X ready: restart to use it". - A session does not move while the agent runs, a message is queued, a
compaction runs, a dialog or a selector (for example
/modelor/settings) is open, a!command runs, a subagent runs a turn, or the editor has text. A restart also waits for subagents to end. A reload keeps them. Shell jobs do not stop a restart: the restarted Pi takes them over (see Shell Jobs). - A restart starts Pi again with the options that are safe to repeat (for
example
--model,--thinking,--tools,-eand--append-system-prompt). It does not repeat session options and messages. If Pi started with a different option (for example--api-keyor an extension flag), the session does not restart. It shows "Pi X ready: restart to use it". - The status line shows
updating: ...while a check installs or tests. After the session moves, the transcript shows one line with the old and the new versions and when the move happened, for exampleUpdated pi-extras 0.30.0 → 0.31.0 · 2:05 PM. A line from another day shows the date (Oct 26) in place of the time. When pi-extras changed, expand the line (ctrl+o) to read the notes of each version between the two. - A reload waits for each extension to end its shutdown step. If one
step does not end, the reload stops there. After 60 seconds, auto-update
writes a line in
update.logand shows a warning. If the launcher started Pi in a terminal, Pi then restarts with the same session. If Pi does not stop in 30 more seconds, auto-update stops the process, and the launcher starts Pi again. In other cases (no launcher, RPC mode, or a session with no file), the warning tells you to quit Pi and start it again. - If a new set waits 30 minutes for the session to become idle, the status line shows "update ready: waits for idle (/reload to use it)". For a new Pi, it shows "restart to use it".
- One store serves one agent directory: the one that
enableused. A check with a differentPI_CODING_AGENT_DIRchanges nothing and records an error.
- Managed: unpinned
npm:entries and unpinned Git entries (for examplegit:github.com/jaedync/pi-extras). It keeps the filters of an entry (extensions,skillsand others). - Not managed: pinned entries (
npm:name@1.2.3,git:...@v1.0.0) and local paths. - If you add a new unpinned entry, the next check manages it.
enable replaces ~/.local/bin/pi with a small shell script. The script:
- starts Pi from the store, with all your arguments;
- keeps a lease on the Pi folder that it started while Pi runs, so the cleanup does not remove that folder. This also works for a Pi that does not load pi-extras;
- stops after its Pi when it gets SIGTERM or SIGHUP, and then does not start Pi again;
- starts Pi again after a session asks for a restart (at most 3 times in 60 seconds). After 2 restarts that did not bring the new set, the session stops and shows a notice. A restart occurs only when Pi stopped with exit code 0 and wrote its request with the token of that start;
- changes
pi updateto an update check that runs in the background with a low priority. The command stops at once. Use/pi-extras updateto see the result; - turns off Pi's own "New version" message while auto-update manages Pi.
With
"pi": "skip", the message stays.
Do not run npm install -g for Pi while auto-update is on. It writes over
the launcher. Run disable first. If the launcher is gone, the next check
records the problem and each session shows a warning. Run enable again
to put the launcher back, or run disable.
| Command | What it does |
|---|---|
/pi-extras update |
Shows the state: on or off, the current set, newer versions, each bad version with its date and reason, the last check, the last error, and what this session waits for |
/pi-extras update now |
Starts a check now |
/pi-extras update retry |
Removes all bad and suspect marks, and then starts a check now. Use it when a version was marked bad by mistake |
/pi-extras update rollback |
Goes back to the previous set. The newer versions are marked bad |
/pi-extras update pause |
Stops the checks. Sessions still move to a set that is ready |
/pi-extras update resume |
Starts the checks again |
The shell command node <pi-extras>/lib/auto-update/cli.ts has the
subcommands check [--force], status, retry, rollback, gc, enable and
disable. Only the shell command can do enable and disable.
Put these in the autoUpdate section of ~/.pi/agent/pi-extras.json:
| Setting | Default | What it does |
|---|---|---|
enabled |
false |
enable and disable set it |
intervalMinutes |
5 |
Minutes between checks (1 or more) |
minReleaseAgeHours |
0 |
Use only npm releases (Pi and npm packages) that are at least this old. Git tags have no age check. For npm packages, npm's own min-release-age (in days) applies too, and the larger value wins. The Pi install ignores npm's setting, as pi update does |
adopt |
"auto" |
"notice": do not reload or restart; show a notice instead |
paused |
false |
No checks |
sources |
{} |
For each source as settings.json names it: "tags" (Git: newest v* tag), "head" (Git: newest commit of the default branch) or "skip" (do not update it) |
pi |
"latest" |
"skip": do not update Pi |
prereleases |
false |
Git: use pre-release tags too (for example v1.0.0-rc.1) |
PI_OFFLINE=1 stops the checks. PI_EXTRAS_UPDATE_STORE sets a different
store folder. The log is update.log in the store.
Old versions stay in the store while a running session uses them. A version that no set and no session uses is removed after 1 hour.
STATUSLINE_TZ: optional IANA timezone for footer clock/reset labels; defaults to the system timezone. An unknown name also gives the system timezone, and Status Plus writes a note instatus-plus.log. If the system timezone is not known (for example, withTZ=""), times use UTC.STATUS_PLUS_POLL_LIMITS=0: disable authenticated quota polling while retaining recorded usage and response-header limits.PI_OFFLINEalso suppresses polling. As in Pi, onlyPI_OFFLINE=1,trueoryesturns offline mode on. Pi processes that share an agent directory poll each provider one time between them. They share the result inPI_CODING_AGENT_DIR/status-plus-limits/, so a new session shows the last budget immediately.PI_EXTRAS_USAGE_GUARD=1or0: turn band warnings on or off for one run. See below for the persistent setting.PI_CODING_AGENT_DIR/pi-extras.json(default~/.pi/agent/pi-extras.json): persistent Usage Guard settings underusageGuard, written by/usage warnings on|off. Keys:enabled(defaultfalse),bands(default[90, 95]),resumeMarginSeconds(default180),proximityPct(default10),maxWaitSeconds(default18000, five hours).PI_RATE_LIMIT_RECOVERY=on|off: override automatic quota waiting for one run.rateLimitRecoveryinpi-extras.json:autoWait(defaultfalse),resumeMarginSeconds(default10,0to3600),maxWaitSeconds(default and hard maximum18000, five hours total per user-started run),maxRecoveries(default and maximum10),anthropicFirstEventSeconds(default45,10to600,0disables the Anthropic stall retry),transientMaxWaitSeconds(default180,10to900,0leaves short rate limits and connection failures to Pi's own retry),localRetry(defaulttrue: a local server is waited for with no limit),localProviders(default[]: provider names that count as local). A number out of range moves to the nearest limit. A negative number, or a value that is not a number, uses the default. A count with a fraction rounds down./rate-limit-recovery on|offsaves onlyautoWait; commands take precedence for the current session.PI_RATE_LIMIT_RECOVERY_ROLE=subagentmarks external child sessions as detection-only.PI_SUBAGENTS=off: disable Subagents.subagentsinpi-extras.json, all optional:defaultModel(a model or short name; default: main's model at the time of the call),maxConcurrent(default4),batchMs(default2000),groupWaitMs(default60000),replyTimeoutMs(default600000),maxRunMinutes(default60, at most1440) andmaxRunCost(US dollars, defaultnullfor no limit), the budget of each run of a child,childToolsExclude(tool names children never get) andresumePolicy("reload", the default, resumes children that a/reloadinterrupts;"always"also allows one automatic attempt after a restart or crash;"notify"never resumes on its own), andfallbackModels(models, in order, that a run moves to when its model can't serve it; default: none, so the run fails), andswapAtTurn(defaulttrue;falsekeeps a working child on its session until its run ends, also when that session is not current), andhideSessions(defaulttrue;falselets children read other agents' sessions and main's session, and runs a child's bash without the sandbox that hides them). The model guide isPI_CODING_AGENT_DIR/subagent-models.md, plus.pi/subagent-models.mdin a project that main trusts. See below.PI_BASH_DEFAULT_TIMEOUT: seconds;0oroffdisables the injected timeout. A value above 2147483.647 (the longest timer Pi can set) falls back to 120. Explicit per-call timeouts are preserved. Subagent children apply the same default to theirbashandpowershellcalls, also when a package filter turns off the Bash Default Timeout extension for main. Main and its children get a rule in the system prompt that gives the default, because Pi's ownbashschema says that the timeout has no default. The rule is only in Pi's default system prompt. ASYSTEM.mdreplaces the part that holds it.PI_IDLE_GC=off: disable Idle GC. When the agent has been idle for 30 seconds, Idle GC runs one full garbage collection that also gives the freed memory back to the system. It runs at most once in 5 minutes, and only when the process grew by 16 MB or more since the last one. It waits while a subagent works. The pause is tens to a few hundred milliseconds. A failure stops it for the session and goes toidle-gc.login the agent directory.PI_LOOP_STALL=off: disable Loop Stall. When Pi's main thread stops for 500 ms or more (PI_LOOP_STALL_MSsets another threshold, 100 or more), Loop Stall adds one line toloop-stall.login the agent directory. The line gives the time, the length of the stall, the main thread's CPU time and the GC time in it, the slow synchronous file writes that ran in it, and the disk I/O pressure (Linux). CPU time close to the stall means JavaScript or GC work; CPU time close to zero means a blocking system call, such as an fsync on a busy disk. At 1 MB the log moves toloop-stall.log.1.PI_EXTRAS_DEFER_TOOLS=0: disable Defer Tools for the run (see Defer Tools).PI_HERDR_TAB_NAME=off, orherdrTabName.enabled: falseinpi-extras.json: disable Herdr Tab Name. In a Herdr tab with one pane, it keeps Pi's session name and the tab's label the same. A session that has a name puts it on the tab, at the start and after each/name(a name longer than 48 characters is cut). A session with no name takes the tab's label, unless the label is empty or a number. A tab renamed in Herdr renames the session after the next run; when both changed, the session name wins. A label set in Herdr that is longer than 48 characters, or has spaces around it, is then written back cut and trimmed. A session started in a tab that has a label gets that label as its name, also after/new. It runs only in the TUI, and not inside tmux or screen, where the pane belongs to the outer program (PI_HERDR_TAB_NAME=onruns it there too). With pi-intercom loaded, a session takes the tab's label only after pi-intercom has registered it with its broker, so that the name reaches the broker. When pi-intercom does not connect in 10 s (it is turned off, or its broker is down), the label comes after those 10 s, and later renames do not wait until pi-intercom connects. A failedherdrcall changes nothing and goes toherdr-tab-name.login the agent directory.PI_CACHE_RETENTION=long: use the hour-long cache-warmth window when the transcript doesn't say. Anthropic replies report hour-long cache writes and Status Plus follows them, so a proxy that always writes them, such as Meridian, needs no setting.PHASE_SPINNER_DEBUG=1: opt-in local timing diagnostics. Leave off normally.PI_VOICE=off: disable voice dictation.PI_VOICE_KEY: a different key (defaultctrl+space).PI_VOICE_HOME: where voice keeps its runtime, models and settings (default~/.cache/pi-extras/voice, or underXDG_CACHE_HOME).PI_COMPUTER_USE=on: enable computer use on macOS. Off by default. See below.PI_TOOL_DISPLAY=off: leave Pi's own tool rows in place./tool-displaywrites its switches undertoolDisplayinpi-extras.json:enabled,others(defaulttrue),chains(defaulttrue),motion(fullorreduced), andthinking(tail,collapsedorfull; defaulttail).phaseSpinner.verbsinpi-extras.json: opt into playful spinner words with"playful"(the built-in pie and π list), or up to 100"Present|Past"pairs such as["Simmering|Simmered"]. Without this setting, the status names the phase descriptively.PI_COPY_BLOCKS=off: leave code blocks and quotes in replies as Pi draws them.statusPlus.toolCountinpi-extras.json:steps(the default) orcalls, switched by clicking the footer's tool count or with/tool-display count.statusPlus.copyLinksinpi-extras.json:falsestops the link copy in a remote session (defaulttrue). See Status Plus.releaseNotes.seeninpi-extras.json: the last pi-extras version whose notes were shown.KAGI_TOKEN_FILE: path to your own subscription session credential. See below.KAGI_TOOL_NAME=web_search: explicit search-tool replacement. Leave unset to keepkagi_searchand avoid conflicting with another search extension.FIRECRAWL_API_KEY, or the file~/.secrets/firecrawl_api_key(another path withFIRECRAWL_API_KEY_FILE): optional Firecrawl API key forgov_search(higher rate limits).PI_GOV_SEARCH_PROXY, orgovSearch.proxyinpi-extras.json: optional HTTP CONNECT proxy forgov_search.govSearch.enabled: falseinpi-extras.jsonleavesgov_searchout. See Gov Search.
See security and privacy before enabling provider-limit polling or supplying search credentials. Extensions execute with your user permissions.
How pi-extras.json is saved. Each extension keeps its settings in its own
section. A save reads the file again and changes only that section. A short
lock, pi-extras.json.lock, makes Pi sessions that save at the same time wait
for each other. A save waits for the lock for up to 3 seconds, then fails with
a message. The new text goes to a temporary file, which then replaces the old
file. When pi-extras.json is a symbolic link, the save writes the file that
the link points to and keeps the link. The lock and the temporary file are in
the folder of that real file, for example your dotfiles folder. The file keeps
its permissions, and a new file gets mode 0600.
When the file does not parse (for example, after an edit that leaves an extra comma), pi-extras does not write it. Each extension uses its defaults, and each save shows an error that names the file. A new session in the terminal also shows a warning. Correct or remove the file to save settings again.
The Status Plus context figure (33k / 100k and its bar) shows the context
size against the point where Pi compacts the session. That point is the
window less compaction.reserveTokens, or less the reserve in
compaction.modelOverrides for the selected model. Thus 100% means Pi compacts
now, and the figure can go past 100% until it does. With compaction off, or
with a reserve as large as the window, the figure uses the whole window. It
turns amber above 70% and red above 90%. After a compaction the size is ?
until the next reply. The figure reads the settings files again when one of
them changes, and shows the change at the next entry in the session. Pi uses
a changed file only after /reload. Thus, after you edit a settings file, use
/reload so that Pi and the figure agree.
Rows for providers. The footer has a row for each provider that did work in the session: a paid provider when it costs something, a free one when it has sent or made tokens. The selected model's provider also gets a row as soon as it has limits, before its first request. When you select a model, Status Plus reads that provider's limits at once. A poll that another Pi process made recently counts, so the switch usually does not ask the provider again.
Provider families. pi-extras counts provider ids that use one plan as one
provider: anthropic, claude-code and meridian are the Anthropic family,
and openai-codex and openai are the Codex family. Each family has one row,
which adds up the spend, airtime and tokens of its members, and one limit
reading, which one poll reads for all Pi processes. The Anthropic poll uses the
proxy quota route of the anthropic provider when it has one, else Pi's
anthropic OAuth login, else the Claude Code login of the claude-code provider
(~/.claude/.credentials.json, or the folder in PI_CLAUDE_CODE_CONFIG_DIR or
CLAUDE_CONFIG_DIR). Thus a claude-code session needs no setup. Status Plus
does not refresh an expired Claude Code login: Claude Code does that itself.
On macOS, Claude Code keeps its login in the Keychain, so a claude-code
session there shows limits only when Pi has its own anthropic login.
A family shows the limits of one account: the first login in the poll order
above. If two members use different accounts, for example Pi's anthropic
login and Claude Code's login, the row shows the account of Pi's anthropic
login for both. A per-token API key is not part of the plan: an anthropic API
key or an openai API key gets no plan windows and no plan cooldown, and its
spend shows on a row of its own ("Ant key" for an anthropic API key).
Status Plus checks Pi's anthropic credential at session start; without Status
Plus, offline, or with polling off, an anthropic API key counts as the plan.
Subagent spend. The footer adds the spend of the subagents that the
current branch started. A child that a subagent call started counts from
that call's result. A child that a codemode script started counts through the
team index of the session. That child counts when it started during a tool
batch on the current branch, from the reply that called the script to the
results. A script that still runs counts its children so far. A child that a
report names, but that the branch did not start, does not count. Thus, after
/undo, children of the removed branch no longer count.
The session file of a child holds all its runs. A branch counts only the runs
that it started: the first run when the branch started the child, and each run
that a message call of main on the branch resumed, direct or in a script.
A run that main did not start counts on the branch that started the child.
Examples are an automatic resume after a restore, /subagents resume, and a
message from a peer. A run that moved to a new session or a fallback model is
one run. A child session from before 0.26.2 has no start line for each run.
It counts as one run on the branch that started the child.
Pi does not keep the arguments of a script call when they are too long, so a
long message in a script does not name its child. Such a call counts for
one run: the first run of a child of main that starts in the tool batch of
the call, and that no named call started. Thus a run that main did not start,
but that starts in that batch, can count on the branch of that batch. Each
branch gives the same result. A run that waits for a free slot and starts
after the batch ends counts on the branch that started the child.
A click on a status line. Status Plus shows the status of each other
extension (ctx.ui.setStatus(key, text)) on a line of its own below the grid.
In fullscreen mode, a left click on one of these lines sends an event on Pi's
event bus. Other extensions can use this event as a contract:
- Name:
status-plus:click. - Payload:
{ key }.keyis the key that the extension gave toctx.ui.setStatus. - When: the left button goes down and up at the same point on the footer line of that status. Today Status Plus cuts a long status to one line. If a later version wraps a status, each of its lines sends the same key.
- Only in fullscreen mode. In regular mode the terminal keeps the mouse, so the event does not occur.
- Not for the remote-pi statuses, because the grid shows them in one cell. A click on the tool count changes the count and sends no event. A click on a link in a status line goes to the link.
Status Plus does nothing else with the click. The extension that set the status decides what occurs:
pi.events.on("status-plus:click", (data) => {
if ((data as { key?: unknown } | undefined)?.key === "my-extension") showDetails();
});Links in a remote session. In fullscreen mode, Pi catches the mouse. When
you click a link, Pi opens it with xdg-open (Linux) or open (macOS) on the
machine that runs Pi. Over SSH, that is the remote machine. On a remote machine
without a desktop, the click does nothing and shows no error. Thus, in a remote
session, Status Plus copies the link and shows Copied link: <url>.
- A session is remote when
SSH_CONNECTION,SSH_CLIENTorMOSH_CONNECTIONis set, or whenPI_CLIPBOARD_OSC52=1. This is the same test that Pi uses for its clipboard. - The copy uses Pi's clipboard function. In a remote session it sends the text through the terminal (OSC 52) to the clipboard of the computer in front of you. Your terminal must accept OSC 52. On the maintainer's Mac the link gets to the clipboard. On the maintainer's Windows PC it does not get there today. When the link is not in your clipboard, select it in the notice.
- When the copy fails, a warning shows the link, so that you can select it.
- A local session opens links as Pi does.
- To turn this off, set
statusPlus.copyLinkstofalseinpi-extras.json, then use/reload.
Tab Status shows Pi's state in the terminal tab.
- iTerm2 3.7.0+ gets a colored Session Status dot with a short detail:
accent while working, warning while a dialog waits for you, dim when idle,
and red after a failed turn until the next prompt. Working details name the
current phase or tools. Idle details say
Done; settabStatus.detail: "reply"to show the first 80 characters of the last reply instead. - Progress animates the tab while Pi works, pauses during dialogs and Rate-limit Recovery waits, turns red after a failed turn, and clears at idle. In Windows Terminal, a full yellow ring means Pi is waiting, either for your input in a dialog or for a rate-limit wait to finish, and a full red ring means the last turn failed.
Background Subagents and Shell Jobs count as working by default, even after main finishes.
Progress turns on by itself only in terminals known to support it, because older terminals can show every progress update as a notification:
| Terminal | Automatic from | Notes |
|---|---|---|
| iTerm2 | 3.6.7 | 3.6.6 supports progress but doesn't advertise it; set progress: true |
| Ghostty | 1.2.0 | |
| WezTerm | nightly 20250209-182623-44866cc1 |
No paused state, so waits stay indeterminate |
| Windows Terminal | 1.6 | It doesn't report its version, so WT_SESSION alone turns it on; older versions ignore it |
Unknown terminals and older versions stay off. Windows Terminal is the
exception: WT_SESSION turns progress on, in WSL tabs too, since Windows
Terminal passes it into WSL. SSH sessions don't get WT_SESSION, so they stay
off. Another terminal's TERM_PROGRAM, such as vscode, wins over an
inherited WT_SESSION. iTerm2 is detected from TERM_PROGRAM=iTerm.app, or
from LC_TERMINAL=iTerm2 (which also works over SSH) when no other terminal
sets TERM_PROGRAM. Inside tmux or screen, the version comes from
LC_TERMINAL_VERSION, never from the multiplexer's own version.
If Pi's terminal.showTerminalProgress setting is on, Pi owns progress
instead; Pi's bar is indeterminate only and ignores background work, pauses
and errors.
Inside tmux, add set -g allow-passthrough all so hidden panes can update the
tab. Tab Status wraps its sequences for tmux, resends busy status every second
and sends the final idle twice.
Settings in pi-extras.json, applied on /reload:
{
"tabStatus": {
"enabled": true,
"sessionStatus": "auto",
"progress": "auto",
"busyWhileBackground": true,
"detail": "done"
}
}sessionStatus and progress take "auto", true to skip the version check,
or false to turn them off. Session Status still needs iTerm2. Force progress
only in a terminal you know supports it.
- Only the top-level interactive session writes to the terminal. Print, JSON and RPC modes and subagents stay silent.
- Both fields reset on startup and clear on exit.
- Idle waits 1.5 seconds, so short gaps between turns don't set off completion alerts.
- A reload keeps the tab as it is until the new runtime knows the background counts. Turning Tab Status off and reloading clears only what it set.
- A crash, or uninstalling pi-extras and then reloading, can leave a stale tab until another program resets it.
iTerm2 can show these details in its Cockpit and in status-change alerts; see security and privacy.
On by default. To compact, Pi normally sends the conversation to the model as a new summarization prompt. That prompt can't use the provider's prompt cache, so the whole context is paid for again at full price. Cache Compaction sends the summary request as the session's next turn instead: the same system prompt, tools, history, reasoning settings and session ID, with a summarization instruction added at the end. The provider reads almost all of it from cache.
Measured on this machine (provider-reported costs, not invoices):
| Context | Model | Pi's compaction | Cache Compaction |
|---|---|---|---|
| ~15k | Claude Sonnet via Meridian | $0.048 | $0.015 |
| ~15k | Codex | $0.019 | $0.0066 |
| ~42k | Claude Sonnet via Meridian | $0.169 | $0.010 |
| ~42k | Codex | $0.055 | $0.0034 |
In live automatic compactions at a 48k window, about 32k of the 33k input tokens came from cache, on both Claude and Codex. The summaries were comparable to Pi's own in a two-model comparison. Results vary by provider and workload.
When it runs. In the main session and in pi-extras subagents, for
/compact and for Pi's automatic compaction after a turn or before a new
prompt, as long as:
- the cache is probably still warm (see
idleSecondsbelow); - the model, session and branch haven't changed since the last request;
- the summary fits in the context window (see below).
Otherwise Pi's own compaction runs, exactly as it would without this extension. After a context overflow, Pi's summary request can be too large for the window; see "Summaries that fit the window" below. The notice says which path ran and why. The summary request streams, so the Phase Spinner can show its newest lines while it is written. The request is the same as one sent without streaming.
Supported APIs. Anthropic Messages (anthropic-messages), OpenAI
Responses (openai-responses, openai-codex-responses,
azure-openai-responses), Google (google-generative-ai, google-vertex)
and OpenAI chat completions (openai-completions). Local engines such as
llama.cpp, vLLM and other OpenAI-compatible servers use chat completions. For
other APIs, Pi's own compaction runs with the reason unsupported-api.
Local engines. Many local engines keep a prompt prefix in memory until
they need the space. For openai-completions models with a local baseUrl,
the default idle limit is 25 minutes. A local baseUrl is a loopback, private
or Tailscale (100.64.0.0/10) address, 0.0.0.0, such an address in IPv6
(::ffff:192.168.1.5), a host name without a dot, or a name that ends in
.local, .lan, .internal, .localdomain, .home.arpa or .ts.net. This
stays under a typical gateway idle window. When the cache is cold, the summary
request is a full prefill of the conversation, which is larger than Pi's own
summary prompt. Set idleSeconds for the provider if your engine evicts
sooner, or if the local baseUrl is a proxy to a cloud API.
Some local engines do use a timer. Ollama unloads an idle model after
OLLAMA_KEEP_ALIVE (5 minutes by default), and LM Studio can unload a model
after an idle TTL. The cache goes with the model. For these engines, set
idleSeconds for the provider to a value below that timer, for example
"ollama": 240.
Measured on a local OpenAI-compatible engine (Qwen3.8-Flash-Next, one synthetic session of 18k tokens with two tool calls): the summary request had 19,229 prompt tokens, and 18,311 of them came from the cache. Only the 918-token instruction was new, and the first token came after 1.2 s.
Chat templates. A chat-completions engine renders the messages with the
model's chat template. The summary request keeps every field that changes the
rendered prompt: the messages, tools, tool_choice, reasoning_effort and
the template arguments. Only the output limit (max_tokens or
max_completion_tokens, as the request used) changes, and the instruction is
a new user message at the end. The extension does not set
tool_choice: "none", because some engines then render the prompt without
the tool list, and the cache misses from the first system token. If the
model calls a tool in the summary turn, the tool does not run, and Pi's own
compaction runs with the reason unusable-summary.
Some templates, such as recent Qwen3 templates, keep the reasoning of earlier
turns by default. With them, the new user message does not change how earlier
turns render. If a client sends
chat_template_kwargs: { "preserve_thinking": false }, the template removes
the reasoning of all turns before the last user message. Then the cache
covers the conversation only up to the first assistant message after the
previous user message (36% of the prompt in a synthetic check).
Background summary (opt-in). Pi's automatic compaction pauses the run
while the summary is written. In local sessions this pause had a median of
69 s and a 90th percentile of 223 s (425 compactions). With
cacheCompaction.background: true, the summary request starts earlier, beside
the session:
- A reply puts the context at
backgroundPercent(default 90) of Pi's trigger (contextWindow - reserveTokens) or more. The extension sends the summary request for the request of that reply. It shares that request's cached prefix, as above. - The session continues. The summary is held in memory. It is not applied.
- At Pi's trigger, the held summary becomes the compaction, with no new request. The reply to the summarized request and all later messages stay verbatim after it. If the summary is still being written, compaction waits for the rest.
The kept tail is larger than Pi's keepRecentTokens: it holds all the work
after the start point. Pi uses the normal path (prefix sharing or its own)
when one of these is true:
- the model, the session or the branch changed after the start point;
- the summary request failed or gave an unusable summary;
- the summary plus the kept tail is more than half of Pi's trigger;
- the compaction is manual (
/compact) or overflow recovery.
Each cycle sends one background request at most. If it fails (for example a short network error), the extension does not try again before Pi's trigger. An extension that edits earlier messages (for example one that shortens old tool results) changes the branch, so the held summary is not used.
A held summary that Pi does not use is stopped. Its tokens are still billed, but they are not in the session's totals. A session that ends between the start point and the trigger also leaves its summary unused. The extension sends no background request to chat-completions models on a local engine, because a parallel request slows or evicts the main request on one GPU.
Meridian serializes the requests of one session key: a summary with the
session's own key made the next main request wait for the whole summary (10.7 s
against 2.7 s in a probe). The background request to Meridian therefore uses
its own key (<session>:background in x-session-affinity). In the probe,
that request still read 25.5k of its 26.1k prompt tokens from the cache. Other
providers get the session's own headers, and every payload keeps the
session's own cache key.
Subagents always load Cache Compaction, even when a package filter leaves it
out of the main session. cacheCompaction.enabled: false turns it off in both.
Cache warming. A provider keeps a prompt cache entry for a limited time:
one hour through Meridian or with a ttl of 1h, else five minutes. If the
next turn comes later, the provider writes the whole context again at the
cache-write price. In 30 days of local sessions, 610 turns came more than one
hour after the previous turn. They cost $958, 11% of the Anthropic spend.
Most of them came after a shell job, a subagent report or a form answer.
The extension keeps the cache entry alive while the session waits for such a
wake. It sends the last real request again, with one short user message at
the end: Cache refresh only. Reply with the single word ok. Do not use tools. The provider reads the full prefix from the cache and writes only the
short tail. Meridian ignores max_tokens, so the instruction keeps the reply
short. In a probe on a 46k-token context, a warm cost $0.017 and a new write
cost $0.39. The next real turn still read 99% of its prompt from the cache.
A warm goes out at 90% of the cache lifetime (54 minutes for one hour, 4.5 minutes for five minutes) after the last real request or the last warm. All of these must be true:
warm.enabledistrueand the model's API is inwarm.apis.- The session, the model and the branch are the same as in the captured request.
- The request has no fixed thinking budget (
budget_tokens). A replay with a small output limit changes that budget, and the provider then misses the cache. The log showsthinking-budgetfor each such request. - A wake is pending, or a tool call is running now. Wakes are a running shell
job, a running subagent, a form from
artifact_askwithoutwatch: false, and api-extras:wake-pendingevent. A form wake ends when its feedback comes or when the user sends a new prompt.
If a compaction or a background summary is in progress, or the agent is running with no tool call, the warm waits. It tries again every 30 seconds until half of the margin before expiry has passed.
Warming stops when one of these is true:
- No wake is pending and no tool call runs.
- The model goes through Meridian, unless
warm.meridianistrue. Meridian replays the warm as a new session, so the warm does not refresh the main cache entry. warm.maxHours(default 8) passed after the last real request.- The prompt is smaller than
warm.minTokens(default 20,000). - The warms of this wait would cost more than one cache miss. For a five-minute cache, this stops warming after about one hour.
- The timer ran too late, for example after the machine slept.
- A warm failed. The extension does not send it again.
- A real request started. It stops a warm that is in progress. If the
provider reported usage for the stopped warm, the extension records it as
a warm. If not, the log shows
aborted-sentwith the estimated cost.
Each warm adds a pi-extras.cache-warm entry to the session with the
provider, the model and the usage. Status Plus and the usage limits count it
in the session's spend and tokens. Each decision adds one JSON line to
cache-warm.log next to cache-compaction.log, with the reason, the tokens
and the cost. It never holds provider text. With warm.meridian, a warm to
Meridian uses the <session>:background key, as the background summary does,
so it does not hold the main session's turn lock. Subagents warm as well while
they wait for their own shell jobs. A subagent that finished has no wake, so
it does not warm. Pi's own cache warmer stops after 30 minutes idle and needs
promptCache in models.json; this one does not. For the APIs in
warm.apis, the extension tells Pi's cache warmer to stop, so the two do not
both warm the same request.
Another extension can announce a wake with
pi.events.emit("pi-extras:wake-pending", { sessionId, key, pending }).
Send pending: false with the same key when the wake comes or is
cancelled.
Log. Each decision adds one JSON line to
PI_CODING_AGENT_DIR/cache-compaction.log (default
~/.pi/agent/cache-compaction.log): the session ID, provider and model, why
Pi compacted, which path ran, the fallback reason and the token estimates.
Cache-path attempts also record the stop reason and token usage. It never
holds messages, summaries, headers or payloads. With Tool Display loaded, the
session's compaction entry also saves the path and fallback reason, outside
the model's context. With a background summary, the path is background, and
the field background says used, why the held summary was not used, or
null when none was held. Fallback reasons for a missed capture start with
capture-: no-system, no-model, projection, identity, payload,
no-context, headers and failed.
Room for the summary. The summary is written inside the same context window as the conversation, so it needs room. The extension asks for room for the previous summary plus 8,000 tokens (6,000 for new material, 2,000 for reasoning), and keeps a safety margin of 4,096 tokens plus 60% of whatever was added since the last request, because that part is only estimated.
At Pi's default compaction.reserveTokens of 16,384, an automatic compaction
has about 12k tokens of room before anything added since the last request.
First summaries fit. Later summaries in long sessions often don't, because
each one carries the previous summary forward. Out of 183 compactions in local
sessions, 58% would have had room at the default, and all of them at 32,768 or
49,152. (Method: the model-written part of each saved summary, at four
characters per token, against room for the previous summary plus 8,000.) On
large-window models, raise the reserve in Pi's settings.json; 32k is about
3% of a 1M-token window:
{ "compaction": { "reserveTokens": 32768 } }With a 258,048-token window and a reserveTokens of 24,576, an automatic
compaction at the trigger has about 18k tokens of room. A first summary fits.
An update fits while the previous summary is below about 10k tokens. Each
token that the context goes past the trigger removes one token of room. When
the room is too small, Pi's own compaction runs with the reason
context-window.
Summaries that fit the window. Pi estimates tokens as four characters per token. Some tokenizers count more. On one local model, Pi estimated 192k tokens for a prompt that the engine counted as 271,737 tokens. Pi's own summary request then also went past the window, so overflow recovery failed and each new prompt stopped with the same error.
This check runs only after a context overflow. Before Pi's own compaction runs, the extension makes an estimate of the size of Pi's summary request:
- It finds the newest reply with token usage after the latest compaction, from the model that compacts. It divides the prompt tokens that the provider reported by Pi's estimate of the messages before that reply. Every system message counts, because a later one holds only its changes. For chat completions, earlier reasoning is not in the estimate, because a chat template can drop it.
- The ratio stays between 1.0 and 2.0. If no reply qualifies, or the estimate is below 16,384 tokens, the ratio is 1.0. Tools and the system prompt make up most of a small prompt.
- It multiplies the size of Pi's summary prompt by this ratio. It adds the
max_tokensthat pi-ai will send. - If the total fits in the context window, Pi's own compaction runs, as before.
- If the total does not fit, the extension sends Pi's summary prompt in parts, with Pi's thinking level and retry settings. Each part holds as many old messages as fit, with room for the summary so far and 16,384 output tokens. A part ends after a complete step when one is near, so a tool call and its result stay together. Each request updates the summary of the part before it. The last request also writes the split-turn section. If the split turn does not fit, its messages go into the parts.
The log and the notice show the path bounded with the reason that Pi's own
compaction would have had, for example overflow. If one message alone is
too large for a request, Pi's own compaction runs. If a part fails, Pi's own
compaction runs, and the log shows bounded-failed:request or
bounded-failed:length. An abort stops the parts and shows aborted.
Settings. In PI_CODING_AGENT_DIR/pi-extras.json (default
~/.pi/agent/pi-extras.json), then /reload:
{
"cacheCompaction": {
"enabled": true,
"idleSeconds": { "anthropic": 3300, "openai-codex": 240 },
"requestWindow": { "anthropic/claude-fable-5-2": 1000000 },
"background": false,
"backgroundPercent": 90,
"warm": {
"enabled": true,
"maxHours": 8,
"minTokens": 20000,
"apis": ["anthropic-messages"],
"formTools": ["artifact_ask"],
"meridian": false
}
}
}enabled:falseleaves Pi's compaction untouched.requestWindow: perprovider/model, the largest request in tokens that the provider accepts. Set it when the model'scontextWindowinmodels.jsonis a local compaction policy below the provider's limit. The summary request then uses the room above Pi's window, so a turn that goes past Pi's trigger does not send the compaction to Pi's own path. A value belowcontextWindowhas no effect. Do not set it for a local engine whose context is the real limit.background:truewrites the summary ahead of Pi's trigger (see "Background summary" above). Defaultfalse.backgroundPercent: the start point, as a percent of Pi's trigger, from 50 to 99. Default 90. An earlier start gives the summary more time, but the kept tail is larger and more summaries go unused.warm: cache warming while the session waits (see "Cache warming" above).enabled(defaulttrue);maxHours, the limit after the last real request, more than 0 and up to 48 (default 8);minTokens, the smallest prompt to warm (default 20,000);apis, the APIs to warm (default["anthropic-messages"]);formTools, the tools whose result waits for feedback (default["artifact_ask"]). A name also matches with an MCP prefix, such asmcp__oc__artifact_ask.meridian(defaultfalse) lets a warm go through Meridian. Leave it off. The warm key has no lineage there, so Meridian replays each warm as a new session. The main cache entry does not refresh.idleSeconds: per provider, how long after the last request the cache counts as warm, from 0 to 86400 (0 means never use the cache path). The default is 55 minutes for Anthropic through Meridian on port 3456, which writes one-hour caches, 25 minutes for chat completions on a local engine (see above), and 4 minutes for everything else. A provider with no value of its own uses the value of its provider family id (see Status Plus). The example'santhropicvalue also applies to Anthropic's own API, which keeps caches for 5 minutes, so leave it out unless every Anthropic route you use keeps long caches.
Providers. Anthropic Messages, OpenAI Responses (including Codex and
Azure), Google Generative AI and Vertex, and OpenAI chat completions. Other
APIs use Pi's compaction.
Tested with Pi 1.0.0. If another extension changes requests in
context_with_system, before_provider_headers or before_provider_request,
load it before this one.
Also falls back when: Pi is recovering from a context overflow; the place where Pi's kept messages begin can't be identified unambiguously; Pi is blocking images in the session; or the reply is an error, empty, a tool call, or cut off at the output limit. Codex requests don't declare an output limit, so for Codex the room check is the extension's own guard rather than a limit the provider enforces.
Cost of a failed attempt. If the cached request fails, the provider still bills it, but it isn't counted in the session's totals. Pi's compaction then runs as usual. See security and privacy for what is kept in memory.
Usage Guard reads the limit snapshots Status Plus polls. It polls limits on
its own only for a provider that has no snapshot yet, and for refresh: true.
A usage call or /usage reads the active model's provider first when it has
no snapshot. all: true also reads each provider of a scoped model (with no
scoped models, of each model that has credentials). These first reads keep the
shared poll gap. Usage Guard reports by provider family (see
Status Plus): a model of any member reads, and is warned
about, the limits of its family, so a claude-code model gets the
anthropic limits. With all: true, limitsUnavailable names each paid
provider family in scope that still has no reading, and the reason. For
dollar figures, a usage call or /usage also reads local session files and,
when set up, Tokenfold and OpenRouter (see Dollar figures).
PI_OFFLINE stops the Tokenfold and OpenRouter requests. Only windows that
govern the active model count: provider-wide windows always, model-specific ones
(such as an Anthropic seven_day_fable bucket) only when the active model id
carries that family. Balances (Enterprise spend, prepaid credits) are reported but
never warned on, and so are per-minute rate limits taken from response headers.
The exception is a proxy's monthly extra-usage cap: it warns at the same bands as
a window, because requests stop at the cap.
- OpenAI through Sign in with ChatGPT (
/login openai) gives apps no usage endpoint and no rate-limit headers (openai/codex#54219). Its requests count toward the same ChatGPT plan as Codex, so a second login,/login openai-codex, works as a meter: Status Plus reads the plan's 5h and 7d windows with it and shows them on the Codex family's row, which theopenaiprovider shares. You do not have to use theopenai-codexmodels. AnopenaiAPI key is billed per token, so it does not start the plan poll, reads no plan windows and keeps its own cooldown and fallback scope. usagetool: percent used, thresholds, reset time, seconds until reset andresumeAfterSeconds(reset plus margin) per window, andwaitablewhen the reset is exact and withinmaxWaitSeconds.setBudgetrecords a session budget ("tell me at 60% of the weekly limit"). It is a warning point: its warning asks the agent to note it and keep working, and to stop only when the user said to stop there.allincludes other providers and non-governing windows.- Zero-cost models: when every catalog price of the active model is 0 (a
local model), and no other provider of its family has a priced model, the
report marks it
unmeteredand its first note says that usage does not limit the session's own work. A note also says that limits withapplies: falsematter only for subagents on that provider. Withall: true,unmeteredModelslists the scoped zero-cost models, which a subagent can use without any limit. The notes come before the figures. - Known window durations and exact reset times add a
paceobject to each limit. It gives the elapsed fraction, expected usage at even pace, and projected usage at reset. A projection from 90% through 110% ison pace. This band allows small request bursts and percentage rounding. Higher projections areabove pace; lower projections arebelow pace. Above-pace limits also give the estimated time to 100%, in ISO and local time. Before 5% of the window passes, or below 1% used, the state istoo early to tell. These early limits omit the projection and the time estimate. The tool rows, popup, and warning text show pace. Pace does not change warning thresholds. Estimates use the average rate since the window began, not the last request. - Warnings are off by default: only a session budget warns, one time, when its
window is reached.
/usage warnings onadds band warnings (90 and 95 by default) and provider blocks. Each fires one time per window, threshold and reset cycle, immediately before a model request. It is a message appended to context, without changing the system prompt or initiating another request. Idle polls do not queue warnings for later delivery: the active provider and model family are checked again for manual prompts, automated wakeups and queued follow-ups. Obsolete automatic warnings remain in raw history. They are omitted from requests for models they do not govern, and from every request after their window has reset. Resets reported within ten minutes of each other count as one cycle, since proxies recompute them on every fetch. The first band is advance notice only. The final message never cuts short work that fits: for a waitable reset (any window, weekly included, withinmaxWaitSecondsof its reset) it tells the agent to finish what fits, then sleep through the reset in a background job (sleep resumeAfterSeconds) and carry on, so the run outlasts the limit instead of halting. A reset days away or a session budget asks for a clean checkpoint and a summary. A model-scoped window notes that other models are unaffected. Fired keys and the budget persist with the session, so a resumed session does not repeat them. - Proxy rejection flags can remain after a quota reset. A later reset cycle or a successful response from the applicable model clears that flag below 95% used. A new structured quota error restores the block. A rejection without newer evidence stays blocked. Cleared block warnings remain in history but do not enter later model requests.
- A proxy such as Meridian can answer the quota poll without its usage-endpoint
source. Then it sends only Claude Code's last readings, which can lag or read high.
- Such a reading does not replace a usage-endpoint reading that is less than 30 minutes old. A window that the answer does not include keeps its last reading until its reset.
- A Claude Code reading that is more than 15 minutes old is ignored. A rejection with a future reset still blocks the window. A rejection with no percent blocks the window and shows as an approximate 100%.
- A Claude Code reading is approximate. A kept usage-endpoint reading that is more than 30 minutes old is also approximate.
- The footer shows
~before the percent of an approximate reading, for example7d ~96%. Theusagetool marks it withapproximate. - An approximate reading fires no band or budget warning, has no dollar figures, and does not go into the granted-reset readings.
- A Claude Code reading can differ by 20 points or more from the last
usage-endpoint reading of its window, more than normal use explains. Then
each Pi process that polls writes one line with both readings to
status-plus.log. The line gives the percentages, reset times, ages and the proxy's status and warning threshold.
/usagequeues the current snapshot for the next turn, or steers it into the run while the agent works;/usage budget 7d 60and/usage budget clearmanage the session budget;/usage warnings on|offpersists the toggle.- Polling: providers whose window sits within
proximityPctof a threshold poll at their faster cadence; failed polls back off exponentially up to ten minutes. The Pi processes of one agent directory share one poll and one backoff, and a provider'sRetry-Afteris obeyed for up to an hour.
The usage tool also gives money, so that an agent can budget work and
subagents in dollars. Each limit gets a dollars object, and the report gets
a top-level dollars summary.
- Spend today: the summary gives the spend on this machine since local midnight, by provider. With Tokenfold, it also gives the spend on all personal machines. Spend is the API list price of the tokens, from the model catalog. It is not a bill.
- Subscription windows (Claude 5h and 7d, Codex): a provider reports only
a percent. The tool divides the spend in the window by the percent used to
get the window size in dollars (
basis: "implied"). Then it gives the dollars that remain. Below 5% used, the tool uses the last size it saw for that window in the last 8 days (sizeFrom: "earlier window"). Without one, it gives only the spend. The figure changes with the model mix, so use it as an estimate. - Tokenfold: when
usageDollars.tokenfoldis set, Claude windows use Tokenfold's spend for all machines on the account (source: "tokenfold"). Without it, the tool counts the spend on this machine only. Then use on other machines makes the limit read low, which is the safe direction. A Tokenfold window with a different reset is ignored. The tool matches a Tokenfold window to this account by its reset time only, so set Tokenfold only on machines that use the account Tokenfold tracks. - Granted resets: a provider can reset a window before its reset time.
The usage before that reset does not count, so the spend in the window
starts at the reset. Tokenfold gives this start (
window_start).- The tool also finds granted resets. It keeps a reading of each window at each limit poll. Polls occur only while Pi runs.
- A fall of 10 points or more, or a fall to 0%, with the same reset time is a granted reset. If the next reading is at 80% or more of the earlier level, the tool cancels the reset.
- The spend starts at the latest of these times: the cycle start, the Tokenfold start, and a granted reset that the tool found.
- Sometimes the tool finds a granted reset that is later than the Tokenfold start. Then the tool uses the spend on this machine from the reset. A note tells you this.
- A model-family window uses the Tokenfold start only when its own percent also fell at that time. Otherwise it uses only the resets in its own readings.
- Sometimes a reset comes after a gap of more than 20 minutes in the
readings. If the Tokenfold start is in that gap, the tool uses the
Tokenfold start. If not, the tool does not count the spend in the gap
(
spendStartApprox), and it does not keep that size for the next cycle.
- Model-family windows (such as
7d-fable): the dollars count only that family's spend and carrymodelFamily. They set a provider's summary only when they govern the active model. - OpenCode Go: the caps are dollars per model, from the OpenCode Go docs
(read 2026-10-10). The 5h window allows 20% of a model's monthly cap and
the weekly window 50%. The tool gives the remaining dollars per model
(
perModel) for the scoped models, the active model, and the models used in the last 7 days. Withall: true, the tool polls a scoped model's provider that has no snapshot yet. The usage endpoint does not report the plan, so set it. - Monthly meters (Claude Enterprise, extra usage): the provider's own
spend and limit, the calendar days and business days (Monday to Friday,
local time zone) until the reset, and
perBusinessDayUsd. That share includes today's spend, so it does not change during the day.meterSpentTodayUsdis the meter's growth since the first reading of the local day, andleftTodayUsdis today's share minus that spend. A negative value means today is over its share. With no reset from the provider, the next UTC month start is used (resetApprox).meterPerListDollaris the meter's growth divided by this machine's list-price spend over the same time. It needs at least $5 of list-price spend. A meter can charge less than list price: on 2026-11-10 a Claude Enterprise meter charged 0.76 of list price.- When the ratio is outside 0.9 to 1.1, a note tells the agent to use the
meter figures for the budget. Subagent
maxCostis list price, so a cap of X meter dollars ismaxCostX /meterPerListDollar. - Spend on other machines raises the meter, so the ratio can read high.
- When this machine spent $1 or more before the first meter reading of the
day, a note gives that amount and a meter estimate.
meterSpentTodayUsdandleftTodayUsddo not include it.
- OpenRouter: the credit balance, the key's spend for the current UTC day, the key limit that remains, and the budget that blocks first.
- Tool row: the band adds the dollars left for the active provider. The
popup shows each limit's dollars under its row, names the provider when
there is more than one, and ends with the summary. Press
jin the popup to copy the JSON that the model received. - Summary:
dollars.listSpentTodayThisMachineUsd(and, with Tokenfold,listSpentTodayAllPersonalMachinesUsd) is spend today at list price.dollars.providers.<provider>giveslistSpentTodayUsd, andremainingUsdwithbindingWindowfor the tightest window. For OpenCode Go, it givesremainingUsdByModel.unsizedWindowsnames the windows with no dollar size yet;remainingUsddoes not cover them, so plan less spend.
Settings, in ~/.pi/agent/pi-extras.json:
{
"usageDollars": {
"tokenfold": { "url": "https://usage.example.com", "keyFile": "~/.config/tokenfold-read-key" },
"opencodeGo": { "plan": "go", "monthlyCaps": { "glm-5.3": 15 } }
}
}tokenfold.urlmust usehttps, orhttpto this machine only. The key file holds a read-only key that Tokenfold accepts onGET /api/ha. The tool never shows the key or the key file path.opencodeGo.planisgo(the default) orgo-plus.monthlyCapsreplaces the cap of a model when OpenCode changes it.- Remote answers are kept for one minute.
refresh: truereads them again. When a source fails, the report gives a note and the other figures.
Usage Guard helps the agent checkpoint before a quota runs out. Rate-limit
Recovery handles the actual rejection without asking an unavailable model to
schedule a sleep. Detection is always on; /rate-limit-recovery on opts an
interactive main session into automatic waiting. /rate-limit-recovery status
shows the policy, off disables it, and cancel stops an active wait.
A structured rate_limit_error or rate_limit_exceeded with a numeric
retry_after in seconds supplies the reset estimate. The extension waits that
remaining delay plus its safety margin, shows a countdown, then makes one
continuation. Escape or Ctrl+C cancels; outside a wait, Pi keeps its usual key
behavior. Switching between Anthropic models, including compatible aliases,
retains the cooldown and uses the newly selected model on resume. Switching
outside that scope cancels. Other providers retain waits across models on the
same provider.
Before the retried request, a persisted message records the actual elapsed wall time and UTC pause/resume timestamps, plus the limited and current models. If the system clock moves backward, the notice labels a lower bound from the completed timer instead of claiming an exact wall-clock duration. Steering queued while waiting reaches the recovery request; explicitly deferred follow-ups remain deferred until the original task finishes, as in Pi. It is a timing notice, not proof that quota has reset. Each user-started run has a shared five-hour maximum wait budget (margins included) and at most ten recoveries by default. Excessive delays are refused, never shortened to retry early. Reload, session replacement and exit cancel; a restarted session never silently resumes a previous wait.
Anthropic subscription requests (OAuth, direct to api.anthropic.com) can be
held near :00 and :30 UTC: the response starts, then sends only keep-alive
pings for minutes, and Pi never retries because pings count as activity. If no
real event follows the headers within anthropicFirstEventSeconds (45 s), the
request fails as a timeout and Pi's normal auto-retry sends it again. API-key
requests, proxies such as a local gateway, and other providers are untouched.
Short rate limits get their own backoff, on by default in every session,
subagents included: OpenRouter's "temporarily rate-limited upstream" and other
429 or rate-limit errors without a structured reset. Pi's own retry gives up
after about 14 seconds, while these usually clear within a minute. The extension
waits 5, 10, 20, 40, then 60 seconds (each ±20% so parallel subagents spread
out, and never less than the 429's Retry-After), for up to
transientMaxWaitSeconds (180 s) per streak, then sends the request again. A
countdown shows in interactive sessions; Esc cancels, and switching models
resumes immediately with the new model. If the limit outlasts the budget, the run
ends with guidance. Quota, billing and usage-limit errors are not treated as
short limits. A limit reported inside an already started stream is recognized
only by its message text, because Pi keeps no status code or structured reset
for it: OpenRouter's "rate-limited upstream" wording gets this backoff, while a
generic message such as "Provider returned error" stays with Pi's own retry.
Connection and proxy failures get the same backoff and the same budget, with a separate streak: at most 180 s of waiting, not counting the failed requests. Pi's own retry stops after about 14 seconds. Before this change, the run then stopped until a person typed, sometimes for hours. These errors are connection failures:
- A dropped connection:
Connection error.,terminated,read ECONNRESET,connect ECONNREFUSED,socket hang up. The first wait is 2 s, because a short break in the connection clears fast. The next waits are 10, 20, 40, then 60 s. - No data in time:
Request timed out., and anupstream_timeouterror. - A busy provider or proxy: an
overloaded_error, or OpenAI'sOur servers are currently overloaded. Please try again later.When the error body has aretry_afterin seconds, the next wait is at least that long. The extension uses at most 60 s of it. The message shows a longer value that the provider asked for. - A gateway with no backend: an HTTP 502, 503 or 504 whose body says
"unavailable", "overloaded", "bad gateway" or "gateway timeout". A local
engine that is off (
engine_unavailable) is retried too. The retries stop when the budget is spent.
The extension matches only the full error text of the transport, not a mention
inside other text. These errors still stop the run: quota, billing and
usage-limit errors, authentication errors (for example api_key_expired, 401
and 403), a full context window (also inside a 5xx), and a structured
rate_limit_error with a retry_after (quota hibernation handles it). Other
errors, for example a bare HTTP 500, stay with Pi's own retry. The extension
replaces each error text that it handles with its own report, which names the
provider and model. Pi does not retry this report. Thus only one retry loop runs
for each error. The original error text goes into a hidden session entry
(rate-limit-recovery, kind connection-failure, at most 2 KB). A subagent that still fails after its retries moves to
its next subagents.fallbackModels entry, as for an overloaded provider.
A local provider has no wait limit. It is your own hardware, so a retry has no
cost and takes no share from other users. A local server that does not answer is
usually off or restarting, and it comes back. A provider is local when its model
costs 0 and its baseUrl host is not public: localhost, a loopback, LAN,
link-local or Tailscale (100.64.0.0/10) address, an IPv6 ULA, a name that ends in
.local, .lan, .internal, .home.arpa or .ts.net, or a name with no dot
(fw01). Meridian listens on loopback but bills a cloud model, so it is not
local. Add a provider that this rule misses to localProviders. Set
localRetry: false to give local providers the budget again.
For a local provider, the connection failure shows once, and the run ends: the
session is idle while it waits. Auto-update can reload or restart Pi, shell jobs
report, and you can type. The extension waits 2 to 30 s, then checks the server
with GET <baseUrl>/models every 10 s. The server answers when it sends any HTTP
reply other than 502, 503 or 504, and is not a model list with no model in it: a
gateway whose engine is stopped (Kiln) answers 200 {"data": []}. The check does
not follow a redirect. When the server answers, a short message ("… answers
again. Continue the task …") starts the run again. A reply that says "not ready"
(502, 503, 504, an empty model list, or a refused connection) shows that the check
works, so the wait goes on with no resend: Kiln off for the night is one wait,
not a failed request every 10 min. Only when the check gets no reply at all
(a timeout) for 10 min does the request go again, with a message that says the
server still does not answer. Only the last of these messages stays in the model's context. One line
below the chat shows the wait, for example
fw01/qwen3.8-flash-next not answering for 4m10s · local server, checked every 10 s · Esc / Ctrl+C cancels. Esc or Ctrl+C ends the wait when the editor is
empty; with a draft in the editor, they work on the draft as usual.
/rate-limit-recovery cancel ends it too. A message that you send ends it, and
your message goes to the model. A shell job or subagent report starts a run that
carries the task on; if that run fails on the same server, the wait starts again
and keeps its start time. A model switch starts the run at once with the new
model. The open wait is saved in the session. It goes on after a reload, and
after an update restart or pi --continue within 10 min of the old process
stopping. It does not go on in a fork or a new session. A server whose /models
lists no model while chat works (rare) waits until you send a message or press
Esc.
The wait has no limit only where it can end well:
- Interactive main sessions only.
pi -p, JSON and RPC runs keep the budget, because no one is there to press Esc. - A subagent waits inside its run, as before: it has no update of its own to
wait for, and its
maxMinutesends it. - A subagent waits with no limit only when
subagents.fallbackModelsis empty. With a fallback model, the budget ends the wait and the run moves to the fallback. Without one, the run waits until itsmaxMinutes. - If the server answers the check but the request fails 3 times in a row (a crash on this prompt, a proxy with no engine behind it), the budget applies again until a good reply.
- Local waits keep their own streak, so they never use up a cloud provider's budget.
Meridian, a local Claude Code proxy, sometimes stops a turn with "Reached
maximum number of turns". Pi does not retry this error. The extension sends the
request again after 2 s, 5 s, 10 s, 30 s, 60 s, then 120 s, in all sessions,
subagents included. The first 3 retries always run. The later retries must fit
in transientMaxWaitSeconds, so the default budget gives 6 retries in 3 min.
The extension sends the request again only when no tool of the failed reply
ran. After the last retry, the run stops with an error.
Recognized quota errors remain errors and are classified as non-transient so
Pi does not run a competing short retry loop. This recovery policy is independent
of Pi's retry.enabled; other transient failures retain Pi's native retries.
For native Anthropic and OpenAI HTTP adapters (including Azure and Codex), a
request-local guard also stops configured transport retries on recognized JSON
429 errors. It preserves endpoint, authentication and model configuration;
ordinary HTTP failures keep both transport and session retry behavior. Inspection
is bounded to 32 KiB and 500 ms. A legacy custom provider's guard stays on one
API; switching that provider to another API preserves its custom routing and
adapter retries rather than retargeting it. Built-in providers with several APIs
(OpenRouter, Copilot) move their guard to the selected API at the next request,
unless a subagent shares it. Unsupported transports, mixed-API
legacy requests and unrecognized formats retain adapter behavior; detection
runs when Pi reports the final error. Header-only reset timing is not guessed.
If waiting is off, timing is missing, or the budget is exhausted, the session
fails with reset guidance instead. Provider reset times are estimates.
Children never hibernate. The pi-extras child launcher installs an explicit quota guard that never sleeps, even when the parent has opted in: one quota rejection fails the child, frees its slot, and tells the parent the provider, expected reset time and time remaining. Hidden transport retries are suppressed on the guarded native HTTP paths described above; custom or unsupported adapter retries remain owned by those adapters. Print, JSON and RPC sessions likewise never auto-wait, so external noninteractive children cannot inherit a long sleep. The bounded backoff for short rate limits and connection failures above still applies to them. External launchers that disable extensions must explicitly load detection; this package cannot intercept requests made outside Pi's extension runtime. The extension makes no quota polls of its own and pauses cache warming for the limited model scope until the estimated reset.
Hold ctrl+space and speak, or tap it to start and tap again to stop. Esc discards the recording, including after you stop while it is still being transcribed. The transcript is typed into the editor and never sent on its own; review it and press Enter yourself. Speech is transcribed in chunks at each pause while you talk, so when you stop only the last chunk is left to decode and the text starts typing in right away.
A recording row in the editor border shows a red dot, the elapsed time, a level meter, one mark per chunk, the microphone and the model. It uses the top border when that is free and the bottom border while the agent is working. Warnings there cover a mic that hears nothing, audio that clips, and a missing permission. After you stop, a wait longer than a second says what it is waiting for (starting voice, loading the speech model, or transcribing).
/voice opens a menu with the current mic and model. /voice mic picks the
input device (saved by name; a missing device falls back to the system default
with a one-time warning). /voice model picks the speech model, /voice status
shows what is installed and why a model was skipped, /voice setup retries
installation, and /voice unload frees the model's memory.
Platforms. macOS, and Linux with PulseAudio or ALSA (including WSLg). Linux has had less testing than macOS. Windows is not supported. Hosts without a microphone download nothing until you start a dictation.
First run. On a machine with a microphone, Pi installs voice in the
background: a private uv,
Python 3.12, the sherpa-onnx speech runtime, and NVIDIA Parakeet models. The
108 MB English model comes first; on machines with 8 GB of RAM or more the
487 MB multilingual Parakeet v3 follows and becomes the default. On Apple
Silicon, /voice model also offers an MLX build of Parakeet v3 (about 2.5 GB).
A model is skipped when the disk has less than twice its size free. Setup
never blocks Pi; a dictation started before it finishes waits for it. If setup
did not start with the session, the first dictation starts it, except offline.
On Linux, Pi first looks for an ALSA capture stream in /proc/asound/pcm. A
sound card with a microphone input has one, also when no microphone is
connected. Then Pi asks a running PulseAudio or PipeWire server, with pactl,
for a source that is not a monitor of an output. WSLg always lists its
RDPSource, so on WSLg setup starts when pactl (from pulseaudio-utils) is
installed. On macOS, a background check lists the input devices, and setup
starts when it finds one. This check needs Pi to run under a plain node
binary. Pi keeps the pactl answer and the macOS result for one day. When Pi
cannot find a microphone, or under other builds on macOS, setup waits for your
first dictation or /voice setup.
Background process. One daemon per user holds the model and serves every Pi session. It starts on the first dictation, exits 15 minutes after the last one, and exits sooner when no Pi session is open. The next dictation starts it again and loses no audio while it loads.
Logs. PI_VOICE_HOME keeps three local logs. provision.log records
setup, and daemon.log records the daemon. A daemon that cannot start (for
example, a Python file without execute permission) is also noted in
daemon.log, and the dictation shows the cause. voice.log records errors
that voice could not show, for example after /reload or /new.
macOS over SSH. macOS gives SSH sessions a silent microphone, so when Pi
runs over SSH, recording runs as a short-lived launchd job in your desktop
session instead. macOS asks one time, on the Mac's screen, to allow
ffmpeg to use the microphone; this needs Homebrew ffmpeg. About the first
half second of each recording is lost while that job starts.
The editor's top divider says what the agent is doing:
─ ⢌⡱⢎ Thinking… 00:12.4 ↓ 212 tokens ──── TPS 109.3 ─ TTFT 0.7s ─ Time 00:15.8 ─
- The word names the phase:
Sending request,Waiting for the model,Thinking(thenStill thinking,Thinking moreandDeep in thought),Writing main.md(a write; an edit readsEditing cc-phase.ts),Writing bash call,Running bashorRunning 3 tools, andWriting reply. A token count follows it while the model writes. Pi's own statuses (compacting, retrying, summarizing a branch) take its place while they run. - The spinner is three cells wide, so the word never moves. Each kind of work has its own: a ping while the request goes out, a helix while the model thinks, a print head while it writes a tool call, a comet orbit while tools run, two comets while it waits for a subagent's reply, and a wave that follows the stream's speed while it writes the reply. Compaction squeezes to a point, a retry drains, and a branch summary walks every row. With Tool Display's folded mode, only one place moves at a time: while a folded line is live, it shows the spinner. The status line then has no spinner and no space for one: its word starts right after the dash and holds still. Its words, clocks and colors stay.
- Color: the status turns amber after 10 seconds of thinking, and red when no tokens have come for 10 seconds. Some providers, Meridian among them, send the start of a tool call and hold its arguments until the model has written them all. While a call has started but none of its arguments came, the status stays calm for 2 minutes, as it does while it waits for a first token. When arguments come, the 10-second rule applies again.
- The clocks: a stopwatch beside the word shows how long the current step
has run, in tenths, in the word's color. It restarts for each new request,
thinking, tool call, tool run and reply;
Still thinkingand the later wordings keep counting.Timeon the right is the whole prompt. Pi's statuses get a stopwatch too, except a retry, whose own text counts down. Reduced motion stops the spinner but not the clocks. - TPS shows how fast tokens come. While the model streams, it counts the tokens that came in the latest second, and it changes on every frame (at least 12 times a second), so it rises and falls with the stream. Some providers hold a tool call back and send it in one piece at the end. Such a burst counts as if it came evenly over the silence before it, so it does not show as a false spike. When no tokens came in the latest second, TPS shows the prompt's average: the output tokens that the provider reported, divided by the time of the requests. The live count uses 4 characters a token, as the token count beside the word does.
- Narrow terminals drop TPS first, then TTFT, then the token count, then
Time. Then a long tool name shortens toWriting…orRunning…. The word, the spinner and the stopwatch go last, and the status never wraps.
While the model thinks, its newest three lines show dimly at the bottom of the conversation, above queued messages. New characters show brighter for a moment, then fade, unless motion is reduced. A tool call the model is still writing shows as Pi's own row in the conversation.
While a compaction runs, a purple band at the bottom of the conversation shows why it runs, the size of the context before it, and the time, with a sweep. With Cache Compaction, the band also counts the tokens of the summary so far and shows its newest 2 lines as they stream. When the compaction is done, the final compaction band in the conversation takes its place. A cancelled compaction turns the band gray for 2 seconds.
When a prompt finishes, a dotted π waves in and out in the divider, and the
conversation keeps an end line such as
π Worked for 41s, done 9:14 PM, avg TPS 98.1, or
π Stopped after 12s when you stopped it. A prompt of one minute or more
reads in minutes, for example π Worked for 2m04s, done 4:13 PM. The average
TPS shows when at least one response completed. The end line and the thinking rows
are only drawn; the model never sees them.
Set phaseSpinner.verbs to "playful" for pie and π words (Proofing,
Kneading, Approximating, …), or to your own "Present|Past" pairs. One verb
then lasts the whole prompt, the phase follows it, and the end line uses its
past tense.
Voice recording uses the bottom border while the agent works, so the status
stays visible. Pi's own working loader stays hidden.
/tool-display motion reduced holds every spinner on a still frame.
The π, ●, ∴ and timeout marks are drawn one cell wide. A terminal set to
draw East Asian ambiguous-width characters two cells wide will misalign them.
Tool Display redraws tool rows in the terminal: Pi's built-in tools, and every other tool too (see below). The tools are built as usual, and the model sees the same tools, descriptions and results; only the rows change.
Each call is one header band: the tool and its target on the left, the time on
the right. The band's color says how it went (green done, red failed, amber
timed out, gray aborted), so there are no status marks, and a failure is named
in words in the right rail (exit 1, timed out). While a call runs, the band
fills toward its timeout and warms as the timeout gets close; a call without a
timeout sweeps instead. Times read the same on every row: 8.6s, then
24m23s and 1h50m, and a timeout beside one is in the same units
(20m02s / 30m00s). Rows share the bullet column with the agent's text.
While a call runs, its bullet says what kind of call it is: a shell command
with a timeout fills ○ ◔ ◑ ◕ ● as it uses up its timeout, a subagent or peer
request has a slowly circling dot, a web call breathes, and other calls keep a
still dot. While the model writes a call, a pen moves in the bullet column, and
the right rail shows writing, the size of the arguments so far and the time,
for example writing 1,204 chars 6.2s. Under the band, the newest 2 lines
of the text the model writes now show, with a caret after the last. New
characters show brighter for a moment, then fade. The preview goes when the
arguments are complete. Some providers hold a call's arguments until the model
has written them all. Then the pen moves slowly and dimly, and the rail shows
only the time. Subagent and message rows show the same pen, rail and lines. A
call that waits its turn, or one rebuilt from history, shows a dim dot. Times of ten
seconds or more are drawn in a warmer color, so slow calls stand out when you
scroll back. Output sits indented under
the band on a gray panel, so each call reads as one block apart from the
conversation.
- bash: the command, and its last few lines of output.
- read: what was read, e.g.
80 linesor20 of 5,321 lines. - edit:
+12 −3, with long diffs collapsed. - write: the file's line count, and its last three lines (where a streaming write is).
- grep, find, ls: what was found (
23 matches in 7 files,42 files).
Every tool row stays visible after it finishes. Pi's native ctrl+o expands all rows, as before.
Output that doesn't fit ends in a line such as … 12 earlier lines; a single
hidden line is shown instead, since the hint would take its place anyway.
Click a row to open the whole call over the full terminal: the full command,
every line of output, and for a chained command each step (pick one with a
click, its number, tab or the arrow keys). copy command and copy output in
its title bar, or c and o, copy them whole; text dragged across copies
what it shows. Esc, q or ✕ closes it. ctrl+o still expands every row in
place.
Agent communication. Mesh agent_send and agent_request rows use purple
bands. Delivery failures keep their red rail words rather than recoloring the
band. list_peers keeps the usual operational colors.
Incoming remote-pi mesh messages show the sender's name and working-directory
basename, → me, and message or replies, over a purple Markdown preview.
Click or ctrl+o expands the body. Transport headers and reply instructions stay
in model context but are hidden from the human view; unfamiliar envelopes fall
back to a purple band over their raw text. Tool Display registers this renderer,
so it works even when Subagents is disabled.
Artifact feedback. An answer or a comment that the owner sends from the
artifact inbox arrives as a band, as a finished shell job does: ⇠, what came
in (answer, comment) and the page's title, with the count of fields in the
right rail. An answer's fields sit in a gutter under it, key and value side by
side, four while collapsed; a comment shows where it is on the page and what it
says. Click or ctrl+o shows all of it. A page that was deleted or expired gets
a gone band. A message with no changes in its details shows its text under
the band.
Other tools. Every other tool's rows get the same band, with the time and any failure in the right rail. pi-extras's own tools have layouts of their own:
- web_search, kagi_search: the query and how many results came back, with the first three under it.
- computer_use: the apps the script used and how many calls and
screenshots it took, with the last calls under it. A failed call says
failedornot allowedin words. Thewindows_usetool, now in a package of its own, gets the same row with its VMs. - usage: each window's use in the band itself, amber from 80% and red when a limit is spent.
- artifact_publish, artifact_ask, artifact_check, artifact_feedback,
artifact_find (the artifact tools): the band says what the call does in a
word,
publish,ask,check,feedback,find artifactsorread artifact, and what it is about (ask Garden plan, 4 questions, private). Under it sit the page's address (the inbox link for a private page), when it expires and how its browser check went, one line each. Anaskrow shows the form and its page. Problems fold into… 2 problems; ctrl+o lists each one and where the check image is kept. The rows never draw an image: a check image the model got is the linecheck image sent to the model. Feedback rows count the answers and comments, and a search lists one line for each hit. With an older artifact extension, whose results have no details, a row shows the first lines of the result's text. The old tool names (artifact_new,artifact_answers,artifact_comments) get the usual band. - codemode: a JavaScript band with numbered
ƒtool-call cells, distinct from shell steps. While arguments stream, a bounded preview shows the newest four JavaScript source lines, or “Writing JavaScript…” until source arrives. It does not label draft calls as queued or executing. Live statuses and elapsed times come from Pi's nested-call events, never guesses about JavaScript statements.overlapmeans call lifetimes overlap, including queue and permission waits; it does not claim parallel execution. The collapsed row keeps the newest four calls. Expand or click for the full script, individual call output and script result. The popup keeps Source and Result views fixed and follows selected call IDs as calls arrive. The whole script scrolls in the body. Copy Script preserves ordinary source text but removes unsafe terminal controls, without changing execution; Copy Preview copies only retained call output. Missing or omitted previews cannot be copied. Truncating live output previews does not mark call history incomplete. Restored calls use Pi's saved metadata and say when nested output was not saved. Older/foreign implementations without call metadata retain their own result renderer./tool-display others offleaves codemode's original row;chains offaffects bash only. Script and tool behavior are unchanged.
Other extensions' tools, such as MCP, subagent and web access tools, keep their own words: the band shows the line the tool would draw for its call, and under it sit the first four lines of the tool's own result. A tool with nothing of its own to say shows its most telling argument and the result's text. The popup lists every argument and the whole result. Rows that already draw a band of their own, such as Shell Jobs, are left as they are.
Drawing other tools' rows relies on how Pi builds a tool row, which is not part of Pi's extension API. If a Pi update changes it, those rows are drawn by their own tools again; Pi's built-in tools keep the band either way.
Thinking. While the model thinks, its newest three lines show above queued
messages and the editor divider. Paragraphs and list items are joined with ·
rather than taking lines of their own, so the three lines hold as much as fits. When thinking
ends, the transcript keeps one ∴ Thought for 12.0s row. The time shows
tenths below one minute (0.4s, 54.9s), then minutes and seconds (1m05s).
Every place that tells a thinking time uses this form and the same measure.
Click the row to read the whole block, and again to go back; ctrl+t does the
same for every block. /tool-display thinking collapsed shows just the label,
as Pi does, and /tool-display thinking full shows everything. In the
collapsed style, live thinking does not show until you push ctrl+t. Then its
newest three lines show above the editor.
In the subagent inspector, a child's live thinking shows in the chat: its
newest three lines, or all of it in the full style. The inspector reads
Pi's hide-thinking setting when it opens. If you pushed ctrl+t before that,
the inspector flips the style (all of it in place of the three lines, and
the three lines in place of all of it).
Folded mode. Folded mode is off by default. /tool-display folded on
turns it on. Then the transcript shows no tool rows and no thinking. Each run
of work between two replies becomes one line. The line hangs under the reply
that made the calls, set in two columns with no blank line between them:
● I'll find where the footer sets the model cell width.
● Ran 4 commands, read 1 file, edited footer.ts, ↑875k ↓1.4k 8.8s
The words count the calls by kind. Each step of a chained command counts as a
command, and the calls in a codemode script count in its place. When all the
edits (or all the writes) in a run are to one file, the line names that file
(edited footer.ts). The artifact tools have kinds too: published 2 pages,
asked 1 form, checked 1 page, searched artifacts 1 time and read the inbox 1 time; their old names count with the new ones. Tools without
a kind of their own, such as MCP tools, are counted together: used 44 tools,
or used 2 other tools after the known kinds. Only one such tool with a short
name is named (called usage 2 times). One failed call is named in red
(ls node_modules failed); two or more are counted (2 failed). When the
words do not fit the width,
the line shows the total count, for example ● Used 45 tools. After the words
come the tokens that the replies sent (↑, cache included) and received
(↓), and the time. The words are gray and the bullet and figures are a
darker gray. While the model works, the line is live and its figures count
up. The words tell only what the model did: a call counts as soon as it
shows, in the past tense, so the words never change when the run ends. The
phase spinner's own spinner, 3 cells wide, takes the bullet's place and
tells what the model does now: the ping while it waits for the model, the
helix while thinking text streams, the print head while it writes a call,
the comet while a call runs, and the two comets while every running call
waits on another agent. While the line is live, the phase spinner's status
line above the editor shows no spinner, so only one spinner moves. A line
under a reply has room for it in its margin,
so its words stay where they settle. A line at the left edge has no margin,
so its words start 2 columns further in while it is live. The words of a
live line keep the same gray, and a soft band of light crosses them from
left to right, as on the phase spinner's word. With
/tool-display motion reduced, the words and the spinner do not move. A run
with no calls yet shows no line until thinking text comes. Then the line
says how long the model has thought, for example Thought for 2.5s. A reply that thinks
and then writes keeps its thinking in a line above its words: the line of the
calls before it, or a line of its own directly above the words, such as
∴ Thought for 2.5s. That line looks like a finished thinking block in the
unfolded view and has no figures. Its time is the time that the thinking
took, not the time of the run. Click a line to open its run. Its rows and
thinking blocks show inside a rule under the line, the line gets brighter, and
the rule ends with ╰─ close. Click the line or close to close the run. A
click on a row inside the rule does not close the run. ctrl+o opens every
run, the ∴ Thought lines too, and ctrl+o again closes every run. A click
after ctrl+o opens or closes one run.
Thinking in folded mode:
- A
∴ Thoughtline opens to the full thinking text, one time, with no second label. A click on the text or ctrl+t does not collapse it. - In an open run with calls, the thinking of each reply shows as a label. A click on the label or ctrl+t flips it to the full text and back.
- While the model thinks and the run is closed, only the line shows. Push ctrl+t to show the newest three lines above the editor.
- While the model thinks and the run is open, the newest lines show inside
the rule (all of them in the
fullstyle). A click or ctrl+t flips them. They then do not show above the editor, so live thinking shows in one place only. - When the thinking ends in an open run of thinking alone, its full text stays, as above. In an open run with calls, the thinking becomes its label.
- A provider can keep its thinking text to itself. An open run of such
thinking shows
no thinking textin its rule.
The end line of a prompt is the same as in the unfolded view. The subagent inspector is not folded.
Compaction. A purple header band shows the reason (auto, manual or
overflow), tokens before and estimated tokens after (~), cost when known,
and elapsed time. The first three summary lines sit under it on Pi's
compaction purple. Click the row or use ctrl+o to read the full Markdown summary.
Timing and sizes survive a session resume; older compactions show only their
before size. Pi's separate billing notice remains when enabled.
Chained commands. A bash command joined with &&, || or ; is shown as
its steps, each with its own status and time, so you can see which one failed
and which never ran. The running step's line breathes gently, and each step
that finishes flashes green (or red, or amber) and fades back, so a run of
quick steps reads as a wave down the list. A leading cd becomes the
location instead of a step.
While a plain sleep 30 runs, as the command or as the running step, the
band fills over those 30 seconds and the rail shows the time left
(2 of 3 18s left). On Linux, Tool Display also watches the sleep
processes the call starts, so it knows the real length of sleep $s and how
many passes a loop has made:
- A
forloop over a list it can count (1 2 3,{1..30},$(seq 1 30)) fills over all its passes:3 of 10 ~4m left. When each pass only sleeps, the time is exact; otherwise each pass is taken to be as long as the last one, and the time left starts with~. A loop that canbreakearly fills toward its last pass. - A
whileoruntilloop has no end to fill toward, so each sleep fills on its own and the rail counts the checks:check 4 12s left.
A wait that the timeout would end early keeps the timeout's fill, so a near
kill still shows. See security and privacy
for how the sleeps are found.
To time each step, Tool Display adds a marker line around each step before the
command runs and removes the markers from the output before Pi or the model
sees it. The model's command and the output it reads are unchanged. Commands
Tool Display can't split safely run exactly as written. Examples are
heredocs, if, for, while and case blocks, function definitions and
background &. Others are exit, set and other commands that use the
shell's state, reads of $LINENO or $_, and any mention of alias or
eval. A pipeline continued on the next line and a list of more than 12
steps also run as written. See security and privacy for
the full list and for what the rewrite does.
/tool-display switches it:
/tool-display on|off: Tool Display's rows, or each tool's own./tool-display others on|off: draw other extensions' tool rows with the band, or leave them to their own renderers./tool-display chains on|off: break chained commands into steps, or run them as written./tool-display motion full|reduced: the reduced setting drops the sweep, the running step's breathing and the finish flashes. It holds spinners on a still frame and updates times every second./tool-display thinking tail|collapsed|full: how thinking blocks rest./tool-display folded on|off: one line for each run of work (see above)./tool-display count calls|steps: how Status Plus counts tools (see below).
The choices are saved in pi-extras.json. Rows change only in the terminal UI;
print, JSON and RPC runs keep Pi's tools untouched. If another extension
already replaces one of Pi's built-in tools, Tool Display leaves that tool's
definition alone and draws its rows as it does any other extension's.
Tool count. Status Plus counts tools as the folded lines do: each step
of a chained command, each call a codemode script made, and every other call
that ran as one. Subagents' calls count the same way. Where Tool Display saved
the steps a chain ran, the count uses them, so a chain that stopped early
counts what it ran; elsewhere it counts the steps in the command text. Calls
in a failed or aborted reply never run and are left out. Click the count in the footer to count one per
call instead, as Pi does, and click again to go back. The choice is saved.
Where the terminal sends no clicks to the footer, /tool-display count calls
does the same.
Shell Jobs are bands, like any call, and a running job's band fills with
its progress. A job is named after its title (Run unit tests becomes
run-unit-tests), and the model is asked to call it by its title when talking
to you. Starting a job leaves a still gray band in the transcript:
Run unit tests $ npm test ⇢ background, its title first, then the
command and status side by side. When the job ends the band takes how it ended and how long it
took (green ✓ exit 0 · 29.9s, red ✗ exit 2, gray ■ stopped), and its
completion repeats the band with the last output lines in a gutter under it;
click it, or ctrl+o, for the log and the rest.
Each running job has a live band above the editor. When the job's progress
can be known, the band fills with it: a command that starts with
sleep 30 (after at most one cd) fills over those 30 seconds, a loop's
sleeps fill as on a bash row (2 of 4 · 25s left, check 3 · 18s left), and a meter in its newest output line
fills it with what the meter says, as in
Download ISO $ curl -LO … 26.1s 45% · 32s left · 312M/690M · 12.1M/s.
The meters read are the ones tools draw into a log: curl, wget, rsync
(--info=progress2), git (--progress), tqdm, ninja, and pip or cargo when
they draw one, as well as any percentage drawn beside a bar. Time left reads
the same for all of them (32s left). A bare percentage counts only while it
climbs, so coverage 85% never fakes a bar, and a finished meter stops
filling, since the job may still have work after it. When nothing says how
far along it is, a sweep crosses the band and its latest output line shows
instead (▸ 212 passing). The spinner at its start dims
to a slow crawl when a log goes quiet. Four jobs fit; the rest give way to
+2 more. Click a job's band to open its live log over the full terminal,
with the same band at the top and buttons (or c and o) to copy the
command and the whole log; Esc, q or ✕ closes it.
Jobs continue through a /reload and through a restart that auto-update
does to install a new Pi. At a restart, the old Pi writes the session's jobs
to a hand-over file (mode 0600, in <agent dir>/shell-jobs/handover/). The
next Pi for the same session takes them over. They show in the widget and in
shell_job list, logs and kill. Each job sends its completion one time.
A job that ended while no Pi ran reports when the new Pi starts. A small
/bin/sh wrapper writes each job's exit code to a file next to its log. If
that file is missing and the process is gone, the completion says "ended,
exit code unknown". The new Pi signals a job only while its process has the
start time that was recorded. Thus a reused pid never gets a signal. If no
Pi takes over a hand-over within 24 hours (for example, the restart failed),
the next Pi that starts for any session stops its jobs whose processes are
still the recorded ones. Their logs stay in the temp folder. Every other end of a session stops the jobs as before: a quit, Ctrl+C,
/new, /resume or a fork.
The wrapper has two small effects on every job. The job's shell is not the
leader of its process group, so kill -- -$$ in an inline command finds no
group; kill 0 still stops the whole group. When the job's shell itself is
killed by a signal, the completion shows exit 128+N (for example exit 137
for SIGKILL), not the signal name.
Every request carries the declaration of every active tool, so a tool the
model calls once a week costs its tokens on every turn. Name such tools in
pi-extras.json and requests leave them out, while codemode scripts can
still call them:
{
"toolLoadout": {
"codemodeOnly": ["get_goal", "create_goal", "propose_goal_draft", "gov_search", "stop_subagent"]
}
}It works for any package's tools. A script sees each hidden tool's full
description and rules through describeTool(name), so a tool keeps its
limits. codemode itself and the file tools are never hidden, and without
codemode nothing is hidden. Tell the model in your AGENTS.md which tools
are hidden and to read describeTool(name) before the first call. Subagents
keep every tool declared, since they have no codemode. The list applies at
the next session start or /reload.
A message that another extension sends can name a hidden tool, for example
the /goal prompts of pi-agent-goal ("Call the propose_goal_draft tool").
Such a message gets one more line that tells the model to call that tool from
a codemode script. The line goes into that user message only, so the system
prompt and the tool list stay the same for the whole session, and the prompt
cache stays valid. What you type yourself is never changed.
Every request declares every active tool, and a declaration costs input tokens
on every turn. Anthropic's tool search lets a request mark a tool
defer_loading: true. The prompt then does not hold its declaration, and the
model loads the tool through a search when it needs it. Meridian gives the
flag to Claude Code's tool search.
Defer Tools adds defer_loading: true to the tools that you name, in each
request to Meridian. A request goes to Meridian when its model uses the
anthropic-messages API and has a Meridian base URL or sends the
x-meridian-agent header. Requests to all other providers do not change.
It is off by default. Older Meridian builds send deferred tools in full and add a hidden model turn to each turn with a tool call, so deferral costs more than it saves there. Turn it on only with Meridian 1.89.0 or newer, which keeps tool search and has no such extra turn. This is the recommended list (tools used in under 5% of sessions):
{
"deferTools": {
"names": [
"agent_send", "list_peers", "agent_request", "intercom",
"artifact_feedback", "artifact_check", "gov_search", "stop_subagent",
"get_goal", "create_goal", "propose_goal_draft", "complete_goal",
"update_goal_progress"
]
}
}names: the tools to defer. With nonames, or"names": [], it defers nothing.meridianBaseUrls: the base URLs that are Meridian. The default is["http://127.0.0.1:3456", "http://localhost:3456"].PI_EXTRAS_DEFER_TOOLS=0turns it off for the run.
A change to the tool list rewrites the whole prompt cache. So each session keeps the list that it had at its start, for its whole life:
- A new session reads
pi-extras.jsonand writes its list into the session as adefer-tools/frozenentry. - A resumed, forked or reloaded session uses the list in that entry and does
not read
pi-extras.json. - A session that had model turns before it had an entry defers nothing, so deferral never starts in the middle of a session.
A config edit thus applies only to new sessions. The same request always gives the same bytes, so a cache warm repeats the real request.
Some tools are never deferred: read, bash, edit, write, codemode and
powershell. A tool that already has a defer_loading value keeps it. A
listed tool that has cache_control is deferred too, and its
cache_control is removed, because Anthropic does not accept both on one
tool. Meridian does not send a tool's cache_control upstream, so this
changes nothing there, and the deferred set does not depend on Pi's cache
retention setting. When every tool of a request would be deferred, the
request does not change, because one tool must stay loaded.
A tool that Tool Loadout hides (toolLoadout.codemodeOnly) is not in the
request, so Defer Tools does nothing for it. Subagents do not load Defer Tools.
Code blocks and quotes in the agent's replies are drawn as cards on a
background of their own. A code block's header shows its language and a
copy label; a quote's label sits at the right of a line near its top that
has room, or on a row of its own. Click a code block's header, or a quote's
label, and the block's exact text goes to the clipboard: the code as written,
tabs included, and a quote without its > markers or the line breaks
wrapping added. The label reads ✓ copied for a moment after. A card ten
rows or taller has a label at its foot too (a quote's last line, or a row
under it), so one is in view from whichever end you scrolled to.
Clicks need Pi's fullscreen mode ("tuiMode": "fullscreen"); in the regular
mode the terminal owns the mouse, so the cards keep their background and
leave the labels out. /copy-block works in both: it copies the last code
block or quote of the latest reply, and /copy-block 2 the second.
Drawing the cards relies on how Pi builds an assistant message, which is not
part of Pi's extension API. If a Pi update changes it, replies are drawn as
Pi draws them and /copy-block still works.
pull_link reads a link and gives the agent its content as markdown. Paste a
link into the prompt, and Pull Link reads it before the model starts (see
Auto-pull). The agent calls the tool for other links and for
more of a result.
In the transcript, a pull_link call is a Tool Display row. The band shows
the link in short, the options of the call and, after the read, the platform.
While the tool works, the right side of the band shows its step, for example
reading captions. Under the band are the title and a summary of 3 lines or
fewer, for example the stars of a repository or the first reply of a thread.
The summary is for you only. The model does not get it. Click the row to see
the full result.
| Link | What you get |
|---|---|
| X/Twitter | The whole public conversation, through the public FxTwitter API (no login): the post with its media and polls, all the posts above it, the replies as a nested outline (the author's own replies first, then most liked), the posts that quote it with their replies, and the chain of quoted posts (up to 4 levels, each with the posts above it). Without a login, X gives only the top replies of each post. The result says what X reports and what came back. If the newer API fails, you get the post, its quote and the posts above it. |
| Bluesky | The whole conversation, through the public AppView API: the post, the posts above it, the full reply tree (most liked first), and the posts that quote it, each with its replies. |
| Threads | The post text and image from the page's preview tags. Replies need a login. |
| Mastodon | The post, the posts above it and the replies, through the server's public API. Any server with /@user/<id> links works. |
The post and its comment tree, through the JSON view of old.reddit.com. Share links (/s/...) work. |
|
| Hacker News | The story or comment and its comment tree, through the Algolia API. |
| The post and its top comments from the page's JSON-LD. | |
| GitHub | A repository and its README, issues and pull requests with comments, files, folders, commits, releases and gists. Set GITHUB_TOKEN or GH_TOKEN for more than 60 requests an hour. |
| YouTube, TikTok, Instagram, Vimeo, Twitch and other yt-dlp sites | Title, channel, date, counts, description, chapters, the thumbnail, a transcript with time stamps, top comments (comments) and frames on request (at, frames). |
An image (.jpg, .png, .webp, .gif, imgur, i.redd.it, or a link whose server sends an image content-type) |
The image. |
| Any other page | Title, author, date, description and the readable text of the page. |
The photos of a post come back as images. Image links in a post or comment
(imgur, i.redd.it and direct image files) also come back as images, up to 6
images in one result. An imgur album needs imgur's API, so it stays a link.
The tool gets the type and the size of each image from its bytes, not from the
server's label. It sends only JPEG, PNG, GIF and WebP files of 3.7 MB or less
that Pi's image library can decode. It makes an image larger than 2000 pixels
on a side smaller.
The tool saves a result longer than 12,000 characters to a file and gives
its path, so the agent can read or search the file. An X or Bluesky
conversation longer than one page (40,000 characters) comes back as a
preview: the post, its quotes, the nearest posts above it, the top replies,
the top quote posts and the notes about what is missing. The agent can page
through the whole text with offset (start at offset=0). Other long
results come back one page at a time, with the offset of the next page.
The files are in
~/.cache/pi-extras/link-context/full-text (only your account can read
them). A file is removed after 7 days, and the folder keeps at most 200
files.
An X conversation can need many requests: each reply that has replies gets its own request. One X link uses at most 300 requests and 90 seconds. Most posts take 2 to 10 requests and a few seconds. A post with thousands of replies can take more than a minute.
A video result is text and one thumbnail. It has no frames, so a long video
does not fill the context. The transcript ends with a hint: to see a moment
that the transcript mentions, call the tool again with at.
Arguments:
at: frames at exact times, for example["4:05", "1:02:30", "95"](up to 12). This call returns only the frames, with a short heading. Settranscript: trueto get the transcript too. A video of 10 minutes or less is downloaded one time at 720p and kept for later calls. For a longer video, the tool cuts a 2-second clip at each time, so it does not download the full video.frames: the number of frames to sample evenly over the video or therange(0 to 48). The tool sends 1 to 4 frames as separate images. It puts more frames on contact sheets of 12, each frame with its time stamp.- For
atandframes, the tool keeps each frame at full size, and the result gives the folder for thereadtool. range: a part of a video, for example12:30-15:00,90-or-0:45. The transcript and the sampled frames use only this part.comments: the number of replies or comments (default all for X and Bluesky, 20 for other posts, 0 for videos). Use a smaller number for a shorter result.quotes: X and Bluesky: the number of posts that quote the post, most liked first (default all, 0 for none).transcript: for a video link, the default is on. For a post that has a video (X, Bluesky, Mastodon), settranscript: trueorframesto read the video too.images: send photos, image links and the video thumbnail as images (default on).offset: a result longer than 40,000 characters comes in pages. The result gives the offset for the next page. The tool keeps the result for 30 minutes, so the next page does not send new requests.
The tool reads only public http and https links. It refuses local and
private addresses and links with credentials. It checks each redirect of its
own requests in the same way, and it sends a GitHub token only to GitHub's
API. It does not resolve DNS for its own requests, so a public name that
points to a private address passes.
Some sites refuse plain HTTP clients. When a page answers HTTP 403, 429 or
503, shows a bot check (for example Cloudflare "Just a moment..."), does not
answer in time or drops the connection, the tool reads the page again with a
local headless Chrome or Chromium. The result then starts with "Read with a
local browser" and gives the reason. A page that is not found (HTTP 404 or
410) stays a failure, and the error says "page not found". A 429 with a
Retry-After of 10 seconds or less gets one retry before the browser.
- The tool uses a browser that is already installed: Google Chrome or
Chromium from the system, then the Chromium of Playwright, then the
Playwright headless shell. When it finds no browser, the error gives the
command that installs one:
npx playwright install chromium(about 170 MB, no root). The tool does not download a browser by itself, because a blocked page in an auto-pull must not start a large download. - Each read uses a new temporary profile, which the tool removes after the read. The browser does not load images. It stops loading after 10 seconds, and the tool stops the browser and all its processes after 30 seconds or when you cancel. The page can be 8 MB at most. Two browsers run at the same time at most.
- The browser sends each request through a small proxy in the tool. The proxy resolves the name and refuses private, loopback, link-local and reserved addresses, also after a redirect.
- To turn the browser off or to use a different browser, set
linkContext.browser(see Settings).
The video tools (yt-dlp, its HTTP libraries and ffmpeg) send each HTTP request
through a small proxy in the media helper. This includes redirects, manifests
and fragments. The proxy resolves the name and refuses private, loopback,
link-local, metadata and reserved addresses. The tool does not download media
or captions on other protocols, such as ftp:// or rtmp://, and ffmpeg can
read only HTTP. yt-dlp does not start other downloaders, such as rtmpdump.
When you set linkContext.proxy or a proxy variable (HTTPS_PROXY,
HTTP_PROXY or ALL_PROXY), the guard still checks each request. Then it
sends the request to your proxy. linkContext.proxy comes before the
variables, and with it the tool ignores NO_PROXY. The variables follow the
rules of yt-dlp: HTTPS goes to HTTPS_PROXY, then HTTP_PROXY, then
ALL_PROXY, and plain HTTP goes to HTTP_PROXY, then ALL_PROXY. A host in
NO_PROXY gets a direct connection, and the guard checks it too. Your proxy
gets the IP address that the guard checked, not the name.
The guard finds each name with the DNS of this host, also for socks5h://.
DNS lookups do not go through your proxy, and .onion names do not work. A
proxy with rules for names sees only IP addresses. The guard cannot see
a name that is private only in the network of your proxy. An HTTP proxy can
get only a Basic login. A proxy URL without a scheme is http://, and
socks:// is socks4://. Other proxy URLs must start with http://,
https://, socks5://, socks5h://, socks4:// or socks4a://. If they do
not, the tool stops before it sends a request.
A shared YouTube link with &list= reads only its one video, not the
playlist.
When you send a message with links, Pull Link reads the links before the model
starts. It adds the results to the session as one message after your message.
The model then has the content in its first request, so it does not need a
pull_link call for these links. This saves one model turn for each message
with links. The cost is the size of the results in the context, also when the
model does not need them, and a wait before the model starts.
- Only messages that you type or send through RPC are read. Messages that an extension sends and the tasks of subagents are not.
- Auto-pull finds the
httpandhttpslinks in the message, also links in code spans and code blocks. It removes the punctuation after a link, such as.,,, quotes and a)that has no(in the link. It reads each link one time. It does not read local and private addresses or links with credentials. - It reads up to 5 links for each message. The message names the next 5
links and gives the number of the others. The model can read them with
pull_link. - Each result is what
pull_linkgives for the link, up to 12,000 characters. A longer X or Bluesky conversation comes as a preview. Other long results come as their first 12,000 characters. Both give the path of the file that holds the whole result, and theoffsetto page through it. - Auto-pull reads an X or Bluesky conversation for at most 20 seconds. Only
the first request for an X post can take longer. When this limit stops the
read, the message says so,
and a
pull_linkcall for the link reads the whole conversation. - One message has up to 4 images in total (photos and thumbnails), in link order.
- A video gives its title, description, chapters and the transcript from its
captions. Auto-pull does not transcribe speech and sends no frames. For a
video without captions, the model can call
pull_link, which transcribes the speech. - All links are read at the same time. The model starts when all links are
read, or after 15 seconds (25 seconds when the message has an X or Bluesky
link). The message names a link that is still being
read. Its read continues for up to 5 minutes, and a
pull_linkcall for that link waits for this read and uses its result. - The results go into the
pull_linkcache. A link that was read in the last 30 minutes is not read again. - Each public link in a message gets a request, so a link that works only
one time (a password reset, an unsubscribe or a sign-in link) is used up.
Use
/autopull offbefore you send such a link. - The first video link installs the media tools (see Video tools), also when auto-pull reads it.
- A link that cannot be read is named with the reason. It does not stop the other links or your message.
- A message that you send while the agent works (a steer or a follow-up) gets its links in the same way. Pi adds the results at the end of the current turn, so they come before your message in the next request.
While Pull Link reads the links, a band above the editor names each link. A link that is read shows in the text color, a link that is still being read is dim, and a link that failed is red. The band also shows the time against the 15-second limit. In the transcript, the result is a band with one line for each link:
● pull_link 2 of 3 links from your message 1 failed 827ms
│ GitHub earendil-works/pi
│ en.wikipedia.org Terminal emulator - Wikipedia 1 image, first 12k of 24k chars
│ httpbin.org failed: HTTP 500
The band is green when Pull Link read one link or more. It is red when all
the reads failed. As on a tool row, the failures and the time are on the
right. Three links show. Click the band or press ctrl+o to see all the links,
each with its address and its summary. Auto-pull uses the same network rules
and the same guard as pull_link.
To turn auto-pull off, set linkContext.autoPull to false. To change it for
the current session only, use /autopull on or /autopull off. /autopull
or /autopull status shows the current state.
Some models make up URLs and fetch them. Qwen3.8 builds send signed Alibaba
OSS links (routify-file-proxy-sg...aliyuncs.com) from their training data,
also when their reasoning names a different page. Each request tells a third
party your IP address and the time. The host guard refuses a pull_link or
fetch_content call when no user message, tool result, custom message or
system prompt names the host or a domain above it. The model's own text does
not count, and nor do subagent reports or a tool result that only repeats
its own call's arguments. Search results and a codemode script's nested
results count. On shared hosting domains (aliyuncs.com, amazonaws.com,
github.io) only the exact host counts. The refusal writes
the host as a[.]b[.]com, so the refusal itself does not name it. The model
then uses a URL from the task or searches first.
The guard checks models from all providers except those in
hostGuardExempt (default: Anthropic). A model that names real hosts from
memory (raw.githubusercontent.com, web.archive.org) gets one refusal and
searches. Add such hosts to hostGuardAllow. The guard does not examine
bash.
The first video link installs the media tools into
~/.cache/pi-extras/link-context (or $XDG_CACHE_HOME/pi-extras/link-context,
or PI_LINK_CONTEXT_HOME). This takes 20 to 60 seconds. It needs git and
npm for the token provider. If they are missing, the tool tries again after
a day, and captions and other sites still work. It does not need sudo,
a browser or a desktop, so it also works on a headless Linux host. The tools are:
- A private Python (from
uv) withyt-dlp,youtube-transcript-api, a static ffmpeg (imageio-ffmpeg) and Pillow. The tool uses the system ffmpeg when there is one: the static build can crash on network input, which yt-dlp uses to cut clips. Pillow draws the time stamps on the frames, so no system fonts are necessary. - The bgutil PO-token provider, built with the Node that runs Pi. Without PO tokens, YouTube refuses most downloads and captions. The tool installs the latest release, and the yt-dlp plugin of the same version.
Video sites often change and stop old versions of these tools, so the tool
updates them every refreshDays days. If an update fails, the tool continues
to use the installed version. With PI_OFFLINE=1 (or true or yes), the
tool does not install, update or look up a release. It uses the installed
tools, and without them a video link fails with a note. But a speech model
that is not in the Hugging Face cache still downloads on the first
transcription. The token provider's npm ci runs with --ignore-scripts.
The transcript comes from the captions of the video. If the video has no
captions, the tool downloads the audio and transcribes it on this machine. It
uses mlx-whisper (whisper-large-v3-turbo) on Apple silicon and
faster-whisper (small, CPU) on other hosts. The first transcription installs
the package, and the package downloads the model from Hugging Face. The tool
does not pin or check the model, and this download does not go through the
guard proxy.
The tool does not download live streams. One download stops at 2 GiB of video
or 1 GiB of audio. When the length of a video is not known, the tool does not
download all of it or transcribe it. Give a range with an end to read a part.
YouTube blocks most datacenter IP addresses ("Sign in to confirm you're not a
bot"). Then the result has the title and channel from YouTube's oEmbed, the
captions if the caption client can still get them, and a note. On a cloud
host, set cookies to a cookies.txt file from a signed-in browser, or set
proxy. yt-dlp writes its cookie jar back when it stops, so the tool gives it
a private copy of the file in a temporary folder, not the file itself. The tool
removes the copy when the media helper stops, also when it kills the helper.
The install steps and the media commands get a minimal environment. It holds
the path, the home folder, the user name and shell, the locale and time zone,
and the temporary and XDG_ folders. It also holds the Hugging Face cache
folders and the proxy and certificate settings. They do not get other
variables, such as API keys. Only the install steps (tar, uv, pip, git,
npm and the TypeScript build) also get the UV_, PIP_, npm_config_ and
NPM_CONFIG_ settings, because a private package index needs them. yt-dlp,
the media helper and the token provider do not get a variable whose name
contains TOKEN, PASSWORD, AUTH, SECRET or KEY.
Put these in the linkContext section of pi-extras.json:
autoPull(defaulttrue): read the links in your messages before the model starts (see Auto-pull).refreshDays(default3): the days between updates of the video tools.0stops the scheduled updates. A new pi-extras version that changes the list of video packages still installs them again.proxy: a proxy URL for yt-dlp and the YouTube transcript client, for examplesocks5://host:1080. The URL can have a user name and password, for examplehttp://user:pass@host:8080(Basic login only). The guard checks each request before it goes to this proxy, and the tool ignoresNO_PROXY(see Pull Link).cookies: the path to a Netscapecookies.txtfile for yt-dlp.browser(defaulttrue): read pages that refuse plain HTTP with a local headless browser (see Pull Link).falseturns it off. A path, for example/usr/bin/chromium, uses that browser.hostGuard(defaulttrue): refuse apull_linkorfetch_contentcall to a host that nothing in the conversation named (see Host guard).falseturns it off.hostGuardExempt(default["anthropic"]): the providers whose models the host guard does not check. A provider exempts its whole provider family (see Status Plus), so the default also exemptsclaude-code.[]checks every model.hostGuardAllow(default[]): hosts that always pass the host guard, with their subdomains, for example["web.archive.org"].asrModel: a different speech-to-text model, for examplemlx-community/whisper-small-mlxormedium.userAgents: a different list of user agents for an adapter, for example{"reddit": ["..."]}. The adapters arex,bluesky,threads,mastodon,reddit,hackernews,linkedinandweb. Each adapter tries its agents in sequence until a site gives a usable answer. Reddit sends a plain client to a login page but answers a link-preview agent, so the Reddit adapter triesfacebookexternalhitfirst.
The subagent tool starts a child agent: a separate Pi session on the model
the agent picks. It has a fresh context, the same working directory and the
same tools, minus a few that make no sense in a child. A child never gets mesh,
goal and shell job tools, computer_use, windows_use, usage, codemode,
tool_search, or other subagent extensions' tools. The full list is in
security and privacy. Children load the
extensions that provide their tools, Cache Compaction and a quota guard. Other
extensions, such as the remote-pi mesh, keep serving the parent session. It
runs in the background and its report arrives
as a message, so the agent keeps working or ends its turn and is woken when it
matters. wait: true blocks instead, for a quick check. readOnly: true keeps
only tools that can't change your files or run commands that the model
chooses: read, grep, find, ls, pull_link, kagi_search,
web_search, fetch_content, get_search_content and web_fetch. It
drops every other tool, also powershell and unknown extension and MCP tools. tools gives the child only the named tools
of the ones it would get, for example ["read", "grep", "web_search"]. It
always keeps message, and subagent and stop_subagent when it may start
subagents. The call is refused, with a list of the tools the child can have,
for an empty list, a name it can't have, or a tool outside that list with
readOnly: true. Its own subagents get no more tools than it has, the
subagents of a read-only child are read-only too, and a resume or a restore
keeps the same tools. context: "fork" gives the child a
condensed copy of the conversation up to the spawn, with mesh messages main
got. A relaunch gets the same copy, not what came later. cwd starts the child in another
directory, as an absolute path or one that starts with ~/, for example
{ task, cwd: "~/dev/app" }. Its tools, AGENTS.md and the paths it edits
then start from that directory. That directory's project settings and
resources (.pi/) load only as Pi would load them without a prompt: when
nothing there needs trust, when you trusted the directory, or when
defaultProjectTrust is "always". The directory must exist; links in its
path are resolved. The child's own subagents start there too unless they name
a cwd of their own. A resume or a restore keeps it.
Models. A child may run on any of the session's scoped models
(enabledModels, the list /scoped-models shows); short names such as luna
or opus work when they match one. The list is read at each call, so a change
in /scoped-models applies at once, and the tool's model list follows at the
next prompt. After you edit enabledModels in settings.json, run /reload.
Pi itself keeps its old list after a reload, so subagents resolve the changed
patterns. When a call names a model that Pi knows but the scope does not
include, the error says why and how to add it. An empty scope (all models
selected in /scoped-models) gives subagents only main's model. A child's
thinking level is the call's, else the one Pi's modelThinkingLevels sets for
that model, else your default. These settings come from the project's
.pi/settings.json only when main trusts the project. Which model suits what is yours to say in a Markdown guide,
~/.pi/agent/subagent-models.md:
- luna: extreme cost savings; bulk search, summaries, mechanical edits
- gpt-6.1 sol: reliable worker for implementation
- opus 5.5: taste; design, naming, reviewing plansIt goes into the tool's description with the model list. When the guide has a
non-empty ## Summary section, only that section goes in, with a pointer to
the file: keep the rules that decide a choice in the summary and the evidence
below it. Claude Code, which Meridian runs, cuts a tool description at 4,096
characters, so the description stops at that length and cuts the guide's end
with a note: put the bans and the rules that matter most first. Pi reads it when a
session starts and on /reload, never mid-session, since a changed tool
description busts the prompt cache. /subagents guide edits it, or ask the
agent to.
When ~/.pi/agent/AGENTS.child.md exists, a child gets it in place of
~/.pi/agent/AGENTS.md, and it drops the AGENTS.md files above its project
root (the git root of its cwd). Put there the rules a worker needs: safety
limits, how to report, language. Without that file, or with
fullInstructions: true on a spawn, the child gets main's context files.
Each launch of a child reads Pi's settings.json, the project's
.pi/settings.json and models.json again. It also reads the context files
(AGENTS.md), SYSTEM.md, APPEND_SYSTEM.md and skills that Pi gives a
child. The text of APPEND_SYSTEM.md comes before the child's own
instructions. A child in main's directory gets the project's .pi/ files and
settings only when main trusts the project. A child with a cwd of its own
gets them as described for cwd above. The child's extensions also read their
own sections of pi-extras.json, such as cacheCompaction and
rateLimitRecovery, at each launch. The subagents section applies at the
next session start or /reload, because it also sets main's tools. For
example, a change to compaction.reserveTokens or to a model's
contextWindow applies to the next child that starts. When models.json
changed, the launch first reads it into the model list that main and its
children share, as /model does. Before that, Pi's own loader checks the
file. If it finds an error, the launch uses the models from before and tells
you one time for each version of the file.
Talking. Every agent has a name made from its task, and main is the
session. message sends a note to an agent by name, or to all. A running
agent reads it after its current tool call; a finished one resumes with its
context to handle it. A failed or stopped one resumes too when main, its
parent or you write to it, and is told first how its last run ended; a peer's
note can't wake it. A child asks with expectReply: true. A question up the
tree, to main or its parent, waits for the answer, which comes fast because
the question wakes them. A question to anyone else, a peer or its own
subagent, returns immediately. The answer arrives as a message that wakes the
asker. The asker's report waits until every agent it asked answers or ends. One
that ends without answering, or can't resume to answer, hands the asker how
its run ended. Some agents wait for the asker, through their subagents or an
answer the asker owes them. A question to such an agent is refused; send a
note instead.
So agents never wait on each other in a loop. When a peer's message resumes a
finished child and that run ends normally, its report goes to its parent
without waking it: main reads it at its next turn, and a child parent at its
next run. A parent that waits only on its subagents wakes for it. A run that
fails or goes over its budget still wakes the parent, and so does one that
main, the parent or you wrote to while it ran. Main never waits: a child's note or question
wakes it, and so does the answer to anything main asked. When main's question resumes a finished child that
answers and then just writes its final text, the report doesn't wake main a
second time. More work, new input or a failure after the answer does, so a
report is never silently missed. Children know each other and can split work
directly.
For a review or fix loop, tell the agents to send messages to each other by
name. Tell them to report to you only at the end. The reviewer asks with
expectReply: true, so its report waits for the answer. A message from a
peer started the run of the other agent, so its report does not wake you. Two
agents can do many rounds, and main wakes one time, for the last report.
Messages to main show above the editor until they are in the transcript.
One that arrives while main works lands at its next turn boundary, and main
takes one more turn before it stops if it hasn't replied since. Esc doesn't
drop it: it stays in the transcript for main's next turn. While main
compacts, mail waits and wakes main after the compaction is done, so no turn
starts on the context being summarized. A /compact that stops main's turn
leaves the mail that landed in it unanswered. So after the compaction is done,
a hidden reminder that names who wrote wakes main to reply.
What you type to a child directly is recorded for main without waking it.
Reports from children started in the same run arrive together, as one message.
message call rows and incoming notes, questions, replies and relays read
like the conversation they are: ◆ reviewer → main asks over what it said.
A question keeps its amber asks word; failed deliveries keep red words.
Incoming mail has a Markdown preview, expandable by click or ctrl+o.
What main sees. Each message to main ends with one line about the other
agents that still work, for example
Still working: impl 37m $17.20 ctx 84k, review queued after impl. This
applies to reports, notes, questions, answers and relays. For each agent, the
line gives the time and cost of its current run, its context size (ctx,
after its first reply), and the agent it waits for. It shows 8 agents at most, then +3 more. Subagents makes the line
when Pi gets the message, not when the message goes into the queue. When no
other agent works, the message has no line. The results of main's message
and stop_subagent calls end with Team now:. It gives the agents that work
or are interrupted, and the number of agents that ended. So main does not use
a turn to ask for the status of its team.
Seeing it. An agent reads like someone in a conversation, with no band
behind it: a ◆ and its name in its provider's color, the one the footer
gives that provider (Anthropic orange, Codex blue). Every other provider, such
as a local or self-hosted server, shares one mint, in the footer too. An
agent whose model names no provider is purple. What it is doing and the facts about it sit close beside them, and what it says set in under its name. Shell Jobs are
bands instead, so the two are easy to tell apart. A backgrounded start reads
◆ reviewer joined opus high working, with its task under it; the last word
says where it is (working, an amber question, finished · 1m12s, red
✗ failed), and expanding the row shows the whole task. Its report is the
agent speaking last: ◆ reviewer reported 1m12s opus, then the run's cost
and tokens (in counts cached prompt tokens too), and the first lines of the
report under it. When its answer is already on screen, the report stays one
line and a click unfolds its text.
Each agent has a row above the editor:
◆ reviewer 1m02s 48k 24% $0.21 opus high ∴ thinking, its name, how
long this run has taken, its context (the size after its last reply and that
size's share of the point where its session compacts), what it has cost, its
model and thinking level, then what it is doing right now. The compaction point
is the model's window less compaction.reserveTokens (or the reserve in
compaction.modelOverrides for the child's model), so 100% means Pi compacts
now. The percent can go past 100 until it does. With compaction off, or with a
reserve as large as the window, the percent is of the whole window. The point
is read at each launch. The percent turns amber above 70 and
red above 90, as the footer does for main's context. After a compaction the
size is ? until the next reply, and a finished agent keeps its last size,
which is where a resume starts. What it is doing comes last, so a narrow
terminal cuts it and keeps the other facts. Names, times, context figures,
costs and models line up, so what each agent is doing starts in one column.
After it has ended, ✓, ✗ or ■ takes the spinner's place. What it is doing
moves the way main's own spinner does for the same work, so the wait for its
model, thinking, writing, a tool call being written, a tool that runs, a wait on
another agent through a tool (these three in the tool color), a compaction
and a retry (in amber) look alike everywhere; a question waits in amber. The
words say the same: waiting for the model until its model sends thinking
text, words or a call, and again after its tools are done. Children of
children sit under their parent. A child the agent waits on (wait: true)
shows the same row in its subagent call instead.
Up to four agents show whole. A fifth turns the last row into a control line,
+7 more subagents · 1 asking · 6 working (view) (expand), so the rows stay
four lines and never say +1 more. The rows kept are the ones that need you
first (asking, failed, interrupted), then those still working; finished ones
give way first. (expand) shows as many rows as fit, up to all of them. It
takes at most half of what the editor and footer leave, so the transcript
keeps the rest. (collapse) takes them back. The rows stay expanded until
then, or until the widget empties. The widget empties when every agent is done
and its report has reached main. A terminal too short to give the rows more
room shows no (expand). (view), a click on +7 more subagents, or
/subagents opens every agent over the full terminal: the same live rows,
children under parents, each with its task under it and a count of what they
are doing on top. Use ↑↓ and Enter, or click either line, to open one; Esc in
its inspector comes back to the list.
Click a row, a subagent row, or pick one in the agents view to open the
inspector over the full terminal: the agent's chat, drawn as Pi draws main's.
Its task is the first message, then its replies, thinking and tool rows, with
the same look Tool Display, Copy Blocks and the thinking tail give main, and
the reply it is writing streams in. It uses main's hideThinkingBlock and
outputPad, from the project's settings only when main trusts the project.
A click on a row works as it does in main.
The chat sits on a wash of the agent's provider color, so it never reads as
main. Its live row is the title bar, and a message box sits between two rules
in the agent's color, as Pi's editor does; the top rule carries its tool
calls, runs, and whether it is read-only (its context is in the title bar).
What other agents, you or main told it reads as conversation rows, not as the
text its model reads:
→ reviewer you wrote or main asks over the message, ◆ finder → reviewer note in the sender's color, and ◆ lead-a reported for a report from one
of its own children. Type and press Enter to write to it (steered in while it
runs, resuming it when it has ended, answering it when it asked); ctrl+x
twice stops it, and Esc or ✕ closes. Main is told what you wrote. Status
Plus counts every child in its totals.
Reports. Each finished run that has a final report saves it in full beside
its session as <session base>.run-<n>.report.md, including failed and stopped
runs. The completion message gives that path and a preview of up to 12,000
characters; main reads the rest with its normal read tool. /subagents report <name> shows the latest report's path. Each run keeps its own file, and run
numbers can skip when a run ends waiting for a reply. If saving fails, the
message still arrives and points to the session file as before.
Follow-up or fresh agent. A report of an agent whose context is 100k
tokens or more has a Context: line, for example Context: 140k tokens. A follow-up re-reads all of it each turn; for unrelated work, a fresh subagent probably costs less. The rule is simple: each turn of a resumed agent sends
its whole context again (a cache read at best, a full cache write after the
cache expires), and a fresh agent starts with only its system prompt and its
task (about 10k to 25k tokens). From 100k tokens, a follow-up turn costs about
4 to 10 times the input of a fresh agent's turn. A follow-up is still better
when the work needs what the agent learned.
Checks. check on subagent or message is a shell command, for example
{ task, check: "npm test", fixUntilPass: 2 }. When a run of the child ends
normally, the harness runs the command before the report goes to the parent.
The command runs through the child's own bash tool, in the child's
directory, with the same shell settings and worktree sandbox. It gets no
input. A timeout stops it after 10 minutes, or after checkTimeoutMinutes (60
at most). The run stays live while the check runs, and the row of the child
shows check: npm test. Then the report ends with the command, the exit code,
the time and the last lines of the output. For a failure, these are 40 lines
or 4,000 characters at most. For a pass, they are 10 lines or 1,000 characters
at most. The report file has the same text. So the parent gets data from the
harness, not only what the child says. A report with a check that did not pass
always wakes the parent. With fixUntilPass (0 to 5, default 0), a failed
check goes back to the child as more of the same run. Messages that came
during the check go with it. The child repairs the cause, and the check runs
again. The run keeps its budget, and the parent gets only the last result. The
fix rounds count across the whole run, also when the run waits for its
subagents or for an answer. The next run starts again at 0. A check that could
not run gets no fix rounds, because the child can't repair the harness. A run
that fails, is stopped or goes over its budget gets no check. A run that
called no tool that can change files since the last check also gets no check,
for example a run that only answered a question or sent messages. The tools
that can't change files are read, grep, find, ls, message and
stop_subagent. All other tools count as changes. This includes subagent
and each tool that Subagents does not know. Then the report ends with Check skipped: no changes since the last check. A change by a subagent counts for
each agent above it, also when a message resumed that subagent. The harness
counts each call when it starts, so a compaction does not hide a change. The
check runs again when its command changed or the last check could not run.
After a restart, the next check always runs. The child keeps its check for its
later runs. A message with check replaces it. A refused message leaves
the check as it was. No message removes a check; to make it pass always, set
check: "true". When that message does not
give fixUntilPass or checkTimeoutMinutes, the old values stay. Only main,
or a child that can run bash itself, can set a check. A message can set a
check only on one of the sender's own subagents. A read-only child can have a
check, but not fixUntilPass.
When a run ends normally, changes files and has no check, its report ends
with Not checked: this run changed files and had no check command. Verify the work before you rely on it. A change is an edit or write call, or a
bash command that the edit lock saw change files in a git work tree.
Stopping. stop_subagent stops a subagent and everything it started:
main can stop any, and a child that may start subagents can stop its own.
The call returns how it ended and its last message, so no report follows; a
message resumes it later. /subagents stop <name>, stop all and ctrl+x in
the inspector stop children for you, and then main gets the stopped report.
A stop ends spawns that have not started first, so a spawn that waits for a
stopped agent never starts. A stopping agent can't start new subagents. An
agent that fails stops its subagents too. Each of their reports goes to the
nearest agent above that still runs, or to main, so no report is lost. A
stop of a queued child that never started says was cancelled while queued; it never started ($0) instead of a run time, and its rows show cancelled while queued.
Names. A name that another agent has gets -2, -3 and so on, and the
spawn result says so: The name oracle was taken; this agent is oracle-2.
A new spawn may take the name of an agent that was cancelled before it ever
started, because that agent has no session, report or spend to keep. A
queued spawn that waits for that name with after keeps it reserved.
Order. after on subagent gives the agents that must end before the new
child starts, for example { task, after: ["impl"] }. The call records the
child and returns. wait: true can't go with after, because the agents
before the child could need an answer from the caller. The child stays queued
until each of these agents is idle, failed, stopped or interrupted. It does
not use a maxConcurrent slot while it waits, and its row shows after impl.
Its time budget starts when it starts. Its first prompt then tells how each
agent ended and gives the path of each report, but not the report text. It
also tells when an agent finished but its check did not pass. With
afterReports: false, the first prompt gives only the order and how each
agent ended, with no report path and no session path. Use it for an
independent second opinion that must not read the first agent's work. It
needs after, and index.json keeps it. Each name must
be an agent of the team. A name is refused when that agent can't end before
the agent that starts the child, because each would wait for the other.
Examples are the agent that starts the child, an agent above it, and a queued
child that waits for one of these. index.json keeps the names, so a restore
or a /reload keeps the wait. A restore resumes a queued child after the
agents it waits for. So when one of these agents resumes too, the child waits
for it again. An agent that stays paused counts as ended. Only the first run
waits; a resume does not. A stop removes a queued child before it starts. When
a run ends normally and a queued child of the same parent waits for it, its
report does not wake the parent. The parent reads it with the report of the
queued child. A run that fails, is stopped or has a failed check still wakes
the parent. A child can't ask a queued child a question, because the answer
can come only after the start. Such a question could also make a loop of
agents that wait for each other. A note is possible, and the queued child
reads it when it starts. For the same reason, a child can't ask a question to
an agent that waits for it through a queued child. For example, lead waits
for its subagent docs, and docs starts after impl. Then impl can't ask
lead a question: lead could end its run before it answers, and each agent
would wait for the other. A queued child does not keep the reports of its
group waiting.
Panels. models: [...] on subagent starts a panel: the same task on
each model, for independent opinions, for example
{ task, name: "review", models: ["opus", "luna", "glm"] }. Each model gets
its own subagent named <name>-<model> (review-opus-5-5), with its own
budgets. A panel needs 2 to 8 different models, and it can't go with model
or wait. mode: "parallel" (the default) starts all members at once.
mode: "sequential" starts each member when the one before ends, with
after and afterReports: false, so no member gets an earlier report. Each
model is checked before the first member starts, and when the team refuses a
member, the members started before it stop. The members' reports are held
until the last member ends, also when a member fails or is stopped. Then the
parent gets one message: a line for each member with how it ended, its time,
its cost and its report path, then each report. After that message, a
member's later runs report on their own. The panel's name gets -2 when an
earlier panel used it.
Writers. The first child to edit files in a checkout holds that checkout
until its run ends. The checkout is the git work tree that contains the edited
file: a repository or one of its worktrees. For a file outside any git work
tree, it is the child's own checkout: its worktree, else git's top level of its
cwd, else that directory itself. Children without a cwd share the parent's
checkout for these files. Paths are compared after links are resolved, so two
paths to one directory are one checkout. So children that edit different
repositories, worktrees or directories never block each other, even when the
parent works in a directory that is not a repository. Outside git the lock
follows the child, not the file: a child with cwd: "~/notes" and a child
without a cwd in ~ do not block each other, even for the same file. The holder keeps the checkout
while it waits on its own subagents, and lets it go when it finishes, fails, is
stopped or is interrupted. Its ancestors and
its own subagents can edit beside it. Any other child's edit or write call
there is refused before it runs, with a message that names the holder and the
checkout. That
child can then do work that does not edit files, or ask its parent for a
worktree. Main never takes or checks the lock. A child's bash command can't
be checked before it runs, so the check comes after it: the child's git status
(with the size and modification time of each listed file) is compared before
and after each call. Ignored files do not count. A command that changed files
takes the lock of the child's work tree as an edit would. When another child
holds it, the changes stay, the command's result tells the child to stop and ask
for a worktree, and you get a warning. This is detection after the fact, not
prevention. Files that an
edit or write call of any agent, main included, targets during the command
do not count. When another agent's bash command runs in the same place at the
same time, the changes can't be told apart, so that call is not checked. The
same place means the same git work tree, from any directory in it. Outside
a git repository, calls are not checked, and you get no warning. After a
git init there, the next call is checked. When git fails for a different
reason or takes more than 10 seconds, calls are not checked, and you get one
warning. Use
isolation: "worktree" for
parallel edits. Each worktree has its own lock, shared by the helpers that
inherit it. A worktree child's edit and write calls outside its worktree
are refused before they run, and the check runs again as each call starts. So
a link that an earlier call of the same reply made can't take a path out. The
check follows symlinks, so an edit through the
worktree's node_modules link into the parent's checkout is refused too. A
worktree child's bash commands run in an OS sandbox that blocks writes into
the parent's checkout (and, for a worktree made from a worktree, into each
checkout it came from). Some exceptions stay writable, so git add,
git commit and git status work in the worktree. In the repository's .git,
these are the worktree's own folder in worktrees/ and objects/. They are
also the ref and reflog of the child's own subagent/<name> branch. The
hooks, the config, info/ and the other branches stay read-only. In
node_modules, only .cache is writable, so that tools such as babel-loader
and webpack can keep their caches. Note that a cache can hold build output
that your own tools run later. The packages, .bin and the other dot-entries are not writable, so
an install in a worktree can't change the parent's dependencies.
The sandbox stops unintended writes into the parent's checkout. It is not a
boundary against a hostile child: the child can still write outside the
repository, for example ~/.gitconfig. When a command fails on a blocked
write, its result names the parent's checkout. The transcript shows the child's own command, not the sandbox around
it. macOS uses sandbox-exec. Linux uses bwrap when it is on PATH. There,
only the paths that exist when the child starts are writable. The child can
also change the other subagent/ branches. Without bwrap, and on Windows, there is no sandbox: the boundary for
bash is only the child's working directory, which is the worktree, and its
instructions. Worktree isolation
needs a git repository: the one at the child's cwd, else the one the parent
works in. So { task, cwd: "~/dev/app", isolation: "worktree" } works when
the parent's directory is not a repository. The child starts in the same
directory of its worktree as its cwd (or, for a subagent's own worktree, its
parent's) is in the repository. A read-only
child's subagents can't make a worktree from a cwd, because making one runs
that repository's git hooks. A worktree child's subagents can name only a cwd
inside that child's worktree. It copies that repository's current files, including uncommitted
changes and untracked files that Git does not ignore, into a separate checkout.
The parent keeps its files and index unchanged. The report gives the worktree,
branch, change counts, and commands to apply changes and remove it after merging.
The apply command runs git only in the parent's checkout, on a snapshot of the
worktree's files that main wrote when the child ended.
A child can end before its subagents. Then their reports give their own apply
commands, so their later changes are not lost.
Untracked node_modules are linked, not copied or included in the changes.
Worktrees stay until you remove them. Resumed children keep the same workspace.
After you apply the changes, the cleanup command force-removes the worktree and
deletes its branch. Check that you kept all needed files first.
Read-only shell. readOnlyShell: true is readOnly: true plus bash,
for work that must run commands but change nothing: git log, git show
and git diff, rg, and builds or tests that write only to /tmp. It works on Linux only. Each
command runs under bwrap with:
- every file read-only (a write fails with "Read-only file system"),
- an empty, private
/tmpfor each command, and a new/devand/proc, - no network, no view of other processes, and no shared System V or POSIX IPC of the host,
- a seccomp filter that refuses to make Unix sockets or an io_uring.
The filter is necessary because a read-only mount does not stop a connection
to a Unix socket, and sockets such as the herdr socket, the Pi broker, the
user D-Bus or a container engine can make other programs write. socketpair
and pipes still work. The filter is known for x86_64 and arm64 only. A FIFO
or a device that the command can see can still be opened for write. A working folder under /tmp stays visible,
read-only. GIT_OPTIONAL_LOCKS=0 stops git status and git diff from
trying to refresh the index; other git commands may warn that they can't
write a lock file. macOS has no read-only shell yet: sandbox-exec with a
default-allow profile leaves Mach and XPC services, such as launchctl or
osascript, that can write for the command. The spawn is refused where no
sandbox runs, and if the
sandbox stops working later, the child's bash refuses every command
instead of running unconfined. A read-only child without a shell can't start
one with readOnlyShell. Such a child can't set a check, as a read-only
child can't.
Hidden sessions. A child can't read the sessions and reports of other
agents, or main's session. Its read, grep, find and ls calls refuse
~/.pi/agent/sessions/subagents and main's session file, and the error says
that the path is hidden from subagents. grep and find also refuse a folder
that holds one of these paths, so search a narrower folder. ls of a folder
above only lists names. A child can still read its own session and reports,
the files of the agents under it, and the reports of the agents it started
after (not with afterReports: false, and not when its own parent can't read
those reports either). A call is checked again on its result,
so a symlink made in the same reply can't get past the check. edit and
write refuse the same paths. A child's
bash runs in a sandbox that puts an empty tmpfs over the folder and
/dev/null over main's session (bwrap on Linux, sandbox-exec on macOS). A
worktree child's sandbox does this too. Where no sandbox tool runs, Pi warns
once and bash can read the folder. In the sandbox, sudo fails, because bwrap
turns on "no new privileges". The hiding keeps an honest child from reading
the work of the agents beside it by accident. It is not a boundary against a
child that tries to read them: a writer child's bash keeps the host's
processes, network and sockets, so it can, for example, read through
/proc/<pid>/root or start a command through the user D-Bus. A
readOnlyShell child has none of these. Set hideSessions: false in the
subagents section of pi-extras.json to turn all of this off.
Limits. At most maxConcurrent children run at the same time; the rest
queue. The limit also applies to a child that goes on in its open session.
Examples are a resumed idle child, a woken parent, and a child that got its
answer. Such a child waits in line ahead of spawns that have not started. A subagent that its
parent starts with wait: true runs in that parent's slot. A child's bash
call that starts with sleep of 15 seconds or more (for example
sleep 240; gh run view) lends its slot to the next agent in line while it
sleeps. After the sleep, it waits for a free slot, then runs the rest of
the command. A queued row shows its place in line, such as 2nd in line for a slot. A child can't start children of its own by default.
Each run of a child has a time budget, maxRunMinutes (default 60), and an
optional cost budget, maxRunCost (no limit by default). The subagent call
can set its own with maxMinutes and maxCost, and the child keeps them
when it resumes. Time counts only while the run works, not while it waits
for an answer, for its subagents, or for a free slot. A run that wakes from
waiting on its subagents keeps what it used. Cost counts only what this run
spent. Each agent has its own budget, which its parent's budget does not
limit. A child can give a subagent a larger budget than its own.
The child's instructions name its budgets. A child can't see a clock, and
without one it guesses its time used far too high and stops early. So each
run with a time budget starts with a [Budget clock] line: the start time
(UTC), the budget in work minutes, the end time if the run does not wait, and
the clock tool to read the time. Every child has clock, also a read-only
child with no shell and a child with a tools list. It gives the time now
(UTC, to the second) and, with a time budget, the work minutes used and left
and the end time. At each 10% of the time budget up to 80%, the child gets a
note with the time now (Now 18:40 UTC.), the work minutes used and left and
the end time. The cost note starts with the time now too.
Between notes, a child guesses from its own pace and drifts high, so the
notes are close together. The 80% note also tells it to plan its report. After a wait of 1 minute or more, a note
gives the new end time. At 80% of a cost budget, the child gets one note that
tells it how much is left. A time budget under 5 minutes gets the first line
but no notes. A run that goes on (on a fallback model, in a new session, or
after a wait) starts with a line of the minutes used and left, and it does not
get a note again. The times are in the prompts, not in the system prompt, so
the system prompt stays the same and prompt caching still works.
At the limit, the run must wrap up. Each new tool call is blocked, and the
widget shows wrapping up: over its 60-minute budget. The child's own
subagents stop without a report. A tool call that already runs gets 60 seconds
to finish; after that, the run is aborted. At the next turn boundary, the child
is asked in the same session for its final report: what is done, what is not
done or not verified, what it leaves behind, and the next steps. The prompt
names the subagents that were stopped. If its reply at the limit called no
tool and no subagent was stopped, that reply is the report. The run then ends
as a normal run does, and the child keeps its context. Its parent gets a report
whose header says, for example, reached its 60-minute budget after 1h00m and wrote this report when told to stop. A report to main wakes it. The check
does not run, and the run gets no fallback model and no move to a new session.
Mail that comes during the wrap-up waits for the next run. The run can go over
maxCost by a few replies at most: the reply that streams at the limit, the
wrap-up reply, and each blocked reply. A run whose last reply crosses
maxCost as it ends also counts as one that reached its budget, so its check
does not run.
If 2 replies after the wrap-up prompt still call tools (both are blocked), if
the model fails, or if the run has not ended 4 minutes after the limit, the
run is stopped with its subagents, like stop_subagent. Its parent then gets
a report that says why, for example stopped after 1h04m: over its 60-minute budget,
with its last message. So a hung child can't keep a pi -p run open forever.
A message to the child resumes it with a fresh budget, and it is told how its
last run ended.
Window limit. maxWindowPct on subagent wraps a run up when the usage
window that governs its model reaches that percent, as a time or cost budget
does: tool calls are blocked and the child writes its report. The reading is
the fullest window of the model's provider that applies to the model (for
example anthropic 7d, or a model-family window such as 7d-fable for
Fable). It comes from Status Plus, so it works for providers that it polls:
anthropic, opencode-go and openai-codex, and the other members of their
provider families (see Status Plus). A claude-code model
reads the anthropic window. A run is checked at each usage update
and each new reading, but only while it works. The spawn is refused when the
window is already at the limit. For a provider with no reading, for example
openai signed in with ChatGPT and no openai-codex login, the spawn result says that the limit can't
act, and the time and cost budgets still apply. The child's instructions name
the limit, and the report says, for example, reached its 80% limit on the anthropic 7d window.
Retry and fallback. A child stays on the model its parent chose. When
the provider is overloaded or unavailable, Pi retries that model 8 times, 5 s
apart at first and doubling up to 2 min: about 8.6 minutes in all. Longer
retry settings of your own stay, and retry.enabled: false stays off. The
time the retries take counts against the run's time budget.
Sometimes a child's model still can't serve its run: its quota or usage
limit is used up, a rate limit lasts longer than the automatic wait, the
provider stays overloaded or unavailable, the Claude Code proxy (Meridian)
ends the turn early after its retries, the credentials are missing or
expired, or the model is gone. Then the run fails, and its parent gets the
error. To let such a run go on, name models in fallbackModels. The run
then continues from the child's session on the next of them. The child is
told first that its model failed and why, and to check the files before it
continues. The fallback skips the model that failed and models outside the
scoped models. After a quota, usage-limit or credentials failure, it also
skips the other models of that provider. Each model gets at most one try per
run. The row shows the new model, and the report says what happened, for
example Ran on anthropic/claude-opus-5-5 after openai-codex/gpt-6.1-sol failed: usage limit reached. A fallback model can cost more than the model
main chose. The run keeps its budget: the time and cost it used before the
switch still count. When no model is left, the run fails and its error says
so. Other failures, such as a full context or a bad request, fail as before.
Memory. A child's session is released when it ends. It is released
immediately when it failed or was stopped. When it finished, it is released
after two quiet minutes, since a follow-up often comes soon. A message to a
released child relaunches it from its session file with its context, and its
inspector reads that file, so a long session with many children keeps only the
working ones in memory. A
message within the two minutes uses the open session only when a launch now
would make the same session. After a /reload, or after a change to a file
that the launch read, the child relaunches from its session file instead. A
finished child whose session is not current releases it immediately.
Moving to a new session. A working child whose session is not current
moves to a new session at its next turn boundary. That is the point after a
turn, when the results of its tool calls are recorded and before its model
gets the next request. The cause is a /reload or a change to a file that the
launch read. These files are settings.json, the project's
.pi/settings.json, models.json, pi-extras.json, the context files,
SYSTEM.md, APPEND_SYSTEM.md and the skills. The new session opens from the
session file with the current code and settings, and the run continues as the
same run. It keeps its run number, its time and cost budget, its check and fix
rounds, its edit lock and the answers it is owed. Messages that it had not
read go to the new session with a short note. The note tells the child that
its session moved and that it continues where it stopped. A child does not
move while a question with expectReply or a subagent call with wait: true
blocks it. It moves at the end of the turn after that call returns. A child
also does not move while its run stops, or when its model is not in scope now.
Each cause moves a run one time at most. Changes to files move a run only one
time, and later changes wait for its next run. This is because Pi writes
settings.json each time you change main's model or thinking level, and a
child can edit a context file many times. New code after a /reload still
moves the run. Its row shows moving to a new session and the cause during
the move. Each move adds a line to ~/.pi/agent/subagents/swaps.log. Pi adds
a changed system prompt after the earlier messages, so the prompt cache keeps
the prefix. A move after a code change can cost one cache miss when the new
code changes the child's tools. swapAtTurn: false in the subagents section
turns moves off.
Headless runs. pi -p and --mode json end when main settles, so main
waits there for its background children. Their reports, questions and notes
go into main's context as it would otherwise settle, with one more turn to
read them; a child's question to main gets main's answer and the child goes
on. Main settles when no child is working and nothing waits for it.
Reload. /reload does not stop children. A child that works moves to the
new code at its next turn boundary, as the same run (see Moving to a new
session). A child that starts after the reload uses the new code. A child
that reaches a turn boundary during the reload waits for the new code. Mail to
main waits during the reload and then arrives one time. When no child works
and all mail is in main's transcript, a team from the new code takes over from
index.json. Until then, team settings from pi-extras.json, such as
maxConcurrent, run budgets, batchMs and swapAtTurn, stay as they were.
Children get new code of an extension at the latest after /reload. They also
get a new context file or skill at the latest after /reload.
Some reloads must interrupt children:
- a new version of pi-extras that can't take over from the old one
- a takeover that fails, or a reload that can't keep the children
- a reload that does not finish in 30 seconds
Then Subagents interrupts the children and records them in index.json. The
next session start resumes them, as resumePolicy allows. Mail to main that
waited during the reload stays in index.json, and the next session start
sends it one time, after its restore notice. A question from a child that
stays paused does not wake main. Under resumePolicy: "notify", main's answer
to it goes to the child's inbox and does not resume the child. A reload that
does not finish, or that can't keep the children, writes a line to
~/.pi/agent/subagents/reload.log. The warnings of that interruption go to the
same file, because no session can show them. For the other cases, Pi shows a
notice.
Restoration. Children survive /reload, restarts and crashes. An
index.json beside their sessions keeps each child's name, settings, state and
run count. Pi writes it in the background as children work, so a slow disk does
not stop Pi, and at once on a signal, a quit or a hand-over to another version.
After a hard kill (SIGKILL, out of memory, power loss), the index can be one or
two writes behind: a child that stopped a moment before can restore as
interrupted. With the index, message reaches the children again after a restart. The index also keeps mail
that has not reached a child yet: notes in its inbox, the answers it is owed,
and messages steered into its run that it had not read. After a restart, its
next run gets that mail first, and an answer it is owed still resumes it.
A run that fails or is stopped keeps its unread messages for the next run
too. Children that a
/reload interrupts resume on their own, one time. After a restart or crash they
come back paused, and main gets one notice listing them with their task and
last activity. /subagents resume <name> continues a paused child; message
finished ones instead. A resumed child is told to check files first, because
its last tool call may not have finished.
Some children never resume on their own: ones paused earlier, grandchildren,
sessions from before 0.15.0, orphans (child sessions missing from the index,
listed in /subagents and restored read-only), children whose workspace moved,
and children whose model is no longer allowed (resuming one of those by hand
uses the current default model and says so). An automatic resume that is
itself interrupted stays paused, and a failed resume can be retried. Only one
Pi process at a time can own a session's children; forks start without them.
A second Pi on the same session. When another Pi process owns the
children, this Pi still gives main the subagent, message and
stop_subagent tools. Each call fails and says which process owns the
children and what that process does. A line above the editor says the same
until the children open here. A parent that is suspended with Ctrl-Z still
owns its children. To free them, do one of these:
- Run
fgin the terminal of that Pi, then quit it there. - Run
/subagents takeoverhere. Takeover asks first. Then it sends that Pi SIGTERM, and SIGCONT so that a suspended Pi can act on it. It waits up to 10 s for that Pi to let the children go, then opens them here and shows them. Interrupted children then follow the resume policy.
Takeover works only for a process of the same user on the same host, and it
never sends a stronger signal than SIGTERM. A suspended Pi can stop again
when it restores its terminal after it lets the children go. Takeover then
says so: run fg in its terminal, and it quits.
Each run adds a line to ~/.pi/agent/subagents/runs.jsonl (model,
task opening, time, tool calls, cost, how it ended); /subagents stats sums it
by model, which is what to tune the guide on. A run that wrote its report at
its budget has a budgetEnd field, and stats count it as at budget, not as
an ordinary finish. Each line holds that run's own
tool calls and cost ("format": 2). Older lines, with no format, hold the
child's totals so far, so stats over them count a child with many runs too
high. /subagents stop <name> or
stop all also stops paused interrupted children.
If another extension already has a subagent tool, such as pi-subagents,
Subagents stays off for the session and says so; remove one of the two. A
message tool from another extension is left alone and only that tool is
skipped.
Opt-in and macOS only: set PI_COMPUTER_USE=on before starting Pi. It adds a
computer_use tool that runs a short script against OpenAI's Computer Use
methods (sky.list_apps, sky.get_app_state, sky.click, sky.type_text and
the rest), so the agent can read an app's accessibility tree and screenshot and
act on it. It needs the ChatGPT app for macOS with Computer Use turned on, which
installs the signed Computer Use client under ~/.codex/computer-use, and
someone logged in to the Mac's desktop. /computer-use shows what is missing.
The first use of each app asks you to allow it. "Don't allow" is the default,
so a stray Enter refuses. "Allow for this session" lasts until the client exits;
"Always allow" is stored by the Computer Use service and also applies to ChatGPT
and Codex computer use. Apps the service rates high risk, such as browsers, show
its prompt injection warning. The service never allows some apps, such as
terminals. Without a UI, such as pi -p, an app that is not always allowed
is denied unless Allow all is on. The agent is then told how to allow it.
Subagents never get computer_use.
/computer-use opens a panel with the client's status, the apps mode and a
checklist of the apps the agent may always use. Check and uncheck any number of
apps, type to filter, and press Enter to save them together; always allowing an
app or turning on Allow all asks you to confirm first. Checking an app ahead of
time lets headless runs use it. Changes apply immediately, including to a
running client. The checklist edits the same approvals file as the ChatGPT
app's settings; if that file ever has a format this version does not know, it
is shown read-only.
The apps mode sits on top of the checklist, applies to every Pi session, and
is kept in ~/.pi/agent/pi-extras.json:
- Ask per app (default): checked apps are used without asking; any other app asks you first.
- Allow all: every app is allowed without asking, including high-risk apps such as browsers and mail, and also without a UI. These approvals last only for the client session and are never stored, so the checklist is unchanged when you switch back. The service still refuses some apps, such as terminals.
- Allow none: every computer use call is refused, even for checked apps.
Each tool call shows the script as highlighted code, then every Computer Use
call it made, with its app and target, how long it took, and any approval.
The client stays running between calls, so element indices stay valid across
turns, and exits after five idle minutes. It runs as a launchd job in your
desktop session, so it works the same from a local terminal and over SSH. Use
only one computer_use provider at a time; remove another computer-use
extension before opting in.
An element index is a position in the latest tree, not an element. When an
action changes an app's layout, an old index can name another control, and the
service clicks it with no error. So the tool keeps what each index named at the
script's last get_app_state of that app. Before an element action that follows
another action on the same app, it reads the full tree again. If the element at
that index is a different one, the call fails with a "stale" error and the action
is not sent. To be the same element, it must have the same level in the tree and
the same role, title, description and ID. Its first child elements must also
match, so a list row that now shows another item fails. For a scroll, only the
element itself counts, because a scroll changes the rows that it shows. Values
can change. A child label that shows state ("4 unread", "Play") causes a
refusal; a new get_app_state clears it. Calls of one script that run at the same time
(Promise.all) go one at a time, so no call can move the layout between the check
and the action.
The first element action after a get_app_state in the same script needs no
extra read. Each later element action costs one full read with a screenshot. A
new script checks its first element action too, because the window can change
between two tool calls. When a check read finds a change, the script's next
get_app_state of that app returns a full tree, not a diff. The check cannot tell apart two elements that look the
same in every way it compares. It does not see a change that the script did not
cause before its first element action, for example a list that reloads by itself
right after the read.
This relies on undocumented parts of the ChatGPT app and can break when it updates. OpenAI does not produce or endorse this integration.
This integration uses a Kagi subscription session token, not the paid Search API. Each user must have their own Kagi account and supply their own credential. No credential is included, requested in chat, or provisioned by installation.
- Obtain a session link from your Kagi browser settings.
- Save it, or just its token, in a private local file using your editor.
- Restrict that file to your user (
chmod 600 /path/to/your/token-file). - Set
KAGI_TOKEN_FILEto that path before starting Pi. The default is~/.secrets/kagi_token.
Never paste a session link or token into a prompt, issue, shell command argument, repository, or Pi settings. No paid Search API key is needed or accepted as a substitute. Authentication failures, challenges and rate limits stop requests; there is no browser bypass or account-login automation. This HTML integration is unofficial and can break when Kagi changes its pages. Use it in accordance with Kagi's terms and your subscription.
Search results are bounded and treated as untrusted source text. A five-minute
in-memory cache reduces repeated queries. Up to four searches run at the same
time. Request starts are at least 150 ms apart, with at most 30 page requests a
minute. So a batch of searches is fast, but a looping agent cannot hammer the
account. A search
that would have to wait past its deadline for that pace fails with a pacing
error instead. The client uses a cooperative I/O deadline;
synchronous parsing cannot be interrupted by that timer. There is a 2 MB
response cap, but pathological HTML can still occupy the event loop.
Without a configured token, the rest of the package loads normally; invoking
kagi_search reports a credential error.
gov_search searches the free
Firecrawl Government Index: more
than 30 million US federal, state and local government pages. The index has
statutes, regulations, city and county codes, court opinions and agency
guidance. The tool sends one request to
https://api.firecrawl.dev/v2/search/gov for each call. It returns ranked
titles, URLs and short descriptions, not an answer.
The tool is on by default. The model can then send queries to Firecrawl, a
third party, from your IP address. To turn the tool off, use one of these
ways, then restart Pi or use /reload:
- Put
"govSearch": { "enabled": false }inpi-extras.json. The extension then registers no tool. Onlyfalseturns it off. - Run
pi configand clearextensions/gov-search.ts. Pi then does not load the extension.
Parameters:
query(required): a question or search phrase. Put the jurisdiction, the agency or citation, and a date in it when you know them. Firecrawl gets the query text, so the tool tells the model to keep personal facts (names, addresses, case numbers) out of it.k: the number of results, from 1 to 100. The default is 10.jurisdiction:usfor federal, orus-and a two-letter state or territory code, for exampleus-caorus-dc.court: a CourtListener court id, for examplescotus,ca9ornysd.courtType:federal-supreme,federal-appellate,federal-district,federal-bankruptcy,federal-special,military,state-supreme,state-appellate,state-trial,state-specialorstate-attorney-general.cites: one cited law or case, for example42 U.S.C. § 1983,5 U.S. 137orus:usc:42:1983.domain: one site, for exampleirs.gov.
All filters must match. A filter keeps only the pages that the index labeled. Thus a filtered search can miss pages, and zero results does not prove that no law exists. The index can also return old versions, bills that did not pass and third-party copies. The tool description tells the model to check the issuer, jurisdiction, date and in-force status, and to read the source page before it states current law.
The output is a numbered list. Each description is cut to 300 characters, and
the whole text stays under 16 KB. When the results do not fit, the
descriptions get shorter first, and then the last results are removed. The
count line gives the number of results, the duplicates removed and the results
removed for size. A duplicate is the same URL again, or the same title and
description on a host of the same site (for example ftc.gov and
search.ftc.gov). The text is marked as untrusted, as with kagi_search.
The search is free and uses no Firecrawl credits. No key is needed. Without a key, Firecrawl limits the requests from each IP address for each day. Over the limit, the tool says that the daily limit is reached (HTTP 429). For higher limits, get a free key from firecrawl.dev. Firecrawl refuses requests without a key from VPN and proxy IP addresses (HTTP 403), so a proxy (see below) needs a key too. Give the key to the tool in one of these ways:
- Put it in the environment variable
FIRECRAWL_API_KEY. - Put it in the file
~/.secrets/firecrawl_api_key(one line). SetFIRECRAWL_API_KEY_FILEto use a different path. Restrict the file to your user (chmod 600). A tool such as Dotfleet can deploy the file for each machine.
The environment variable wins when both are set. No variable and no default
file means a search without a key. A key file that exists but cannot be read
or does not hold a valid key stops the search with an error. A missing file at
the path in FIRECRAWL_API_KEY_FILE also stops it. The tool reads the key only
when a search runs, so a new key needs no /reload. It sends the key only in
the Authorization header to api.firecrawl.dev, and never shows it in output
or errors.
Firecrawl refuses requests without a key from IP addresses that look like a VPN
or proxy (HTTP 403). The tool then says so. Thus, when the proxy below sends
the requests through a VPN, also set a key. After a 429,
the tool sends no request for the time in Retry-After (60 seconds when there
is none; one hour at most).
Each request has a limit of 20 seconds. An abort stops it at once. Up to 4 searches run at the same time.
To send the requests through a VPN, set an HTTP CONNECT proxy, for example wireproxy in HTTP mode:
PI_GOV_SEARCH_PROXY=http://127.0.0.1:8080in the environment of Pi, or"govSearch": { "proxy": "http://127.0.0.1:8080" }inpi-extras.json.
The environment variable wins over the file. An empty variable does not hide
the file. Both work when a tool such as Dotfleet sets them for each machine.
The tool reads the setting at each search, so a change needs no /reload.
The tool does not use HTTP_PROXY, HTTPS_PROXY or NO_PROXY.
The tool sends CONNECT api.firecrawl.dev:443 to the proxy. The proxy looks up
the address, so this machine makes no DNS request for the API. TLS runs from
Pi to the API inside the tunnel, so the proxy sees only the host name.
The proxy URL must:
- use
http://and have no user name, password, path or query; - name
localhostor an IP address in one of these ranges: loopback (127.0.0.0/8,::1), private (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7), or Tailscale (100.64.0.0/10,fd7a:115c:a1e0::/48).
The tool fails closed. When a proxy is set, it never sends a request directly. These conditions stop the search with an error, and no request goes out:
- the proxy URL is not correct or is not in an allowed range;
pi-extras.jsondoes not parse, so the tool cannot know if a proxy is set;- the proxy does not answer, or refuses the tunnel.
npm ci --ignore-scripts
npm run typecheck
npm test
npm run test:install
npm run audit:packageThe image above is rendered from a real Pi session, not drawn: npm run preview:render stages one against a scripted local endpoint (no model calls)
with a spoken dictation, then lays the captured terminal onto the card. It needs
macOS, tmux, ffmpeg and a provisioned voice model. npm run preview:check -- --open shows it as a 4:3 crop and at README width for review. Only the WebP is
committed; the PNG for GitHub's social preview is written next to it,
ignored by git, and uploaded now and then rather than with each release (see
CONTRIBUTING.md).
Tests use synthetic credentials and isolated homes, not live accounts. The test
runner bounds each suite to two minutes, or ten minutes when CI is set or
with PI_TEST_SERIAL=1. The installation smoke test uses Pi's
real package manager and an isolated home; it never starts a model request.
No build step or install lifecycle hook is needed. Pi loads TypeScript directly.
Runtime dependencies are installed by Pi; no global extension npm install is needed.
The default branch is release-ready: merged commits are updates for unpinned users. See contributing and third-party notices.
