7145 Commits
Author SHA1 Message Date
Mario Zechner a92283056d fix(chord): Delta's tracker holds its one open change without a WeakRef
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.
2026-10-27 23:46:11 +00:00
Christian Klotz 85c2ee3e76 docs(coding-agent): restore the full extensions guide, corrected against current source (#11498)
## 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
2026-10-28 00:31:24 +02:00
Mario Zechner bb65e3e497 fix(durable): import toCodemodeIdentifier from pi-codemode/declarations
The Code Mode extension imported it from /portable, which loads the WASM
QuickJS runtime the extension never uses.

closes #11254
2026-10-27 21:31:47 +00:00
Mario Zechner 5f312dac4c feat(chord,durable): failures are watch calls; one vocabulary for watches, observers and reports
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.
2026-10-27 20:38:08 +00:00
robopipi[bot]andtriagent d3398f826a fix(ai): detect LiteLLM ContextWindowExceededError as context overflow (#11494)
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>
2026-10-27 20:27:30 +01:00
Mario Zechner b3a29f1111 fix(durable): end restart loops visibly; a host death charges a generation's attempt
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
2026-10-27 16:57:59 +00:00
Mario Zechner f3255de765 feat(durable): windowed conversation view, ConversationTranscript and runFailure; remove watchEvents
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
2026-10-27 16:57:24 +00:00
Mario Zechner 72363383ff docs: allow npm run build when tests need dist/; npm run check stays the check 2026-10-27 15:42:21 +00:00
59ca40cf58 docs(coding-agent): update llama.cpp setup for llama.app (#10179)
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>
2026-10-27 16:21:37 +01:00
Michael Yu 479d6e5e29 fix(env): include stderr in pi-env startup errors (#10716)
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.
2026-10-27 16:16:49 +01:00
robopipi[bot]andtriagent cd1ea342ae fix(tui): fall back to raw LaTeX when display math is wider than the content width (#11487)
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>
2026-10-27 16:09:08 +01:00
robopipi[bot]andtriagent 807a5c051f fix(coding-agent): keep session memory in line with the file when a write fails (#11486)
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>
2026-10-27 15:58:08 +01:00
robopipi[bot]andtriagent 269afc66ab fix(coding-agent): pass thinking level to branch summarization (#11482)
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>
2026-10-27 15:38:06 +01:00
David Brailovsky 842c73170a fix(ai): fall back on Bedrock strict tool limits
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
2026-10-27 14:45:23 +01:00
David Brailovsky 81d6759fc9 fix(ai): declare GPT-5.4 and GPT-5.5 tools at the top level
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
2026-10-27 14:25:45 +01:00
Mario Zechner db14038bd8 fix(chord): bound the Node SQLite driver's prepared-statement cache
The driver kept every statement it ever prepared. It now keeps the 256 most
recently used per connection; below half the limit a hit stays one lookup.
2026-10-27 11:28:28 +00:00
Victor Navarro 177e2ad7e4 fix(ai): use OpenCode protocol-specific endpoints (#11434)
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._
2026-10-27 10:40:41 +01:00
Mario Zechner 973b74c55f fix(chord): keep the workerd runCounted probe row outside the bulk range 2026-10-27 08:58:50 +00:00
Mario Zechner 8025c414d1 feat(chord): SqliteExecutor.runCounted resolves with the rows a statement changed
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.
2026-10-27 08:54:53 +00:00
Christian Klotz 531340f100 fix(ai): remove unused root bedrock-provider shims
./bedrock-provider has resolved to dist/ since a7559f01e (v0.59.0), and
the root files are not in the published package.
2026-10-27 10:39:28 +02:00
robopipi[bot]andtriagent c1810bf70f feat(coding-agent): let inline ctx.ui.custom() components report blocked program status (#11471)
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>
2026-10-27 09:27:28 +01:00
robopipi[bot]andtriagent a6a7bac868 fix(coding-agent): hidden thinking with an empty label renders no rows (#11470)
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>
2026-10-27 09:19:37 +01:00
robopipi[bot]andtriagent 5fb9a0b110 fix(coding-agent): skip npm audit and fund output when installing pi packages (#11469)
`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>
2026-10-27 09:17:54 +01:00
robopipi[bot]andtriagent deb12d78eb fix(tui): recognize kitty F1-F12 sequences and ignore F13+ keys (#11468)
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 #11419
Fixes #11345

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-27 09:16:36 +01:00
robopipi[bot]andtriagent f049787d9e fix(coding-agent): default openai provider to gpt-6.1-sol (#11467)
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>
2026-10-27 09:13:12 +01:00
robopipi[bot]andtriagent 7853186d14 fix(coding-agent): keep open extension dialog visible when the editor component is replaced (#11466)
`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>
2026-10-27 09:11:18 +01:00
robopipi[bot]andtriagent fecc19e4e9 fix(coding-agent): keep grep context lines aligned with ripgrep when a file has a lone CR (#11465)
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>
2026-10-27 09:07:23 +01:00
Mario Zechner 6c384aa30f fix(ai): resolve pi-telemetry from source in workspace tests
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.
2026-10-26 21:05:54 +00:00
Mario Zechner 8b0caefc22 feat(durable,chord): publish commits at adoption, address-following watches, synchronous observers
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.
2026-10-26 20:44:34 +00:00
Armin Ronacher 363ad78bf0 feat(ai): add GPT-6.1 Terra
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.
2026-10-26 21:27:08 +01:00
Armin Ronacher 89c921d8b5 fix: harden handling of untrusted URLs, terminal text, and streamed data
- 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
2026-10-26 20:46:31 +01:00
Petr Houška f9daca3480 ci: run tests in parallel jobs and drop unused apt packages
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.
2026-10-26 14:28:25 +01:00
Petr Houška ae823bc0bb fix(coding-agent): rank tools first when the search query is their name
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
2026-10-26 14:17:45 +01:00
triagent c17be1e982 fix: skip custom-message runs in prompt-url-widget 2026-10-26 13:58:51 +01:00
triagent 66c2fa3d7a fix(coding-agent): update undici to 8.12.3 for security advisories 2026-10-26 13:50:55 +01:00
github-actions[bot] b84d5d1bc3 chore: approve contributors from issue #11132 2026-10-26 11:15:34 +00:00
Mario Zechner d5b3de22d0 fix(durable): record the requested thinking level on assistant messages 2026-10-25 08:05:39 +00:00
robopipi[bot]andtriagent 0eea54f11c fix: normalize path separators in check-entry-graphs on Windows (#11399)
`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>
2026-10-25 08:54:24 +01:00
robopipi[bot]andtriagent 984cf6643d fix(coding-agent): resolve provider/id references when the provider id contains a slash (#11397)
`--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>
2026-10-25 08:48:27 +01:00
robopipi[bot]andtriagent 5f960a0e2b fix(coding-agent): pass real path as npm prefix when agent dir is a symlink (#11396)
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>
2026-10-25 08:47:34 +01:00
robopipi[bot]andtriagent 911fbab32f fix: unset all supported provider env vars in pi-test.sh --no-env (#11395)
`./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>
2026-10-25 08:43:53 +01:00
Mario Zechner 38ff99177f fix(durable): continue runs after tool handoffs 2026-10-24 22:48:11 +02:00
Mario Zechner 6c2fd73b0c feat(durable): deferred storage of throttled commits
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.
2026-10-24 17:49:30 +00:00
Mario Zechner d0aa218cb1 perf(chord): cheaper container copies in Delta's tracker and applyImmutable
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.
2026-10-24 16:56:53 +00:00
Mario Zechner f321e5bdd2 docs(chord): delta README, growing strings through drafts and appendText 2026-10-24 12:17:38 +00:00
Armin Ronacher 1de2d7adea fix(ai): treat Kimi K3.1 Pro models as Kimi K3 family
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.
2026-10-24 13:46:57 +02:00
Mario Zechner bdb151774e fix(chord): give the PendingOps fuzz tests room on loaded CI runners
The tracker fuzz test hit vitest's 5 s default timeout on CI (6.2 s); fewer sequences and a
30 s timeout per fuzz test.
2026-10-24 09:48:13 +00:00
Mario Zechner d8d99950ac feat(chord,durable): PendingOps folds pending ops for slow consumers; Watch delivers one call per busy period
- 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
2026-10-24 09:37:12 +00:00
Mario Zechner 1e42501bb3 fix(durable): keep pi.live cached in the partial-commit cost test
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.
2026-10-24 08:35:20 +00:00
Mario Zechner da412b0392 feat(chord,durable): appendText for cheap string appends; Durable partial commits append instead of reassigning
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
2026-10-24 08:21:41 +00:00