Commit graph

7313 commits

Author SHA1 Message Date
Armin Ronacher
b4c2e92acb fix: remove vulnerable dependency chains
Replace build-time shell utilities with Node filesystem operations and retire the native sandbox example in favor of existing isolation options. Upgrade glob matching and worker-test dependencies and refresh the lockfiles.
2026-10-31 17:54:29 +01:00
Petr Houška
6de14ec098
docs(coding-agent): note tmux 3.6 color reply leak in tmux guide (#11645)
Adds a troubleshooting section to the tmux guide
(`packages/coding-agent/docs/tmux.md`, published at
https://pi.dev/docs/latest/tmux) for the color-code text that tmux 3.6
types into the editor at startup.

tmux 3.6 forwards Pi's palette queries to the outer terminal. When a
reply reaches tmux split across reads, tmux treats its bytes as key
presses and sends them to Pi as typed text. tmux 3.7 fixes this
([tmux#4749](https://github.com/tmux/tmux/issues/4749),
[tmux#4793](https://github.com/tmux/tmux/issues/4793)). Pi cannot filter
the text because it arrives exactly like typing.

The section also notes that switching to a fixed theme does not help:
`applyFromSettings()` queries terminal colors regardless of the theme,
which reporters confirmed in #10679 (`--use-theme dark`) and #10256
(`"theme": "dark"`).

Related: #10250, #10679, #10256.
2026-10-31 17:29:36 +01:00
Mario Zechner
f0646a00fb docs(chord): slice 10 fix round small points 2026-10-31 15:57:13 +00:00
Petr Houška
25426f72ae
docs(coding-agent): document Windows terminal and conhost limitations (#11644)
Adds a "Choose a terminal" section to
`packages/coding-agent/docs/windows.md`
(https://pi.dev/docs/latest/windows), with two tables (last updated
October 2026):

- Terminals and which conhost they use: Windows Terminal (recommended),
VS Code, WezTerm nightly (bundled, up to date); WezTerm 20240203
(bundled, old); Alacritty (built-in unless `conpty.dll` +
`OpenConsole.exe` are provided); classic console window, mintty,
ConEmu/Cmder, Windows OpenSSH (built-in).
- Known problems by conhost type:
- Built-in: crash when switching fullscreen -> regular or using `Ctrl+G`
from fullscreen (#9186, microsoft/terminal#17515).
- Built-in behind another terminal (mintty, OpenSSH): query replies leak
into the prompt, BEL opens the external editor (#10256, #10362, #10742).
- Old bundled (WezTerm 20240203): reply fragments in the prompt (#10717,
#10970).
- Built-in, classic console: reported regular-mode redraw issue (#6300).
- Notes that the fixes exist upstream and reach the built-in conhost
with a future Windows update, timing unknown.

Sources: VS Code defaults `terminal.integrated.windowsUseConptyDll` to
`true` (bundled conpty v1.25). Alacritty loads `conpty.dll` only if
present (`alacritty_terminal/src/tty/windows/conpty.rs`) and its
installer ships only `alacritty.exe`.

The crash was verified locally in the classic console window on conhost
10.0.26100.9807: fullscreen -> regular crashed 3/3 runs and `Ctrl+G`
from fullscreen crashed 2/2 runs (`0xC0000005`).
2026-10-31 16:15:42 +01:00
Armin Ronacher
705d047cb5 fix(coding-agent): tighten system prompt 2026-10-31 16:11:43 +01:00
Mario Zechner
ed2bdde64b docs(chord): HTTP core as a fetch handler, Node listener as the easy path 2026-10-31 15:04:43 +00:00
Mario Zechner
56cae78234 docs(chord): plugins import each other by name 2026-10-31 14:46:28 +00:00
Mario Zechner
112a8b46f3 docs(chord): tutorial: base code vs plugins, shipped plugins, subpaths 2026-10-31 14:43:24 +00:00
Mario Zechner
bb36171aac docs(chord): tutorial: only the server builds 2026-10-31 14:28:28 +00:00
Mario Zechner
86338abbd8 docs(chord): tutorial: loading explained (build vs evaluate, host packages, why plugins.load) 2026-10-31 14:26:12 +00:00
Mario Zechner
fe6211607b docs(chord): tutorial: persisted documents and tables 2026-10-31 14:09:23 +00:00
Mario Zechner
dae7bd8fcb docs(chord): tutorial: an object's parts before they are used 2026-10-31 14:07:01 +00:00
Mario Zechner
436d99e04b docs(chord): tutorial: one schema for contract and document 2026-10-31 14:01:40 +00:00
Mario Zechner
a34444d6bd docs(chord): tutorial: overview figure, client hosts 2026-10-31 13:56:14 +00:00
Mario Zechner
081f28bad7 docs(chord): tutorial on hosts and plugins 2026-10-31 13:50:20 +00:00
Mario Zechner
e22468919b docs(chord): HTTP server as a native object; the opinionated app kit as the next design pass 2026-10-31 13:47:28 +00:00
Mario Zechner
b79f21095d docs(chord): slice 10 explainer (HTTP and workers) 2026-10-31 13:30:29 +00:00
Mario Zechner
07309b63eb docs(chord): slice 10 security decisions: origins only from relays, public worker errors, hosts by contribution, the HTTP baseline 2026-10-31 13:28:07 +00:00
Mario Zechner
44d9fb5f6e docs(chord): slice 10 spec wording from the build 2026-10-31 09:18:58 +00:00
Mario Zechner
9b7a44c48d docs(chord): slice 11 draft, P1-P2, one definition per facet kind 2026-10-31 08:22:49 +00:00
Mario Zechner
e4adba965e docs(chord): remove the stale v2 explainer (from before slice 1) 2026-10-31 08:02:27 +00:00
Mario Zechner
bc9a20f847 docs(chord): slice 10 approved 2026-10-31 07:56:35 +00:00
Mario Zechner
61f73f2a80 docs(chord): slice 10 spec review round 2 folded (Q1-Q6), ready to build 2026-10-31 07:56:28 +00:00
Mario Zechner
35dfa75737 fix(durable): idle checks and wake() without per-commit scans
After every commit that changed a task, the scheduler re-checked every
conversation with a kept context range (every conversation read in the last
contextRetentionMs) by walking its subtree, scanned them again for the next
expiry, and wake() scanned every live task. CPU per run grew linearly with the
number of conversations.

Now a commit re-checks only the conversations above tasks that became live or
ended (collected before the sweep; a chain load that resolves an unknown for a
live unloaded task falls back to a full check), idle context ranges expire
from an index ordered by idleSince (re-sorted when the clock goes back; zero
retention drops all idle ranges; reconcile passes still expire for hosts that
decline timers), and wake() takes the earliest sleep from the sleeping tasks
and scans live tasks only while nothing runs.

One Session, one-tool runs, no storage latency: 2048 conversations 100 -> 805
runs/s, 4096: 46 -> 772; local SQLite at 2048: 88 -> 459; CPU per run flat at
about 1.6 ms.
2026-10-30 23:17:56 +00:00
Mario Zechner
918c6ffc22 docs(chord): slice 10 spec review folded (R1-R6), no automatic worker restarts, app kit malleable by plugins 2026-10-30 22:57:59 +00:00
xu0o0
79006f0619
fix(durable,coding-agent): support Unicode word segmentation in tool search (#11037)
This PR uses unicode-aware segmentation and normalization to handle tool
search for multilingual text.

Fixes #10816

---------

Co-authored-by: Petr Houška <houskape@gmail.com>
2026-10-30 23:34:00 +01:00
Petr Houška
c7372af420
fix(durable): rank tool search like the coding agent (#11631)
Ports the coding agent's tool ranking fixes for #11331 (#11440, #11625)
to durable, which keeps its own copy of the ranker in `src/search.ts`.

Problem: durable's `rankDocuments()` was still the coding agent's ranker
from before both fixes. `thread_read` with `limit: 1` returned
`queue_read`, and a tool name among other words (`peekaboo app`) lost to
tools that only mention the words in their descriptions.

Change:
- `src/search.ts`: same scoring as the coding agent's `Bm25Ranker`, plus
coverage first: a tool matching more of the query's terms (in any field)
ranks above one matching fewer, whatever the scores. Without it, name
weighting regressed `screenshot window` (1 to 2, `window` beat `image`)
and `describe app` (1 to 2, `app` beat `see`). The name is a separate
field (3x IDF per term, above any number of description mentions), a
tool whose whole local name is in the query gets a bonus worth one more
match of every query term, a query that is exactly a tool's name ranks
it first, and namespace text is a separate 0.5x field that is not
length-normalized.
- `tool_search`: the search text no longer repeats the name; the ranker
reads it from `name`.
- `searchTools()`: same, and the namespace text now includes the
namespace instructions, as in the coding agent.
- `test/search.test.ts`: the coding agent's ranker cases.

Remaining difference from the coding agent: durable's `tool_search` has
no namespaces (they are a code mode option), and `searchTools()` ranks
on the tool sample from the start data (description plus TypeScript
declaration) rather than description plus schema text.

`searchTools()` results are journaled, so replaying a script started
before this change returns the old results; no `CODEMODE_MODULE_VERSION`
bump.


## Before/after

`searchTools()` ranking on a made-up set of 16 MCP tools from 5 servers.
Each tool is searched with its description; each server is a code mode
namespace (prefix `mcp__<server>__`) with the description and
instructions below. Rank of the wanted tool among all matches, with the
top result when it is another tool.

| Server | Description | Instructions | Tools: description |
|---|---|---|---|
| `github` | GitHub repositories, issues, and pull requests | Use
owner/repo for repositories. Issues and pull requests share numbers. |
`list_issues`: List issues in a repository, filtered by state, labels,
or assignee.<br>`get_issue`: Get an issue with its comments by
number.<br>`create_issue`: Open a new issue in a
repository.<br>`search_code`: Search code across
repositories.<br>`create_pull_request`: Open a pull request from a
branch. |
| `linear` | Linear issue tracker | Search issues before creating new
ones. Issues belong to teams. | `search_issues`: Search Linear issues by
text.<br>`create_issue`: Create a Linear issue in a team. |
| `peekaboo` | macOS automation | Peekaboo controls macOS apps. Use app
names or bundle IDs. Peekaboo can capture any app window. | `app`:
Launch, quit, relaunch, focus, hide, or switch applications. Targets an
application by name, bundle identifier, or process ID, and can wait
until it is ready.<br>`see`: Capture the app window and describe the UI
elements of the app.<br>`window`: Move, resize, or close a window of an
app.<br>`menu`: Click a menu item in an app.<br>`image`: Take a
screenshot of the screen, an app, or a window. |
| `app` | Thread and queue state of the app | Threads hold messages; the
queue holds pending messages of a thread. | `queue_read`: Read a queued
message of this thread.<br>`thread_read`: Read durable state and a
paginated timeline from any thread. The default view returns user
messages, assistant messages and plans. Continue with afterPosition
until hasMore is false. |
| `docs` | Project documentation | Search the docs before answering
questions about the API. | `search`: Search the
documentation.<br>`read_page`: Read a documentation page by path. |

Before, a tool's search text was its name, the name with `_` as spaces,
its description, and the namespace name and description (no
instructions). After, the name and the namespace (with instructions) are
separate fields.

| Query | Wanted | Before: rank | After: rank | Change |
|---|---|---|---|---|
| `thread_read` | `app__thread_read` | 2 (top: `app__queue_read`) | 1 |
$\color{green}{\textsf{better}}$ |
| `mcp__app__thread_read` | `app__thread_read` | 2 (top:
`app__queue_read`) | 1 | $\color{green}{\textsf{better}}$ |
| `peekaboo app` | `peekaboo__app` | 5 (top: `peekaboo__see`) | 1 |
$\color{green}{\textsf{better}}$ |
| `app` | `peekaboo__app` | 4 (top: `app__queue_read`) | 1 |
$\color{green}{\textsf{better}}$ |
| `read thread messages` | `app__thread_read` | 2 (top:
`app__queue_read`) | 1 | $\color{green}{\textsf{better}}$ |
| `open the peekaboo app tool` | `peekaboo__app` | 5 (top:
`peekaboo__see`) | 1 | $\color{green}{\textsf{better}}$ |
| `launch app` | `peekaboo__app` | 1 | 1 | $\color{gray}{\textsf{same}}$
|
| `create linear issue` | `linear__create_issue` | 1 | 1 |
$\color{gray}{\textsf{same}}$ |
| `search documentation` | `docs__search` | 1 | 1 |
$\color{gray}{\textsf{same}}$ |
| `read docs` | `docs__read_page` | 1 | 1 |
$\color{gray}{\textsf{same}}$ |
| `pull request` | `github__create_pull_request` | 1 | 1 |
$\color{gray}{\textsf{same}}$ |
| `get issue comments` | `github__get_issue` | 1 | 1 |
$\color{gray}{\textsf{same}}$ |
| `screenshot window` | `peekaboo__image` | 1 | 1 |
$\color{gray}{\textsf{same}}$ |
| `describe app` | `peekaboo__see` | 1 | 1 |
$\color{gray}{\textsf{same}}$ |
| `create issue` | `github__create_issue` | 2 (top:
`linear__create_issue`) | 2 (top: `linear__create_issue`) |
$\color{gray}{\textsf{same}}$ |
| `search github issues` | `github__list_issues` | 5 (top:
`github__search_code`) | 5 (top: `github__search_code`) |
$\color{gray}{\textsf{same}}$ |

No regressions on this set. `search github issues` stays at 5:
`list_issues` does not contain "search", so it ties on coverage (2 of 3
terms) with `search_code` and `linear__search_issues`, and their scores
win.
2026-10-30 23:08:57 +01:00
Petr Houška
e033f255f0
fix(coding-agent): rank tools matching more query terms first in tool search (#11632)
Follow-up to #11625 (#11331). Same change in durable's copy: #11631.

Problem: #11625 made a query term in a tool's name outweigh any number
of description mentions. That regressed queries where another tool
matches more of the query in its description:
- `screenshot window`: `peekaboo__window` (name has `window`) beat
`peekaboo__image` ("Take a screenshot of the screen, an app, or a
window.").
- `describe app`: `peekaboo__app` (whole name `app`) beat
`peekaboo__see` ("... describe the UI elements of the app.").

Fix: `Bm25Ranker` sorts by coverage first: a tool matching more of the
query's distinct terms, in any field (name, description and schema,
namespace), ranks above one matching fewer, whatever the scores. Within
the same coverage, the score decides as before. A query that is exactly
a tool's name still ranks that tool first.

On the 16-tool set from #11631 (5 servers, 16 queries), this fixes both
regressions and changes nothing else; `search github issues` stays at
rank 5, since `list_issues` lacks "search" and ties on coverage with
`search_code` and `linear__search_issues`.

Changelog: updates the unreleased #11331 entry from #11625.
2026-10-30 23:07:43 +01:00
Petr Houška
9376c8d17c
fix(coding-agent): pass host keybinding hints to extension renderers (#11630)
Fixes #4748.

## Problem

Extensions that import `keyText`/`keyHint` from
`@earendil-works/pi-coding-agent` can resolve their own copy of the
package (their own `node_modules`, or the bundled `cli.js` vs
`dist/index.js`). That copy's keybindings are never set by the host, so
it falls back to a manager with only `tui.*` bindings, and
`keyHint("app.tools.expand", "to expand")` renders as ` to expand`.

## Change

The host passes its own `keyText` and `keyHint` to extension renderers,
the same way it passes `theme`:

- `ToolRenderContext` (`renderCall`/`renderResult` context):
`context.keyHint(...)`
- `MessageRenderOptions` and `EntryRenderOptions`:
`options.keyHint(...)`

All three extend a new exported `KeybindingHints` interface. The docs
and the `debug-provider` example now use the injected functions and warn
against importing them in renderers.

Not done:

- Moving the `pi-tui` keybindings singleton to `globalThis`. Per the
discussion in #7011, extension-facing state should be host-owned and
passed through the extension API instead of shared through process-wide
module state.
- `ctx.ui.keyText`/`keyHint`. No reported case needs them outside
renderers, RPC and print modes never install app keybindings, and adding
them to `ExtensionUIContext` would break SDK hosts that implement it.

## Tests

- `test/extension-keybinding-hints.test.ts`: tool, message, and entry
renderers format a user-overridden `app.tools.expand` binding.
- `npm run check` passes.
2026-10-30 23:01:20 +01:00
Petr Houška
8bc94b19ff
feat(coding-agent): add PI_SELF_UPDATE_COMMAND and selfUpdateCommand for pi update (#11629)
Refs #7756, #5607.

Problem: when a wrapper or another package manager provides Pi (mise, a
`PNPM_HOME` wrapper such as nub, Nix), `pi update` cannot update it and
prints "pi cannot self-update this installation." Users with several
machines have to remember which tool updates Pi on which machine.

Example:

```sh
export PI_SELF_UPDATE_COMMAND="mise use -g npm:{packageName}@{version}"
pi update
# Updating pi with mise use -g npm:@earendil-works/pi-coding-agent@1.4.0...
# Updated pi from 1.3.0 to 1.4.0
```

or in `~/.pi/agent/settings.json`:

```json
{ "selfUpdateCommand": ["nix", "profile", "upgrade", "pi"] }
```

Behavior:

- `pi update` still checks pi.dev first. The command runs only when a
newer release exists, or with `--force`, and the release note prints
first.
- Placeholders come from that release: `{packageName}`, `{version}`,
`{installSpec}` (`<packageName>@<version>`). A package rename announced
by pi.dev flows into them.
- `PI_SELF_UPDATE_COMMAND` takes precedence and is split on whitespace
(no quoting). The setting is argv, for arguments with spaces, and is
read only from global settings, so a project's `.pi/settings.json`
cannot inject a command.
- The configured command replaces Pi's own update logic for every
install method, including installer-managed installs: it is explicit
user intent.
- Exit code 0 does not prove an update. Pi reports "Updated pi from X to
Y" when its own `package.json` now shows Y (in-place updates) or when
`pi --version` from PATH reports Y (wrappers such as mise or Nix that
install each version into a new directory). Otherwise it warns: ``Ran
<cmd>, but `pi --version` reports X, not Y.``
- "cannot self-update this installation" now mentions the env var and
setting.

Docs: `docs/cli.md` (new "Custom update command" section),
`docs/settings.md`, `docs/environment-variables.md`, `--help` env list,
regenerated `schemas/settings.schema.json`.

Tests: unit tests for argv resolution and placeholders in
`test/config.test.ts`; CLI tests in `test/package-command-paths.test.ts`
for env precedence over the setting and the managed install, global-only
setting gated by the version check, and command failure. `npm run check`
passes.

Manual end-to-end check with real mise 2026.10.23 in a sandboxed HOME,
running this branch as the mise-installed 1.3.0 (`PI_PACKAGE_DIR`
pointing at mise's install):

| Scenario | Result |
|---|---|
| `mise use -g npm:{packageName}@{version}` | mise installs 1.4.0;
`Updated pi from 1.3.0 to 1.4.0` |
| Already 1.4.0 | `pi is already up to date (v1.4.0)`, mise not run |
| In-place `npm install -g --prefix <dir> {installSpec}` via the setting
| `Updated pi from 1.3.0 to 1.4.0` |
| Command `true` (exit 0, installs nothing) | ``Ran true, but `pi
--version` reports 1.3.0, not 1.4.0.`` |

This PR is AI-generated.
2026-10-30 22:30:39 +01:00
Mario Zechner
9e4868a605 docs(chord): slice 10 points D9-D12, RouteHandle.retry, design §23 folded 2026-10-30 21:12:13 +00:00
Mario Zechner
f8497996fe docs(chord): slice 10 spec draft, HTTP points D1-D8 2026-10-30 21:00:20 +00:00
Petr Houška
cc44fa2169
fix(coding-agent): weigh tool names above descriptions in tool search (#11625)
Follow-up to #11440 for the latest comment on #11331.

Problem: the ranker scored a tool as one bag of words. A query term in
the tool's name counted like a mention in its description, and the
server's instructions were copied into every tool's text, which
lengthened it and lowered its score. Models often add words to a tool
name, so `peekaboo app` ranked `mcp__peekaboo__app` 14th: other peekaboo
tools mention "app" in shorter descriptions, and its own description
says "applications".

Fix: `Bm25Ranker` scores three fields separately:
- name: a query term in the name counts 3x its IDF; the description part
of BM25 is at most 2.2x, so a name match beats any number of description
mentions;
- description and schema text: BM25 as before, length-normalized on this
field only;
- namespace name, description, and instructions: 0.5x IDF per matching
term, without length normalization.

A tool whose whole local name (last `__` segment) is in the query gets a
bonus worth one more match of every query term, so it mostly wins
(`peekaboo app`, `describe app`, `read thread_read state`). A query that
is exactly a tool's name still ranks it first (#11440). Applies to
`tool_search` and codemode `searchTools()`.

Not included: the comment's second point (loading a deferred tool on a
direct call instead of `Tool X not found`). That error comes from
`prepareToolCall` in `packages/agent`, which has no hook for unknown
tools; it needs an agent-loop API change.
2026-10-30 21:56:03 +01:00
Petr Houška
6454d07ec3
fix(coding-agent): start WSL interop helpers from / and report external editor failures (#11627)
Fixes #11592.

## Problem

WSL starts Windows programs in the current directory, translated to
`\\wsl.localhost\<distro>\<cwd>`. When that path exceeds MAX_PATH, every
Windows program fails to start with `Invalid argument`. pi inherits its
cwd when spawning `powershell.exe`, so in a deep directory:

- clipboard image paste returns `null` (the PowerShell backend fails,
stderr is discarded)
- copy via `powershell.exe` (WSL without WSLg) fails
- opening the browser for sign-in fails (`xdg-open` on WSL runs
`explorer.exe`)

Reproduced on WSL (FedoraLinux-44): `powershell.exe` and `cmd.exe` work
from a 225-char cwd and fail from 230. With an image on the Windows
clipboard, `readClipboardImage()` returns the PNG from `/tmp` and `null`
from a 245-char cwd; with this change it returns the PNG from both.
`LongPathsEnabled=1` does not help.

## Changes

- `runClipboardCommand` and `openBrowser` run from `/` on POSIX. These
tools never use the cwd (temp files are passed by absolute path), and
`/` is always the shortest UNC path. `tmpdir()` was not used because
`$TMPDIR` can itself be deep.
- The external editor keeps pi's cwd on purpose, so project-relative
paths and integrated terminals keep working. A Windows editor such as
`code` can still fail from a deep WSL directory, as with git's
`core.editor`. A comment documents this.
- External editor failures were silent: the UI redrew over the error
immediately. pi now shows `Failed to launch external editor: <cmd>`,
`External editor exited with code N; prompt unchanged`, or the signal
name. This shows in the status line for the main editor and inline in
the extension editor dialog.

## Testing

- New tests: clipboard commands run from `/` (POSIX); an editor that
cannot be launched is reported (POSIX); the existing failure test checks
the message.
- Verified on WSL by running the real modules: clipboard image paste
from a deep cwd, and editor success, exit code 1, launch failure, and
SIGTERM.
- On Windows, the 3 existing `external-editor.test.ts` tests already
fail on `main`: the editor command is split on spaces, so `C:\Program
Files\nodejs\node.exe` breaks. Not addressed here.

This PR is AI-generated.
2026-10-30 21:47:58 +01:00
Petr Houška
c2760d32cc
fix(coding-agent): name the package manager in pi update only when it owns the install (#11628)
Fixes #7756.

Problem: `detectInstallMethod()` classifies any path containing `/pnpm/`
as pnpm. An install that only lives under `PNPM_HOME`, for example a
wrapper under `$PNPM_HOME/global-nub/5/node_modules/...`, is detected as
pnpm. `pi update` then correctly refuses to self-update (the path is not
under `pnpm root -g`), but `getSelfUpdateUnavailableInstruction()`
printed:

```
This installation is not managed by a global pnpm install. Update it with the package manager, wrapper, or source checkout that provides it.
```

Fix: name the package manager only when it owns the install (managed but
not writable). All other cases fall through to the neutral message:

```
Update @earendil-works/pi-coding-agent using the package manager, wrapper, or source checkout that provides this installation.
```

`detectInstallMethod()` is unchanged: every user-facing path already
goes through the ownership check, and returning `unknown` would trip the
Windows "only supported for npm and pnpm" guard with a worse message.
The update banner already says `Run pi update` and no longer suggests
`pnpm install -g`.

The pi.dev installer is not affected: it verifies npm ownership with
`npm ls -g` and never infers pnpm from paths.

Tests: regression test in `test/config.test.ts` (fails without the fix).
`npm run check` passes.

This PR is AI-generated.
2026-10-30 21:23:10 +01:00
Mario Zechner
a14cdf90f0 feat(chord): v2 slice 7c, plugins component and per-plugin load and reload
Plugin discovery, esbuild builds (optional peer dependency), vm evaluation with
pluginFile and mapStack, dependency installs, a watcher with dirty marks and
rollout across hosts; host.load and host.reload per plugin with the closure rule;
examples 35-39 as folders with real plugin directories.
2026-10-30 18:18:23 +00:00
Mario Zechner
b7e8249520 docs(chord): 7c build: abort after commit names what it applied, per-plugin wording, examples 2026-10-30 18:17:46 +00:00
Christian Klotz
2a46c42080
docs(coding-agent): document which working directory resumed sessions use (#11617)
Closes #11532.

The docs and CLI help did not say which working directory a resumed
session runs in. That directory controls tools, `@path` references,
project configuration, and context files.

## Changes

- `docs/sessions.md`: states that a session keeps the working directory
it was created in, adds a table mapping each entry point to its working
directory, and describes what happens when that directory no longer
exists. Removes a paragraph that repeated the `--session`/`--fork`
descriptions.
- `docs/cli.md` and `--help`: clearer `--continue`, `--resume`,
`--session`, `--session-id`, and `--fork` descriptions. The help only
mentions the directory where it is not obvious (`--fork`).
- `/resume` and `/import` command descriptions, mirrored in
`docs/slash-commands.md`. `/import` notes that it preserves the session
ID and working directory.

| Entry point | Working directory |
|---|---|
| `--continue` | The current directory |
| `--resume`, `/resume`, `--session`, `/import` | The session's working
directory, which can differ from the current directory |
| `--fork`, or a `--session` ID from another directory that you choose
to fork | The current directory, in a new session |

Behavior was verified against `createSessionManager` in `src/main.ts`,
`SessionManager.open`/`continueRecent`/`forkFrom`, and
`AgentSessionRuntime.importFromJsonl`.
2026-10-30 17:22:05 +00:00
David Brailovsky
d280e1ffa2
feat(durable): beforeProviderRequest generation hook (#11619)
Adds a `beforeProviderRequest` generation hook so extensions can replace
the provider request body before it is sent. It is the durable
counterpart of coding-agent's `before_provider_request`.

Durable calls `streamSimple()`, so apps can't set provider-specific
fields such as Anthropic's `thinking.display: "updates"` (#10804)
without wrapping `Models`. Now it's an extension:

```ts
hook(GenerationTask, {
	beforeProviderRequest: ({ model, payload }) => {
		if (model.api !== "anthropic-messages") return undefined;
		// change payload
		return { payload };
	},
})
```
2026-10-30 18:15:05 +01:00
Mario Zechner
3bbb06aad7 docs(chord): 7c closure rule kept as is; review wording 2026-10-30 16:27:15 +00:00
Mario Zechner
26e05e64f0 docs(chord): reload's failed option for plugins kept before staging 2026-10-30 15:42:41 +00:00
Mario Zechner
318d9add68 docs(chord): one failed list in load and reload reports 2026-10-30 15:01:20 +00:00
Mario Zechner
18a155e2f5 docs(chord): slice 7c spec (plugins), per plugin at startup and reload 2026-10-30 14:56:23 +00:00
Mario Zechner
50af8c1601 refactor(chord): simplification audit of slices 6a-7b
The owner's audit decisions (decisions.md, "Simplification audit"): cut the reload-only
installing state (staging is synchronous) and the static registry cycle edges (the
runtime guard stays); retry propagation by a visited set and a hop budget (retry.hops)
instead of operation ids and through lists; a pure swap sends holds a fresh bound; one
rule after a live failure (every member followed until the admitted commits published);
parked_long for any long park, naming the first blocker in declaration order with its
state; one seal token per attempt. Keys follow the old instance during a restart's drain
like members. src/next: about 255 code lines fewer.
2026-10-30 13:30:54 +00:00
Armin Ronacher
be15fe3c17 fix(coding-agent): accept -l in pi mcp login and logout
Report unknown options as typed and suggest a configured server for a
mistyped name.
2026-10-30 14:15:25 +01:00
robopipi[bot]
0441483926
fix(durable): return image blocks from read in Code Mode scripts (#11606)
In pi-durable, a Code Mode script that called the built-in `read` on an
image got `""` back, so `image(await tools.read({ path: "pixel.png" }))`
threw a TypeError. `read` declared no `structuredOutputSchema`. Code
Mode resolves a schema-less tool with only its text, and drops images on
purpose. `read`'s output for an image is a single image block with no
text, so the script got an empty string. The coding agent fixed the same
problem for #10251.

With this change, `read` declares a `structuredOutputSchema`: a string,
or an image block `{ type: "image", data, mimeType }`. It returns the
matching `structuredOutput`:
- for a text file, its text, so scripts get the same value as before;
- for an image, its `ImageContent`, which `image()` accepts.

The "Read image file" note stays an info diagnostic, as before. The
README section on structured output and the changelog are updated.

**Testing**
- New test in `test/codemode.test.ts`. A script reads a real PNG and a
text file through `tools.read()`, checks the block's type, MIME type,
and data, and passes the block to `image()`. It checks that the script
succeeds, the image is saved and shown, and the text read still returns
its text. The test fails without the change: the script errors because
`image()` gets `""`.
- `test/tools-read-differential.test.ts`: the reference implementation
now includes the new `structuredOutput` field.
- `npm run check`.
- `test/codemode.test.ts`, `codemode-describe`, `tools`, `tools-images`,
`tools-read-differential`, `harness-tools`, `harness-hook-reads`,
`harness-read-only`, and `spec-usage` all pass.

Fixes #11015

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-30 12:23:48 +01:00
robopipi[bot]
50d9f823e4
fix(chord): remove whole ANSI escape sequences from tool output (#11607)
`sanitizeOutput()` in `@earendil-works/chord/output` deleted the ESC
byte (0x1b) along with the other control characters, but it left the
rest of each escape sequence in place. Colored tool output such as
`\x1b[31mERROR\x1b[0m` came back from `OutputBuffer.snapshot()` as
`[31mERROR[0m`. Because the ESC byte was already gone, later ANSI
stripping could no longer recognize the leftover fragments.

`sanitizeOutput()` now removes whole 7-bit escape sequences before it
removes single control characters. It handles:
- CSI sequences (colors, cursor and erase commands)
- string sequences (OSC, DCS, APC, PM, SOS) terminated by BEL or `ESC \`
- short `ESC x` forms such as `ESC ( B` or `ESC 7`

`snapshot()` sanitizes the concatenated retained text, so a sequence
split across pushed chunks is also removed.

**Testing**
- Added a regression test in `packages/chord/test/output.test.ts`. It
covers SGR colors, cursor and erase CSI, charset and short escapes, OSC
8 hyperlinks and an OSC title, plus an `OutputBuffer` fed a color
sequence split across three chunks. The test fails without the change
and passes with it.
- `test/output.test.ts` and `test/output-skip.test.ts` pass.
- Biome is clean. The only `tsc` errors in `npm run check` are in
`packages/ai` tests and are unrelated to this change.
- The durable package test results are the same with and without the
change: 71 test files fail on both, and every individual test that runs
passes.
- Reproduction from the issue: before the change both lines printed
`"[31mERROR[0m\n"`; after it, both print `"ERROR\n"`.

Fixes #11604

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-30 12:17:49 +01:00
robopipi[bot]
102d128713
fix(env): report watch overflow when a change list exceeds the frame limit (#11605)
After a rescan, the env daemon's file watcher sent every changed path in
a single `change` event. When a big batch of changes made that event
larger than the 16 MiB frame limit, `write_frame` rejected it. The
daemon's only output writer then shut down. The daemon process kept
running but sent no more replies or pings. Requests hung until the
client's 30 s silence limit dropped the connection and reconnected.

Now the watcher builds the event with `change_event`. If the serialized
path list would not fit in a frame (`MAX_PAYLOAD`), it sends the
existing `{ kind: "change", overflow: true }` event in its place. The
client already maps that to an `overflow` file change, so the wire
format does not change and the connection keeps working.

Testing:
- Added Rust unit tests in `watch.rs`. One checks that a small path list
is sent as `paths`. The other checks that a list larger than
`MAX_PAYLOAD` becomes `overflow`.
- `cargo fmt --check`, `cargo clippy --locked --all-targets -- -D
warnings` and `cargo test --locked` pass in `packages/env/daemon`.
- The `packages/env` vitest suite passes.
- Reproduced with the real daemon: I watched a directory and renamed in
a directory of 6,000 files with long paths, about 20 MB of path JSON.
Before the change, `fileInfo` requests timed out from the flush until
the client reconnected about 30 s later. After the change, the watcher
reports `overflow` right away and `fileInfo` keeps working. With 3,000
files, which fits in a frame, the paths are still reported as before.

Fixes #10516

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-30 12:15:09 +01:00
Mario Zechner
e5c6182bab feat(durable): group commit and batch Storage.commit
A Session no longer holds its line while a commit is stored. Commits that
arrive while one storage call is in flight go out together in the next call,
each with its own seq and outcome; a job runs until it touches a key an
unpublished commit wrote, then waits for that commit's publication. Outcomes
are delivered in line order; publication is in line order, one commit per
macrotask. The scheduler's decisions run as regions that record the keys they
read and rerun after a wait, with side effects only after a clean check.

Storage.commit(batches, receive, context) replaces commit(writes, adopt,
context): prefix semantics (Durable's own checks refuse a batch and keep the
ones before it; any other failure rejects the call), receipts per stored
batch, optional publishing() scope. Memory, SQLite, JSONL, IndexedDB and
StoreStorage implement it natively; StoreStorage through the new
ChordStore.commitSeries (one store transaction per call), replacing
commitInBatch.

A normal run makes no storage reads: bounded record cache, conversation
tails, task-document index, kept context ranges. A tool slot's running status
and a generation's attempt are stored with the next commit (12 instead of 14
commits per run).

One Harness with 128 conversations over storage with a 4 ms round trip: about
5 -> 440 agent runs per second. Design: packages/durable/docs/group-commit.md.
2026-10-30 10:41:11 +00:00
Armin Ronacher
908aa7b74b feat(coding-agent): prefer the client ID metadata document for MCP OAuth
oauth.clientRegistration defaults to auto: pi's Client ID Metadata Document
when the authorization server supports it for public clients, otherwise
dynamic client registration. Registered clients stay in use. pi mcp add
gains --oauth-client-registration.

closes #11426
2026-10-30 10:36:22 +01:00