Chord and Durable change a document one transaction at a time, so a tracker registered a WeakRef per
change for competitors that never existed on their hot path. The WeakRef targets survived scavenges
and V8 could pretenure every overlay context into old space (47.5 MB promoted against 2 MB in a
tracker loop). The tracker now keeps its open change in a plain field and moves it into the weak
registry only when a second change opens; stale semantics are unchanged. A draft dropped without
prepare or abort stays referenced until the tracker's next change or adopt (at most one per tracker).
`stored` and `rawNodes` are created on first use.
## Summary
The docs refresh (#9898) cut `docs/extensions.md` from about 3,100 lines
to 130, dropping API reference and tool guidance. #11039 restored part
of it. This PR restores the full guide and corrects it against current
source.
- **Structure:** the original sections, with the ASCII lifecycle diagram
replaced by three Mermaid diagrams (run, turn, session) and ordered hook
lists, plus a handler composition table.
- **Merged:** content added after the cut: tool exposure, calling other
tools, structured output, MCP servers, `registerToolRenderer`, virtual
models, `provider_stream_event`, `before_agent_start` `messageType`.
- **Corrected:** removed or renamed APIs (`modelRegistry.stream()`,
prompt sections are `<tools>`/`<rules>`, `builtin:read` paths,
`highlightCode`), wrong behavior claims (errors only by throwing,
`model_select` on restore, `sendUserMessage` always triggering a turn,
override warning, `truncateLine` bytes), examples that did not
type-check, links to removed `tui.md` patterns and `rpc.md` anchors, and
wrong API lists in the examples table.
- **Kept:** anchors that other docs and changelogs link to, as `<a id>`
tags.
- **Fixed example:** `reload-runtime.ts` tool queued `/reload-runtime`
with `deliverAs: "followUp"`. Queued text reaches the model as a plain
user message, so the command never ran and Pi never reloaded. The tool
now sets a flag and returns `terminate: true`; an `agent_settled`
handler sends `/reload-runtime` with `expandPromptTemplates: true`,
which runs after the settled handlers finish, while the agent is idle.
In the long run it may be better to revisit how API reference is,
provided to the agent, especially since more capable models do explore
the source without it.
## Validation
- `npm run check`
- Edited TypeScript examples type-check against current types
- All relative links and anchors in `extensions.md` resolve, including
inbound anchors from other docs
- `reload-runtime.ts`: faux-provider harness test (temporary, not
committed) shows one reload after the run settles and no extra model
request; the old version never reloads. Also verified in interactive
mode with a real model.
- Reviewed by GPT-6 Astra and GPT-5.6 Sol against source; all findings
applied
Chord's shared watch: listeners take a tagged call, { kind: "value", value, ops, retired } or
{ kind: "failed", error }; a recoverable failure keeps the watch open and the next value arrives as
[["r", value]]; WatchEnd is tagged by kind, and an aborted context ends a watch "aborted".
Durable: observers take the watch's shapes (advance(call, ctx), end(end)); onReport(report) with
report.kind and report.error, session_failed is storage_failed; subscribeCommits is observeCommits;
SessionEnd and ReportFrom are tagged by kind; FileChange is tagged and file watchers stop with
stop(ctx); CompactionFailure and CompactionStatus name their trigger (pi.live version 2 with a
migrate); the task_outdated report uses the scheduler's reasons; TaskInspection's ready is pending;
DocumentCommitChange is tagged. Stored formats are unchanged.
LiteLLM's pre-call context check rejects requests that are too large
with HTTP 400 and a message like:
```
litellm.ContextWindowExceededError: litellm.BadRequestError: litellm._pre_call_checks: Context Window exceeded for given call. ... Max Input Tokens=262144, Got=265262 ...
```
None of the `OVERFLOW_PATTERNS` matched this wording, so
`isContextOverflow()` returned false. As a result, the agent session
never ran overflow compaction and retry, and the turn failed with the
400 error.
This change adds `/context window exceeded/i` to `OVERFLOW_PATTERNS` and
documents the error in the doc comment that lists providers.
Testing:
- Added a regression test in `packages/ai/test/overflow.test.ts` with
the exact error message from the issue. It fails without the pattern and
passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/overflow.test.ts` in `packages/ai`: 32 passed.
- `npm run check` passes.
- Running `isContextOverflow()` directly on the reported message
returned `false` before this change and returns `true` after it.
Fixes#11492
Requested by @rwachtler
Co-authored-by: triagent <triagent@localhost>
A generation whose request or poll fetch was in flight at a host death counts
one failed attempt of the retry policy (backoff, the partial kept, the same
deferred handle polled again); without retries left the run ends with
runFailure interrupted. restartLimit ends every other task visibly instead of
parking it (tool error result, compactionFailure, runFailure restart_limit,
faulted extension tasks) and reports task_interrupted; retryTask, task_parked
and the restart_limit blocked reason are removed.
closes#11139
ConversationView.entries is a window over the fork-aware history with before;
conversation.watch(ctx, { from, min, max }) and observeConversation take the
window. ConversationTranscript merges windows into a client's transcript with
gaps, loadEarlier, fillGap and resume. pi.live.runFailure says why a run ended
without an answer. watchEvents and its event types are removed; the changelog
has the migration guide.
closes#11327
update the llama.cpp docs to use the llama.app installer and the new
`llama serve` command
commit by @hanouticelina
disclaimer: my julien-cto agent helped me write this
---------
Co-authored-by: Celina Hanouti <hanouticelina@gmail.com>
Co-authored-by: Mario Zechner <contact@badlogicgames.com>
When the daemon exits before `hello`, the error has only the exit code.
For example, with `loginShell: true` and a profile that aborts on a
missing file:
```
pi-env exited with code 3 before it was ready
```
`sshConnection`'s probe and deploy steps already report ssh's stderr, so
this is about the launch itself: a login profile, a daemon that can't
execute, or a `Connection` created with its own command, which has no
probe or deploy step, so auth and network errors also end up here.
This keeps a short stderr tail until `hello` and adds it to startup
errors:
```
pi-env exited with code 3 before it was ready
remote stderr:
profile: /opt/toolchain/env.sh: No such file or directory
```
The tail is capped at 20 lines / 2000 chars, with terminal escapes and
control characters stripped. Ready sessions are unchanged.
I hit it while testing `loginShell` against an sshd in Docker. Tests are
in `test/connection.test.ts`, and `npm run check` and `./test.sh` pass.
Display math (`$$…$$`, `\[…\]`) renders as several 2D rows: numerator,
fraction bar and denominator are separate lines. When the layout was
wider than the Markdown content width, `render()` wrapped each row with
`wrapTextWithAnsi` on its own. Only the overflowing row broke, so the
rows below moved down and the equation no longer lined up. For example,
`dx` landed between the fraction bar and the denominators.
The `latexBlock` case in `Markdown.renderToken()` now checks the
rendered layout against the available width. If any row is too wide, it
uses the raw LaTeX source, the same fallback already used when parsing
fails. The raw source then wraps like normal text. Layouts that fit
render exactly as before.
**Testing**
- Added a regression test in `packages/tui/test/markdown.test.ts` using
the equation from the issue. At width 40 it renders the 2D layout
unchanged. At width 30 it falls back to the raw `\[ … \]` source, with
no fraction bars and no line wider than 30. The test fails without the
fix and passes with it.
- `node --test test/markdown.test.ts`: 93 pass. `node --test
test/latex.test.ts`: 111 pass.
- `npm run check` reports no errors in the touched files. The only
errors are 6 type errors in `packages/ai/test`, caused by stale
generated model data, and they are the same on `main` without this
change.
- Reproduction at width 30, before the fix:
```
∂² u ∂² u n 1 1
──── + ──── = ∑ ── +∫ f(x)
dx
∂ x² ∂ y² i=1 i² 0
```
After the fix, the raw source, wrapped:
```
\[
\frac{\partial^2 u}{\partial
x^2} + \frac{\partial^2
...
\]
```
Fixes#11483
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
When writing a session entry failed (ENOSPC, EBUSY, ETIMEDOUT on network
drives), `SessionManager._appendEntry` had already added the entry to
memory and made it the current leaf. The process kept running, and the
next entry written to disk pointed to a parent that was not in the file.
On resume, pi follows the history only back to that gap: everything
before it disappears. In #11460 that also dropped the first system
message, which declares the tools, so resume turned off every built-in
tool.
`_appendEntry` now undoes the in-memory append if writing fails, then
rethrows. The error reaches the caller as before; later entries chain to
the last entry actually on disk.
Reports of this problem:
- #11460: ENOSPC; resume dropped all built-in tools.
- #10166: failure injected with EBUSY; the next entry on disk points to
a missing parent.
- #10661: ETIMEDOUT on Windows + NFS; resumed sessions showed only the
last few messages. That report also describes NUL holes in the file,
which this change does not address.
**Testing**
- Two tests in `test/session-manager/file-operations.test.ts` make
writes fail with real file permissions: a read-only session file (failed
append) and a read-only session directory (failed first file creation).
They check that the entry is not kept, the leaf is unchanged, the next
entry chains to the last written one, and the reopened session's branch
contains both entries. Both fail without the change and pass with it.
- `test/session-manager/` and `test/default-tools-setting.test.ts` pass
(140 tests).
- `npm run check` passes.
Fixes#11460
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>
The `/tree` "Summarize" option ignored the session's thinking level.
`AgentSession` already passes `thinkingLevel` into the
`generateBranchSummary` options, but `generateBranchSummary` never read
it, so the request was built without `reasoning`.
For reasoning models on OpenAI Responses and Azure Responses, a missing
`reasoning` becomes `effort: thinkingLevelMap.off ?? "none"`. Models
that don't accept `"none"` then reject the branch summary with a 400
error:
```
Unsupported value: 'none' is not supported with the 'gpt-6-astra-…' model.
```
This change adds `thinkingLevel` to `GenerateBranchSummaryOptions`. It
sets `reasoning` on the request when the model supports reasoning and
the level is not `off`, the same rule compaction uses in
`createSummarizationOptions`.
**Testing**
- Added a test to
`packages/coding-agent/test/branch-summarization.test.ts` that records
the stream options for three cases: a reasoning model at `xhigh`, a
reasoning model at `off`, and a non-reasoning model. The expected
`reasoning` values are `["xhigh", undefined, undefined]`. Without the
fix, the test fails and every case gets `undefined`. With the fix, it
passes.
- Ran `branch-summarization`, `agent-session-branching`,
`branch-summary-extensions` and `compaction-summary-reasoning`: all
pass.
- `npm run check` passes.
- Not tested against a real provider endpoint.
Fixes#11444
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>
Retry Bedrock strict tool limit rejections once with strict: "prefer" tools non-strict, as the Anthropic provider already does. Share the limit error messages and the prefer check between both providers.
closes#11416
GPT-5.5 and older namespace tools loaded through additional_tools or tool
search and reject replayed calls without the namespace, so switching to them
after another model's tool calls failed with 400. Drop dynamic tool loading
for these models on OpenAI and OpenCode; keep mid-conversation system messages.
closes#11415
closes#11132
pi sends every opencode and opencode-go request to the shared base
(`https://opencode.ai/inference/v1`, `/inference/go/v1`), Jev included.
this switches them to the documented protocol-specific endpoints, which
is also what models.dev lists per model:
| api | before | after |
|---|---|---|
| openai-completions / openai-responses | `/inference/v1` |
`/inference/openai/v1` |
| anthropic-messages | `/inference` | `/inference/anthropic` (SDK
appends `/v1/messages`) |
| google-generative-ai | `/inference/v1` | `/inference/google/v1beta` |
| jev-1.13 / jev-1.13-free | `/inference/v1` | `/inference/systemone/v1`
|
go uses the same paths under `/inference/go/...`.
changes: base URLs in `generate-models.ts`, the jev URL expectations in
`test/classifier-models.test.ts`, and a stale comment in
`providers/opencode.ts`. `models.generated.ts` has no diff since the
catalog data is gitignored.
verified with the regenerated catalog:
- all 111 opencode/opencode-go models hit their new URL, none 404
- real requests with an OpenCode key returned replies on
anthropic-messages, google-generative-ai, openai-responses,
openai-completions and jev
- go returned 403 "subscription required" from the new paths (my key has
no Go sub), so routing works there too
`npm run check` passes. `./test.sh` passes for ai and coding-agent; the
only remaining failures were timeouts that pass in isolation and pi-env
tests that need cargo locally.
---
_Drafted with AI assistance, reviewed by me._
The Node driver takes the count from node:sqlite; the Durable Object driver reads
SELECT changes() after the statement, since the cursor's rowsWritten also counts
index writes (verified in workerd). run is unchanged and costs nothing more.
Inline `ctx.ui.custom()` components never report pi as blocked through
OSC 7501 program status. This is on purpose, because the same API hosts
loaders. As a result, a custom dialog that waits for the user (an
approval prompt, the `question.ts` example) shows up as `working`, and
the terminal cannot tell that pi needs input.
The extension dialog stack already stores a blocked status for each
dialog and reports the visible one. This change adds an optional
`blocked` option to `ctx.ui.custom()`:
```ts
ctx.ui.custom(factory, { blocked: { kind: "question", message: "Pick a color" } });
```
The inline branch of `showExtensionCustom()` passes it to
`pushExtensionDialog()`. Components without the option behave as before.
Overlays ignore the option because they are not part of the dialog
stack. `docs/tui.md` documents the option.
Testing:
- Added a test to `test/interactive-mode-extension-dialogs.test.ts`:
- A custom component that opts in reports `blocked`.
- A plain custom component stacked on top of it clears the blocked
status.
- Closing the plain component restores the blocked status.
- Closing the opted-in component clears it.
- The test fails without the change to `interactive-mode.ts` and passes
with it.
- `npm run check`: biome and lint pass, and there are no type errors in
coding-agent. The only errors are existing type errors in
`packages/ai/test` from locally generated model data. They are not
related to this change.
Fixes#11428
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
When thinking was hidden and the hidden-thinking label was set to `""`
(for example with `ctx.ui.setHiddenThinkingLabel("")`), every assistant
message that contained thinking still drew blank rows:
- thinking followed by a tool call drew 2 blank rows;
- thinking followed by text drew 3 blank rows before the text.
`AssistantMessageComponent.updateContent()` had three causes:
- It counted hidden thinking as visible content when deciding on the
leading and trailing spacers.
- It always added the label component. With an empty label the theme's
escape codes are still there, so the component rendered one blank row.
**What changed**
- A hidden thinking run with an empty label is now skipped entirely. Its
run index is still advanced, so click overrides on other runs keep their
indices.
- Spacers are now added lazily, just before the next visible block. The
result is the same as before: one spacer before the first visible block,
and one after a visible thinking run that is followed by more visible
content. But spacing now follows what actually renders.
- Default behaviour (the "Thinking..." label, or expanded thinking) is
unchanged.
**Testing**
- Added two tests to `test/assistant-message.test.ts`:
- a hidden thinking run with an empty label renders nothing, both before
a tool call (`[]`) and before text (`["", "answer"]`);
- with the default label, rendering still gives `["", "Thinking...", "",
"answer"]`.
- The empty-label test fails without the fix and passes with it.
- `npm run check` passes. The `test/assistant-message.test.ts` and
`test/interactive-mode*` tests pass.
- Rendering the component directly, with hidden thinking and an empty
label:
- thinking + tool call: before, 2 blank rows; after, `[]`;
- thinking + text: before, 3 blank rows then the text; after, `["", "
hi"]`.
Fixes#11402
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
`pi install`, `pi remove` and `pi update --extensions` run npm without
`--no-audit`, so npm prints its "N vulnerabilities … run `npm audit
fix`" advice after every install. pi gives users no way to clear that
warning, and following it causes harm. In `~/.pi/agent/npm`, `npm audit
fix` runs without `--legacy-peer-deps`, so npm installs the
host-provided `@earendil-works/pi-*` peers there: a second copy of pi.
The comment above `getNpmInstallArgs` warns against exactly that.
The change adds `--no-audit --no-fund` wherever pi already passes
`--legacy-peer-deps` to npm:
- `getNpmInstallArgs` (npm package installs and updates)
- `getGitDependencyInstallArgs` (dependency installs for git packages)
- npm uninstall
pi's managed self-update (`runManagedNpmCi`) already passes both flags.
bun and pnpm arguments are unchanged.
I did not edit `CHANGELOG.md`, per CONTRIBUTING.md.
**Testing**
- Updated the expected npm argument arrays in
`test/package-manager.test.ts`. With the source change reverted, 11
tests fail; with it, all 132 pass.
- `npm run check` passes.
- Ran the built CLI by hand with `PI_CODING_AGENT_DIR=/tmp/pa pi install
npm:lodash@4.17.20`:
- Before the change, the output ended with npm's "To address all issues,
run: npm audit fix" block.
- After, it shows only "added 1 package" and "Installed
npm:lodash@4.17.20".
Fixes#11409
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
With the kitty keyboard protocol active, these function keys were not
recognized:
- F1, F2 and F4 arrive as `CSI P`/`Q`/`S`, or as `CSI 1;<mod> P/Q/S`.
- Any F-key pressed with a modifier, e.g. `CSI 15;5~` for Ctrl+F5.
`matchesKey` returned false whenever a modifier was set and only
compared against the legacy sequences. `parseKittySequence` did not know
any F-key forms. So shortcuts bound to keys like `f1` or `ctrl+f5` never
fired.
Separately, F13–F35 (and other kitty functional keys) arrive as CSI-u
codepoints in the Private Use Area. `decodeKittyPrintable` returned them
as text, so the editor inserted invisible U+E020+ characters into the
prompt.
Changes in `packages/tui/src/keys.ts`:
- Add F1–F12 internal codepoints. Map `~` numbers 11–15, 17–21, 23 and
24 to them.
- Parse the `CSI [1;mod[:event]] P/Q/S` form. F3 is left out because
`CSI 1;<n>R` collides with cursor position reports, so kitty sends F3 as
`CSI 13~`.
- Let the `f1`…`f12` cases in `matchesKey` fall back to
`matchesKittySequence` so modifiers work. Name the keys in `parseKey`
output.
- Make `isKeyRelease` and `isKeyRepeat` recognize `:3`/`:2` events on
P/Q/S, so F1/F2/F4 releases don't trigger shortcuts a second time.
- `decodeKittyPrintable` now rejects the kitty functional-key PUA range
(57344–63743). Keypad keys are normalized to ASCII first, so they still
decode.
Testing:
- Added tests in `packages/tui/test/keys.test.ts`. They cover kitty
F1–F12 with and without modifiers, F3 not being confused with a cursor
position report, release and repeat detection, and F13+ not decoding as
text. Two of the new tests fail without the change; all pass with it.
- Ran `npm test` in `packages/tui` and `npm run check`; both pass.
- Before the change: `parseKey("\x1b[P")` and `parseKey("\x1b[1;5P")`
returned `undefined`, `matchesKey("\x1b[15;5~", "ctrl+f5")` returned
false, and `decodePrintableKey("\x1b[57376u")` returned `"\ue020"`.
- After the change: these give `f1`, `ctrl+f1`, true and `undefined`.
Fixes#11419Fixes#11345
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
The `openai` provider still defaulted to `gpt-5.5`. The same default
applies to API-key logins and to "Sign in with ChatGPT", so logging in
through ChatGPT selected `openai/gpt-5.5`. The reporter says that model
no longer works with a ChatGPT subscription. The `openai-codex` and
`github-copilot` providers already default to `gpt-6.1-sol`.
This change sets `defaultModelPerProvider.openai` to `gpt-6.1-sol` in
`packages/coding-agent/src/core/model-resolver.ts`. The new value
applies in three places: the model picked after `/login` when no model
was set, the fallback model at startup, and the default model for
`--provider openai`. API-key users get the new default too.
I did not test against a real ChatGPT account, so the claim that the
subscription rejects `gpt-5.5` is not checked here.
Testing:
- Updated the assertion in `test/model-resolver.test.ts` ("openai
defaults track current models"). It now expects `gpt-6.1-sol`; the
model-resolver tests pass.
- `npm run check` passes.
- `pi --list-models gpt-6.1` still lists `openai/gpt-6.1-sol`, so the
new default is in the catalog.
Fixes#11442
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
`ctx.ui.setEditorComponent()` always cleared the editor slot and put the
new editor there with focus. If an extension dialog
(`confirm`/`select`/`input`/`editor`, or an inline `custom()` component)
was open at that moment, the editor replaced it on screen. The dialog
was still on the dialog stack and its promise still pending, but nobody
could see or answer it. It hung until its timeout, or forever without
one.
Now `setCustomEditorComponent` leaves the editor slot and focus alone
while a dialog is open. The new editor is still created and wired up,
and `showTopExtensionDialog` shows it when the last dialog closes.
Without an open dialog nothing changes.
Testing:
- Added a regression test to
`test/interactive-mode-extension-dialogs.test.ts`. It opens a confirm,
replaces the editor, then checks that the confirm stays visible and
answerable and that the new editor is focused afterwards. It fails
without the fix and passes with it.
- `npm run check` passes.
- Ran the reproduction from the issue in tmux (`pi -e ./a.ts -e ./b.ts`,
where a.ts opens a non-blocking `confirm` in `session_start` and b.ts
calls `setEditorComponent`).
- Before: only the editor shows, and the confirm times out.
- After: the confirm shows, and Enter gives `answer: true`.
Fixes#11431
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
When `context` is set, the grep tool reads the file again to print the
lines around each match. It turned CRLF into LF and also turned every
lone CR into LF before splitting the text into lines. ripgrep, which
supplies the match line numbers, counts lines only at LF. A lone CR
therefore added an extra line on pi's side, so every later line was off
by one.
For example, in a file containing `before\rprogress\nneedle\nafter`, a
search for `needle` with `context: 1` showed `progress` as the line-2
match and left out `after`.
The fix keeps the CRLF normalization and splits only on LF, so pi
numbers lines the same way ripgrep does. Displayed lines already have
`\r` removed, so the line before the match now shows as
`beforeprogress`.
**Testing**
- Added a test in `packages/coding-agent/test/tools.test.ts` for a file
with a lone CR searched with `context: 1`. It fails without the fix and
passes with it.
- `test/tools.test.ts`: all 87 tests pass.
- `npm run check` passes.
- Reproduction from the issue. Before the fix:
```
f.txt-1- before
f.txt:2: progress
f.txt-3- needle
```
After the fix:
```
f.txt-1- beforeprogress
f.txt:2: needle
f.txt-3- after
```
Fixes#11398
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
The root and ./testing exports of pi-telemetry had no source
condition, so Durable's and pi-env's tests, which resolve with source,
failed to import it without a build: the Environments workflow has
failed on every run since 2026-10-18.
Durable publishes a stored commit inside Storage.commit's adopt: the
telemetry events, its own observers (document watches, conversation
views, task graph, scheduler, waits), and a dispatch of host
subscribeCommits listeners one microtask later, before every wait and
abort the commit triggers. A commit the storage rejects after adoption is
published before the Session fails; a throwing telemetry adapter is
reported as host_error "telemetry" instead of rejecting a durable commit.
Chord's watch listeners take one change object, { value, ops, retired }
({ value } for native watches); retirement is a flag, not an end.
Durable's document watches follow their address, and a task document's
watch ends after its retirement call. New synchronous observers for
adapters: observeDoc, observeConversation, observeTaskGraph; the watches
are built on them. The env file watcher's change type is renamed
FileChange.
Add gpt-6.1-terra to the OpenAI, Azure OpenAI Responses, and OpenAI
Codex providers with official pricing ($1 input, $0.05 cached input,
$1.25 cache writes, $5 output, long-context tier above 272k), tool
search, additional tools, and mid-conversation effort changes, as for
GPT-6.1 Sol. Unlike Sol it accepts reasoning.effort "none", so off is
supported; both reject "minimal" (verified against Codex). Codex serves
it with a 500K context window.
- ai: OAuth logins only accept http(s) URLs from servers (shared
trustedHttpUrl, now also checked by Radius); pi-messages rejects
contentIndex values that are not the next or an existing block; the
CLI auth file is written with mode 0600
- ai: OpenCode DeepSeek V4.1 Flash (renamed from V4 Flash upstream) no
longer sends prompt_cache_retention
- tui: setTitle() replaces control characters; export
replaceControlCharacters()
- coding-agent: the login dialog only opens web URLs and never prints
control characters from sign-in, device-code, or info URLs
- durable: partial commits no longer merge a new __proto__ member into
Object.prototype
Split build-check-test into build-check plus a test matrix (coding-agent,
durable, other). Install only fd and ripgrep, and only in jobs whose tests
use them; the cairo/pango/jpeg/gif/rsvg dev libraries were for compiling
canvas, which npm ci --ignore-scripts never does.
A query that equals a tool's full name or the part after a __ separator
(thread_read for mcp__app__thread_read) now ranks that tool first in
tool_search and codemode searchTools(). BM25 length normalization could
otherwise rank a shorter description sharing the name's words above it.
closes#11331
`scripts/check-entry-graphs.mjs` built the list of files each export
reaches with `path.relative()`, which returns paths with `\` separators
on Windows. The allow and forbid patterns are written with `/`, which
caused two problems on Windows:
- The allow entry `packages/telemetry/src/index.ts` never matched, so
the bare `index.ts` forbid pattern flagged it and the check failed even
though nothing was wrong.
- Forbid patterns that contain a `/` (`providers/`, `runtime/host.ts`,
...) never matched, so real violations went unreported.
The fix converts the relative paths to `/` before any comparison. On
Linux and macOS nothing changes.
Testing:
- The scripts have no unit tests, so no test was added.
- `node scripts/check-entry-graphs.mjs` and `npm run check` pass on
Linux.
- I simulated Windows path handling with `path.win32`, without running
Windows itself:
- Before the fix: `relative(...)` gives
`packages\telemetry\src\index.ts`, and
`endsWith("packages/telemetry/src/index.ts")` is false.
- After the fix: the path becomes `packages/telemetry/src/index.ts`, and
the suffix check is true.
Fixes#11333
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
`--model "<provider>/<id>"` returned "not found" when the provider id
itself contains a `/`. URL-derived ids such as
`llama-server=http://host:10800` are one example.
`splitProviderPrefix()` in `model-resolver.ts` split the reference at
the first `/`, which turned the prefix into `llama-server=http:`. No
provider has that id, so resolution fell through to not-found.
`splitProviderPrefix()` now compares the text against the provider ids
in the catalog. A provider matches when the text starts with its id
followed by `/`, and the longest matching id wins. Ordinary provider ids
resolve exactly as before.
Testing:
- I added two cases to the `resolveModelReference` table in
`test/model-resolver.test.ts`, using a provider
`llama=http://host:10800`: one plain, one with a `:high` thinking
suffix. Both fail without the change and pass with it. The test runs
every case against the catalog in both orders.
- `node ../../node_modules/vitest/dist/cli.js --run
test/model-resolver.test.ts`: 64 passed.
- `npm run check`: passes.
- Before the change, a standalone check of `resolveModelReference` with
the reporter's exact provider and model
(`llama-server=http://host:10800/unsloth/Qwen3.8-27B-GGUF:BF16`)
returned `not-found`. It now returns `resolved`.
- One test, `test/model-runtime-classifiers.test.ts` ("applies provider
compat from models.json to classifier models"), fails in my environment
both with and without this change. It is unrelated to this fix.
Fixes#11394
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
When the agent directory is reached through a symlink (for example
`~/.pi` linked into a dotfiles repo), pi passed the symlinked path as
`--prefix` to `npm install` and `npm uninstall`. npm then writes lock
entries such as `../../../dotfiles/pi/agent/npm/node_modules/foo`
instead of `node_modules/foo`. Each later install adds one more nesting
level, so `package-lock.json` keeps growing.
This change resolves the install root with `realpathSync` before passing
it to the package manager:
- in `getNpmInstallArgs()`, which covers install and update for npm,
pnpm and bun;
- in `uninstallNpm()`.
The paths pi loads extensions from are unchanged.
**Testing**
- Added a regression test in `test/package-manager.test.ts`. It uses a
symlinked agent dir and checks that install and uninstall get the real
path as `--prefix`. The test fails without the fix and passes with it.
- Wrapped the existing `--prefix`/`--cwd` expectations in
`realpathSync(...)`. They now match on systems where the temp dir is
itself a symlink, such as macOS.
- Ran `npm run check` (clean) and `test/package-manager.test.ts` plus
`test/package-manager-ssh.test.ts` (all pass).
- Manual reproduction with `~/.pi` as a symlink, running `pi install
npm:ms` several times:
- before: lock keys grew to
`../../../dotfiles/pi/agent/npm/node_modules/ms`,
`../../../dotfiles/dotfiles/pi/...`, and so on;
- after: `[ '', 'node_modules/ms' ]`, and `pi remove npm:ms` leaves `[
'' ]`.
Fixes#11358
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
`./pi-test.sh --no-env` is supposed to run pi without any API keys, but
its hard-coded `unset` list had fallen behind
`packages/ai/src/env-api-keys.ts`. Credentials such as
`DEEPSEEK_API_KEY`, `MOONSHOT_API_KEY` and `ANTHROPIC_AUTH_TOKEN` were
still picked up from the environment.
This change adds the 26 missing variables to the list:
- the Anthropic auth token and federation variables
- the API keys for Ant Ling, Baseten, Cloudflare, DeepSeek, Fireworks,
Google Cloud, Kimi, Meta, Moonshot, Nvidia, Qwen token plan, Radius,
Together, Typesafe, Xiaomi (all plans) and ZAI coding CN
The list now covers every variable name in `env-api-keys.ts`.
Testing:
- No automated test covers this shell script, so none was added.
- A loop over every variable name in `env-api-keys.ts` confirmed each
now has an `unset` in `pi-test.sh`.
- Before the change, `PI_CODING_AGENT_DIR=$(mktemp -d)
DEEPSEEK_API_KEY=x ./pi-test.sh --no-env --offline -ne --list-models
deepseek` listed DeepSeek models.
- After the change, the same command with both `DEEPSEEK_API_KEY` and
`MOONSHOT_API_KEY` set prints "No models available."
- `bash -n pi-test.sh` and `npm run check` pass.
Fixes#11359
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
A commit may be marked deferrable ({ persistence: { kind: "deferred", withinMs? } }) on Session, Conversation,
TaskRuntime and a tool's api.commit. When every write changes an existing conversation document with history
"latest" and fork "initial", it is adopted in memory and shown to watches at once, and stored with the next commit
that reads or writes the document, before the document leaves memory, at close, or within withinMs (default
settings.progress.persistWithinMs, 1000 ms). The generation partial throttle and the tool output throttle use it.
subscribeCommits() gets these commits only once stored, with exactly what was stored; internal observers get
memory-only publications. Spec §1/§13 changed, new §4.1. 32 streams over SQLite: storage transactions 562/s -> 99/s,
disk 14.3 -> 4.9 MB/s, CPU 29% -> 22% of one core. A crash loses up to about 1 s of streamed content.
Tracker revisions, applyImmutable() and applyImmutableBatches() copy each container on a changed path with
spread (Object.assign() onto a null-prototype object) instead of one Object.defineProperty() per key: same own
data properties, prototype and own __proto__ member, about 25 times faster per container. The copy-count test
counts key listings through a proxy instead of spying on Object.keys.
Adds a tracker GC regression test: 10,000 adopted changes retain only the document, not a context registration
per change. Follow-up to #9265.
Kimi Coding, Moonshot, and OpenCode Go generated K3.1 Pro and K3.1 Pro Highspeed with Kimi K2 settings because K3 detection matched exact IDs. Detect the K3 family by ID pattern, and Kimi Coding K3-style models by their effort options. Add implied subscription costs and clamp gateway output limits like K3.
- chord/delta: PendingOps (push, size, take -> PendingTake): raw pending ops, r drops everything
before it, a leaf index merges appends and primitive sets per path (cleared by any other op),
past 10,000 ops only "replace with the current value"; one batch is kept by reference and
received ops are never mutated
- chord: Watch queues frames in PendingOps while its listener is busy or not started and delivers
one call with the newest value, the folded ops (or [["r", newest]] after an overflow) and the
newest frame's context; replaces the 100-frame collapse. NativeWatch is unchanged and takes an
optional replace()
- durable: watchEvents keeps every event batch in order (NativeWatch with a snapshot replace);
document, view and task-graph watches fold; spec, README, example 04 and tests updated
- tests: fold fuzzers (tracker, appendText, splices/reorders, root arrays, diffRevisions) with
codec round trips, adversarial cases, cap, ownership, cost and GC guards
The suite's evicting run (PI_DURABLE_DOCUMENT_RETENTION_MS=0) evicts the unwatched pi.live after every
commit, so each partial commit replayed the document's delta chain and the per-commit cost grew with the
stream. Pin the document retention in that test so it measures appends, as the default retention does.
Follow-up to #9265.
- chord: Delta appendText(draft, key, text) / StringKeys<T> record an append op without the
O(length) prefix compare that a plain `draft[key] += text` costs in prepare()
- durable: partial commits append text and thinking deltas with appendText (assignJson plan,
checked by length), and apply tool-call argument changes from their own StreamingJsonParser
of the raw toolcall_delta text instead of reassigning the arguments snapshot
- durable "." entry-graph budget 73 -> 76 for the parser it now imports