Commit graph

7498 commits

Author SHA1 Message Date
Mario Zechner
0a180b049a feat(p2): keep serving older Sessions, Catalog and SessionTree versions
Pages and TUIs loaded before a deploy hold the contracts they were built
with; without accepts they wait unbound (contract_mismatch) until they
reload. The histories are now written with evolve from the released
versions, and the providers accept them:

- Sessions 3 (rows without main and branches) and 4 (a null branch label
  reads as the session title); v4's treeSelf arguments pass as they are
- Catalog 2 and 3, the same row mappings
- SessionTree 1: the tree without labels (no branches, no node
  conversation); rename is new and needs nothing

test/p2/contract-versions.test.ts binds the released declarations
against the current server.
2026-11-04 15:38:43 +01:00
Mario Zechner
7670c8be43 feat(p2): stored, renamable /tree branch labels
p2.main version 2 stores a label per branch: { conversation, label }.
A new branch is labelled in the commit that forks it, with the start of
the message the owner picked (the answer for "Branch from here", the
edited user message for "Edit"). Conversation 1 keeps null, which every
UI shows as the session title. Version 1 migrates to null labels; at
start the worker fills the other branches once from their fork points.

- SessionTree v2: rename (owner), Tree.branches, TreeNode.conversation
- Sessions v5 / Catalog v4: a branch label may be null
- the worker reports p2.main changes; the derived labels are gone
- web: a pencil beside each sidebar branch row opens a rename sheet
- TUI: /tree shows branch labels; Shift+L renames the selected branch
2026-11-04 15:38:43 +01:00
Mario Zechner
d6bf3549c0 docs(chord): calls in flight survive a loading unbind 2026-11-04 14:36:08 +00:00
David Brailovsky
d3d9c5aeb4 feat(p2): quiet-tools plugin and plugin settings in the TUI
quiet-tools draws every tool call as one line; Ctrl+O, a click or a failure
shows the full content. Its /settings row switches back to the tools' own cards.

TuiSettingFields lets plugins add /settings rows, stored at
settings.plugins.<plugin>.<name> of the visitor's prefs. Tool renderer
resolvers receive the settings; a change redraws every card.
2026-11-04 14:55:06 +01:00
David Brailovsky
e885255061 feat(p2): /tree, branches of the main conversation as Durable forks
Each branch is a top-level conversation forked from another at an entry;
the session-scoped p2.main document lists them and names the current one.
Earlier branches are read-only. The main agent's per-agent state
(subagents, artifacts, voice and chat mode, pending move) stays on
conversation 1 for every branch.

- tree plugin: SessionTree (tree, navigate, select); the worker reports
  main and branches to the catalog (Sessions v4, catalog schema 4)
- TUI: /tree and double Escape (pi's default "tree"), a tree selector
  adapted from coding-agent
- web: Edit on user messages, Branch from here on answers, branch rows
  under the session in the sidebar
- main-agent checks use the branch list instead of conversation 1
- search indexes each branch's own entries

Design: apps/p2/docs/tree.md
2026-11-04 13:40:36 +01:00
Mario Zechner
0a730c8a76 fix(p2): archiving a viewed session shows no error and starts no refused worker
Archiving from the session sheet navigates away first, which unmounted SessionOptions and released its Sessions
reference; Chord cancels a released reference's calls, so the archive call rejected with "p2.sessions: released",
shown as the composer toast over the owner-notice lane. The archive now holds a reference of its own until it is done,
and the server runs its steps in a root of its own.

The archive stopped the worker with Hosts.stop, which restarts it for its holders: the page's lingering
conversation started a worker the archived session refused (resolve_failed, "Worker: Session is archived" notice).
SessionWorkerControl.suspend stops it and leaves its holders suspended until a restore (retry) or their release;
archive, a failed fork's archived copy and an archived session's index rebuild use it.
2026-11-04 13:19:01 +01:00
Mario Zechner
15a5114599 fix(chord): a page cannot claim chord.* attributes on its spans; tutorial layout
The server drops a page's own chord.* attributes and writes chord.host and the principal hook's result itself; tests
for the hook returning nothing, the NOOP adapter and calls over a link.
2026-11-04 12:13:07 +00:00
Mario Zechner
ff33ef42c4 feat(chord): telemetry from pages and workers stored by the server, who a span was for
The four telemetry gaps (rows on commits, chord.host, microsecond times, a read-only store) and: pages record and
send span batches over their link to the server's ingest object (checked, capped, rate-limited, stamped browser and
with the link's principal); workers send theirs too, so the server is the store's only writer; principalAttributes
lets an app choose what a span records about its principal. The tutorial shows a real page's click traced to the
worker, with the trace viewer.
2026-11-04 11:59:14 +00:00
Mario Zechner
80f7a11494 docs(chord): the server is the one place telemetry is stored 2026-11-04 11:01:31 +00:00
Mario Zechner
f666087695 docs(chord): the principal hook caches for itself; a Chord host option 2026-11-04 10:58:46 +00:00
Mario Zechner
72dd26e084 docs(chord): spans record who they were for, through an app hook 2026-11-04 10:57:57 +00:00
Mario Zechner
596523e01e docs(chord): telemetry from browser pages decided 2026-11-04 10:54:06 +00:00
github-actions[bot]
818c42cdeb chore: update Nix model catalog pin 2026-11-04 10:53:01 +00:00
Mario Zechner
0efe50cb6d docs(chord): tutorial chapter on telemetry, with a small trace viewer 2026-11-04 10:40:17 +00:00
Danila Poyarkov
0f4d5e960a
fix(ai): allow prompt cache keys for compatible proxies (#9442) 2026-11-04 11:36:35 +01:00
Mario Zechner
a3a1005053 fix(coding-agent): fix CI type check and image-only steering test
- type-check subagent-extension.test.ts against the extension view like the other subagent test
- update the test to current ToolRenderContext and ExtensionToolContext types
- use a decodable PNG in the image-only steering test; the fake data was omitted by image processing
2026-11-04 10:34:45 +00:00
Dale Lowe
3e4f872654
feat(ai): add Qwen Token Plan Individual CN provider (#7989)
Closes #7847
2026-11-04 11:32:45 +01:00
gaoyk19
14b804d809
feat(ai): add requiresNonNullAssistantContent compat flag (#8118)
Closes #8063
2026-11-04 11:29:54 +01:00
muyiyr
25490ea49b
fix(tui): prevent cursor marker leaks (#9441)
Closes #9257, closes #9332
2026-11-04 11:26:48 +01:00
Michael Renner
aba42ae6b9
fix(coding-agent): make subagent progress and failures reliable (#8250)
The shipped subagent example could say a child was done while it was
still working, lose the useful part of process failures, and return a
normal-looking tool result when the delegated work had actually failed.
Single and chain output could also exceed the normal tool limit.

This also adds a dedicated regression suite covering progress state,
cancellation, process failures, output limits, cwd-based agent
selection, and persisted tool-result error state.

After this change:

- queued, running, completed, failed, and aborted work stay distinct
throughout the run;
- interrupting the parent run preserves partial child results, stops
queued work from starting, and escalates when a child ignores SIGTERM;
- signal exits, spawn failures, provider errors, stderr, and assistant
output remain visible instead of collapsing to `(no output)`;
- failed subagents become failed tool results, so the parent model can
react instead of treating them as successful calls;
- single and chain results stay within the normal 50 KB / 2,000-line
limit;
- project agents are selected and confirmed from each task's actual
working directory rather than the dispatcher's directory.

## Further potential improvements

These are useful follow-ups, but they are separate product and storage
decisions rather than part of the failure-handling fixes in this PR.

<details>
<summary>Potential follow-ups</summary>

### Keep persisted transcripts bounded

Subagent tool results persist the full child message history in
`details`, which can make parent session files large and slow to reload.
A follow-up could compact that history or spill it to disk.

### Define an aggregate parallel-output budget

Parallel mode still gives every task its own 50 KB budget. A follow-up
could keep every task represented while enforcing one overall
tool-result limit.

### Report child usage to the calling session

Per-child token and cost usage is visible in result cards but is not
added to the calling session's totals.

### Isolate malformed agent definitions

One malformed YAML agent can still break discovery for every valid agent
in the same directory.

### Limit subagent recursion

A nesting limit would prevent accidental process and paid-request
multiplication when subagents can invoke the subagent tool again.

* https://github.com/earendil-works/pi/issues/8195

### Add a configurable timeout

Unattended runs would benefit from a generous opt-in timeout for
children that never complete.

</details>

## Related issues

### Spawn failures lose their diagnostics

This directly addresses the reported case where a nonexistent child
working directory produces `(no output)` instead of the actual spawn
error. The regression suite includes that same reproduction.

* https://github.com/earendil-works/pi/issues/8129

### Persisted child history can bloat sessions

This PR intentionally keeps full child history in tool-result details,
so session compaction or spill-to-disk remains follow-up work rather
than something fixed here.

* https://github.com/earendil-works/pi/issues/7452
2026-11-04 11:24:12 +01:00
Mattia Cerutti
dbc61d647b
feat(durable): expose thinking budget and websocket timeout options (#10410) 2026-11-04 11:22:51 +01:00
wren
51313540a4
fix(coding-agent): preserve pasted text during editor restoration (#10146)
Closes #9809
2026-11-04 11:20:58 +01:00
Gangqiang Yao
8a3e163b0d
fix(coding-agent): prevent prompt and tree navigation overlap (#9155)
Closes #9154
2026-11-04 11:18:25 +01:00
jingtao
4b97777378
fix(ai): mark Claude Sonnet 5 as not supporting temperature (#7961) 2026-11-04 11:16:16 +01:00
Julien Chaumond
592667dffd
docs(coding-agent): llama.cpp tweaks (#11624)
use llama.cpp's default port and remove the stale `--jinja` note

same change as #11616 by @pcuenca with the original commit preserved

disclaimer: my julien-cto agent helped me write this

Co-authored-by: Pedro Cuenca <pedro@huggingface.co>
2026-11-04 11:07:48 +01:00
Alexey Zaytsev
aceb1bd128
feat(coding-agent): upgrade Mermaid terminal rendering (#8158)
Replaces grok-mermaid with lovely-mermaid 0.3.3. Closes #8157
2026-11-04 11:06:51 +01:00
Pier Carlo Cadoppi
e6865ec7e1
fix(coding-agent): realpath extension entries before jiti import (#8112)
Closes #8092
2026-11-04 11:04:24 +01:00
Mario Zechner
378d45a05b feat(chord): telemetry, spans across hosts (slice 9)
Chord's telemetry schema; spans for calls, methods, relays, holds, activations, natives, loads, reloads, resolves,
revocations, routines, jobs and HTTP requests; trace carriers on hold and call (bounded, printable ASCII) continued by
relays and workers; events and host-wide metrics; the app kit passes an adapter to its hosts. renderTrace escapes
control characters. Example 62 prints a trace across three hosts.
2026-11-04 10:02:48 +00:00
Giannis Papakostas
ee962fb4db
fix(plan-mode): make progress tracking robust and tolerant (#7950)
## What

Hardens the plan-mode example extension's step-progress tracking.
Previously, completion markers were only recognized in the exact
`[DONE:n]` form inside visible text blocks, so steps frequently never
got checked off during execution.

## Changes

- **`getTextContent`** now reads `thinking` blocks as well as text
blocks, so `[DONE:n]` tags emitted during reasoning are honored.
- **Marker tolerance** (`extractDoneSteps`): accepts `[DONE: 1]`, `[DONE
1]`, `Done: n`, and prose phrasings like "Step n done / is complete /
finished" — all adjacency-anchored so negations ("Step 9 is not done")
never match.
- **Whole-plan fallback** (`allStepsComplete`): if the run's final
message says the plan is done (e.g. "All steps complete"), all remaining
steps are marked at `agent_end`.
- **Plan extraction dedup**: only the last `Plan:` section is extracted,
so a plan drafted in a thinking block no longer duplicates the steps
from the text block.
- **No more truncation**: step text is kept in full (previously cut at
50 chars); the widget truncates for display only. The execution prompt
now shows the complete step text.
- **No more dropped steps**: steps whose text starts with a backtick
(e.g. `1. \`index.ts\` — …`) are no longer skipped.
- **Widget refresh** happens on every turn instead of only when a marker
matched.
- **Plan-complete card** is delivered with `nextTurn` instead of
triggering an extra LLM turn.

## Tests

`packages/coding-agent/test/plan-mode-utils.test.ts` updated for the new
behavior (truncation, backtick steps, marker-count semantics) and
extended with coverage for the new cases (marker variants, prose
phrasings, negation safety, thinking-draft dedup, markdown titles,
whole-plan completion).

All 43 tests in `plan-mode-utils.test.ts` +
`plan-mode-extension.test.ts` pass (`npx vitest run`).

Note: the repo-wide type-check currently fails on `main` with
pre-existing `ModelCatalog`/`never` errors unrelated to this change;
this branch adds none (same count with and without the diff).
2026-11-04 10:53:52 +01:00
robopipi[bot]
c13f6acfbe
Highlight .dart, .nix, and .groovy files in read/write views (#11790)
The `read` and `write` tool views find a file's language from its
extension with `getLanguageFromPath()`. That function's extension table
had no entries for Dart, Nix, or Groovy. Their highlight.js grammars are
loaded at startup, but the lookup returned `undefined`, so these files
were shown as plain uncolored text.

This change adds `dart → dart`, `nix → nix`, `groovy → groovy`, and
`gradle → groovy` to the table in
`packages/coding-agent/src/modes/interactive/theme/theme.ts`.

HTML export keeps its own separate extension map and a vendored
highlight bundle without a Dart grammar. This change does not touch
either.

## Testing

- Added a test to `test/syntax-highlight.test.ts` that checks the new
extension mappings. It fails without the fix and passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/syntax-highlight.test.ts` passes.
- Before, `getLanguageFromPath("a.dart")` (and `.nix`, `.groovy`,
`build.gradle`) returned `undefined` from the built dist. After, they
return `dart`, `nix`, `groovy`, and `groovy`.
- `npm run check` reports 6 type errors in `packages/ai/test` (model ID
types). Main without this change has the same 6 errors.

Fixes #11643

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 10:45:55 +01:00
robopipi[bot]
c5d3978350
fix(ai): send session-id instead of session_id on OpenAI Responses (#11793)
The `openai-responses` client sent the session-affinity header as
`session_id`. Proxies such as nginx with `underscores_in_headers off`
silently drop headers with underscores, and Envoy can be configured to
reject them. Codex now sends `session-id`, and the
`openai-codex-responses` path already followed it (#4967).

With the default `openai` session-affinity format, `openai-responses`
now sends `session-id`. `x-client-request-id` and `prompt_cache_key` are
unchanged. The `openai-nosession` and `openrouter` formats behave as
before.

The compat schema description for `sessionAffinityFormat`
(`openai-responses`) and `docs/models-configuration.md` are updated to
match.

Not changed: `openai-completions` still sends `session_id`. I did not
check whether xAI or other backends on the Responses API depend on the
underscored name. Their `prompt_cache_key` is still sent, so prompt
caching should not depend on the header.

Testing:
- `test/openai-responses-compat.test.ts` now captures `session-id`. It
also checks that `session_id` is no longer sent, and the
explicit-override test uses the new name. `test/xai-responses.test.ts`
expects `session-id` and no `session_id`.
- Without the source change, these tests fail (3 failures); with it,
both files pass (50 tests).
- `npm run check` passes.
- The full `packages/ai` suite passes, except
`generate-models-strict.test.ts`. That failure is unrelated: the test
depends on live model catalog data.

Fixes #11639

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 10:28:59 +01:00
梧桐雨
a63c4ec5c8
fix(coding-agent): clear delivered image-only queue entries (#8612)
## Summary

- remove delivered queue entries even when the user message has no text
- keep image-only steering and follow-up pending counts in sync with the
agent queue
- cover image-only steering delivery with a regression test

## Testing

- `npm run check`
- `./test.sh`

Fixes #8581
2026-11-04 10:24:43 +01:00
Sai Mallikarjun
f3a64af07b
fix(ai): preserve aborted stop reason during lazy setup (#8635)
Fixes #8409

## Summary
- Pass the request abort signal through lazy stream setup wrappers.
- Report setup failures as aborted when the request signal is already
aborted.
- Added a regression test for aborting during a tool execution before
next auth setup.
2026-11-04 10:23:35 +01:00
Gangqiang Yao
7a53c310a3
fix(coding-agent): settle tool results before disposal (#9126)
Fixes #9124.

Disposing a runtime during tool execution disconnects persistence before
the interrupted turn settles, leaving a stored assistant tool call
without its result. Await `session.abort()` before shutdown and disposal
so the interrupted tool result and final assistant message are
persisted.

This matches `teardownCurrent()`. `AgentSession.dispose()` remains
synchronous. Like the existing replacement path, the wait has no
timeout; handling tools that ignore cancellation is outside this change.

Related: #9986 concerns results for tool calls skipped during a
mid-session abort. This fix preserves the result of an already-running
tool during runtime shutdown.

Validation on main `f1b2e77f5`:

- The regression fails without the fix and passes with it, checking the
durable message order and `Operation aborted` result immediately after
`dispose()` returns, before awaiting the prompt.
- `npm run check`, offline build, and `./test.sh` pass.
2026-11-04 10:21:16 +01:00
Sviatoslav Abakumov
2cfab7c269
fix(coding-agent): render session tree search cursor (#9157)
This PR adds the cursor block to the **Type to search** input in
**Session Tree**. This is similar to the search input in **Resume
Session**.

Before:

<img width="1120" height="778" alt="Screenshot 2026-09-04 at 19 25 30"
src="https://github.com/user-attachments/assets/8f2e0ba9-8476-452b-a6be-1ad9b8d7d6c8"
/>

After:

<img width="1120" height="778" alt="Screenshot 2026-09-04 at 19 25 50"
src="https://github.com/user-attachments/assets/7f6ffcbf-db5a-4732-84fe-6ee0544116fa"
/>
2026-11-04 10:19:38 +01:00
Gangqiang Yao
c3b938e3e9
fix(coding-agent): reject reload while running or compacting (#9222)
Fixes #9221.

An extension tool can lose its context when an RPC command or SDK caller
reloads the session during tool execution: the tool awaits work, reload
invalidates its runner, then `ctx.getSystemPrompt()` throws a
stale-context error. The failed tool result is stored in session
history.

Reject reload when `isStreaming` or `isCompacting` is already set,
before shutdown or runner invalidation. This gives RPC and direct
callers the same entry checks as the TUI. The production change remains
seven lines.

#9548 removed the wrapper access behind the original reproduction. The
regression exercises the tool's own context access after an await,
through both an RPC-style extension command and direct
`AgentSession.reload()`. Rejection preserves the successful result in
session history and the next provider request. Compaction, tree
navigation, and idle reload are also covered.

Scope: this guards operations already marked running or compacting. It
does not serialize reload with prompt preflight or prevent a new prompt
from starting during an ongoing reload.

Related: #5735 proposes a broader deferred-reload API. This patch only
adds the existing TUI entry checks to the core reload method.

Validation on main `f1b2e77f5`:

- Without the guard, four regression cases fail and idle reload passes.
With it, all five pass.
- `npm run check`, offline build, and `./test.sh` pass.
2026-11-04 10:18:59 +01:00
Danila Poyarkov
dca1f55527
fix(coding-agent): preserve indentation in rendered diffs (#9274)
## Problem

The edit tool's intra-line renderer can drop indentation from a removed
line when text is inserted before otherwise unchanged content.

For example, this input:

```diff
-83   await commitRecordingChunk({
+83   const result = await commitRecordingChunk({
```

was rendered as:

```diff
-83 await commitRecordingChunk({
+83   const result = await commitRecordingChunk({
```

`diffWords()` groups the shared leading spaces into the added segment
(`" const result = "`). The existing highlighting logic moved those
spaces outside inverse styling, but only appended them to the added
line.

## Solution

Extract whitespace shared by both lines before calling `diffWords()`,
then initialize both rendered lines with that prefix. This preserves
indentation without changing the existing word-level highlighting
behavior. The existing handling for whitespace that belongs to only one
side remains unchanged.

Added regression coverage for:

- insertion before indented content;
- replacement within indented content;
- keeping shared indentation outside inverse highlighting.

## Validation

- `npm run check`
- `./test.sh`
- targeted `packages/coding-agent/test/diff.test.ts` (3 tests)
2026-11-04 10:17:34 +01:00
Gangqiang Yao
66c6556771
fix(coding-agent): track discarded user bash output (#10165)
Closes #10164.

A user `!` command can discard earlier chunks but report `truncated:
false` when the retained tail fits within the output limits. The model
then receives no truncation notice or full-log path.

Track discarded chunks and include that flag in the executor's result
for both completion and cancellation. This applies to the current shared
finalization path and preserves streaming ANSI handling. Output limits
and message format stay unchanged.

Related: #2852. Here the complete log is already saved; the incorrect
flag hides its path from the model.

Validation on main `f1b2e77f5`:

- Two deterministic 64 KiB + 44 KiB cases fail without the fix. All
three regressions pass with it, checking completion, cancellation, the
complete log, model-facing path, and short-output behavior.
- Regressions are in `test/bash-executor-truncation.test.ts`; that file
and `test/tools.test.ts` pass all 88 tests.
- `npm run check`, offline build, and `./test.sh` pass.
2026-11-04 10:16:53 +01:00
Christian Tietze
3b57ed7e27
fix: limit selector window height (#11072)
With this PR, we stop  rendering every option.
Instead, we render a centered, clamped window of up to 10 options and
show `(selection/total)` when the list overflows.

This keeps the selected option visible in long extension-provided lists
without changing navigation, keybindings, callbacks, countdowns, or
short-list rendering.

Fixes #6688.

## Merge Danger

**Door:** Two-way

The change is localized to `ExtensionSelectorComponent.updateList()` and
can be reverted without migration or persisted-state concerns.

**Blast Radius:** Low

Only extension selectors with more than 10 options change appearance;
they now render a window and position indicator instead of every option.

---------

Co-authored-by: Pi via openai-codex (gpt-5.6-sol) <noreply@openai-codex>
2026-11-04 10:15:27 +01:00
robopipi[bot]
f6278a472e
fix(mcp): read large stdio messages in linear time (#11791)
`JsonRpcLineReader.readStdout` (used by the stdio transport and by
`PipeTransport`) appended every stdout chunk to the buffered bytes with
`Buffer.concat`, then searched the whole buffer from the start for a
newline. A large message arriving in many chunks was therefore copied
and scanned again on every chunk. A 12 MiB tool result in 64 KiB chunks
took about 350 ms just to frame.

Now the reader keeps the bytes of an incomplete message as a list of
chunks, with a running byte count, and searches only the new chunk for a
newline. When the newline arrives, the chunks are joined once. A line
that fits inside a single chunk is sliced directly. The rest is
unchanged: the `maxMessageBytes` limit (now checked against the running
count), CRLF and blank-line handling, errors for invalid JSON and
oversized messages, and the incomplete-message error in `end()`.

Testing:
- New `packages/mcp/test/lines.test.ts`:
- a 1 MiB message fed in 4 KiB chunks calls `Buffer.concat` once (fails
on the old code);
- a mixed input of multi-byte UTF-8, CRLF, blank lines, an invalid line
and several messages gives the same results at every chunk size from 1
byte to the whole input;
  - reading recovers after an oversized message;
  - a message without its newline is reported at the end.
- `test/lines.test.ts`, `test/stdio.test.ts` and `test/pipe.test.ts`
pass. `npm run check` passes.
- Micro-benchmark, one message fed in 64 KiB chunks:
  - before: 1 MiB 5.4 ms, 4 MiB 49.9 ms, 12 MiB 348 ms;
  - after: 2.2 ms, 7.9 ms, 21.6 ms.

Fixes #11761

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 10:09:23 +01:00
Mario Zechner
f4427f8a3e docs(chord): the tutorial uses tx.update with its integer key now that int keys type-check 2026-11-04 09:07:34 +00:00
robopipi[bot]
9bca21a0b8
fix(coding-agent): update minimatch to 11.0.3 for GHSA-g3hv-75wg-m249 (#11789)
`@earendil-works/pi-coding-agent` pins `minimatch@10.2.6`.
GHSA-g3hv-75wg-m249 (moderate, a ReDoS via globparts amplification)
covers every minimatch version below 11.0.1, and there is no patched
10.x release. As a result, `npm audit` fails in every project that
installs pi-coding-agent.

This change pins `minimatch` to `11.0.3` and regenerates
`package-lock.json` and `packages/coding-agent/install-lock/` with
`scripts/generate-coding-agent-install-lock.mjs`. minimatch 11 adds one
new transitive dependency, `@isaacs/nonrecursive-glob-match@1.0.3`,
which has no install scripts. It also moves brace-expansion to ^5.0.12.
pi uses minimatch in only two places, `model-resolver.ts` and
`package-manager.ts`. Both use the named `minimatch` export, which 11.x
keeps unchanged.

No new test was added: the change only bumps a dependency, and existing
tests cover both places that use it.

Testing:
- `npm run build` and `npm run check` pass, and the install-lock check
reports it is up to date.
- `test/model-resolver.test.ts`, `test/package-manager.test.ts` and
`test/package-manager-ssh.test.ts` pass (213 tests).
- Before: installing 1.4.0 and running `npm audit` reports `minimatch
<11.0.1 … GHSA-g3hv-75wg-m249`.
- After: `npm audit --package-lock-only` in
`packages/coding-agent/install-lock` reports `found 0 vulnerabilities`.
- At the monorepo root, `npm audit` still flags a `minimatch@3.1.5` that
comes in through the `shx` dev dependency. pi does not ship that copy,
and this change does not touch it.

Fixes #11646

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 10:05:21 +01:00
robopipi[bot]
b0c64f33c8
fix(coding-agent): load user keybindings in pi config (#11788)
`pi config` ignored `~/.pi/agent/keybindings.json`. `selectConfig()`
never called `setKeybindings()`, so the config selector's
`getKeybindings()` fell back to the default TUI bindings. Remaps such as
`"tui.select.down": ["down", "ctrl+n"]` did nothing there, although they
work in `/settings` and the `--resume` picker.

`selectConfig()` now installs
`KeybindingsManager.create(options.agentDir)` before it builds the UI,
the same way the session picker and startup UI do.

**Testing**
- Added `test/config-selector-keybindings.test.ts`. It runs
`selectConfig()` with a stubbed terminal and component and an agent dir
whose `keybindings.json` remaps `tui.select.up` and `tui.select.down`,
then checks that the active keybindings include `ctrl+p` and `ctrl+n`.
It fails without the change and passes with it.
- Manual check: ran `pi config` in tmux with that `keybindings.json`.
Before the change, Ctrl+N did nothing. After it, Ctrl+N moves the
selection like Down.
- `npm run check` reports only two errors, both missing modules in
`packages/schema/test/bench/candidates.ts`. They occur on `main` without
this change.

Fixes #11651

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:59:49 +01:00
robopipi[bot]
081a6e87f6
fix(coding-agent): share the auth.json lock across symlinked paths (#11787)
`FileAuthStorageBackend` locked `auth.json` with proper-lockfile and
`realpath: false`, so the lock directory was `<path as given>.lock`.
Reads and writes follow symlinks to the same real file, but each path
alias got its own lock. For example, an agent dir whose `auth.json` is a
symlink to a shared file used a different lock than the real file. Two
pi processes could then enter the credential-modification section at the
same time and refresh the same OAuth token concurrently.

This change drops `realpath: false` from the sync and async lock calls,
so proper-lockfile resolves the path and every alias of the file uses
one lock. Resolving the path needs the file to exist, which is already
the case: `ensureFileExists()` runs before either lock is taken.

The same `realpath: false` pattern is also in `settings-manager.ts`,
`trust-manager.ts`, and `package-manager-cli.ts`. This change leaves
those alone.

## Testing

- I added a test to `test/auth-storage.test.ts`. It holds the lock on
the real `auth.json`, then checks that a backend opened through a
symlink alias waits in `withLockAsync` until aborted, and gets `ELOCKED`
from `withLock`. Neither path runs the mutation. Without the fix the
test fails because the async call enters the lock; with the fix it
passes.
- `node ../../node_modules/vitest/dist/cli.js --run test/auth-storage
test/settings`: 151 tests pass.
- `npm run check` passes.
- With the reproduction script from the issue, current main prints
`symlink alias: ENTERED`; with this change it prints `symlink alias:
BLOCKED`. The same-path control prints `BLOCKED` in both cases.

Fixes #11655

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
Co-authored-by: Mario Zechner <contact@badlogicgames.com>
2026-11-04 09:57:47 +01:00
robopipi[bot]
d53980fe11
fix(ai): add glm-5.3 to qwen-token-plan-individual models (#11785)
`qwen-token-plan-individual` filters the models.dev `alibaba-token-plan`
catalog through a fixed list of model IDs. That list still stopped at
`glm-5.2`. models.dev now has `glm-5.3`, and `qwen-token-plan` and
`qwen-token-plan-cn` already list it. Individual did not, because
`glm-5.3` was not on its list.

This change adds `glm-5.3` to the Individual list and updates the
"verified" date in the comment above it.

Tests in `packages/ai/test/qwen-token-plan-models.test.ts`:
- `glm-5.3` is added to the expected list for the exact-list test.
- A new test checks that Individual's `glm-5.3` uses the international
Token Plan base URL. It also checks the thinking levels models.dev gives
for it: low, high and max.

How I tested it:
- `npm run generate-models`, then `test/qwen-token-plan-models.test.ts`:
all 90 tests pass. Without the change to the list, 3 tests fail.
- `npm run check` passes.
- Before: `pi --list-models qwen-token-plan-individual` listed `glm-5.2`
but not `glm-5.3`. After: it lists both.

Fixes #11658

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:54:48 +01:00
robopipi[bot]
4c992154d9
fix(ai): keep the underlying cause in connection error messages (#11783)
When a request fails before the provider answers (connection refused or
reset, DNS, TLS or certificate, proxy), the `openai` and Anthropic SDKs
throw an error with the generic message `"Connection error."`. The
actual reason is in the `cause` chain, usually two levels down: undici's
`TypeError: fetch failed` wraps `connect ECONNREFUSED 127.0.0.1:8080`.
pi built `errorMessage` from `error.message` only, so users, print mode,
and session files saw only "Connection error." and had nothing to go on.

This change adds `withCauseDetail()` in `utils/error-body.ts`:
- It walks the `cause` chain, up to 5 levels, and appends the deepest
message, plus its `code` if the message doesn't already contain it. For
example: `Connection error.: connect ECONNREFUSED 127.0.0.1:8080`.
- `normalizeProviderError` applies it only when the error has no HTTP
status and no body. That covers every adapter that goes through
`formatProviderError`.
- The Anthropic adapter's catch block also uses it, since it read
`error.message` directly.

Errors without a cause come out unchanged. Retry classification still
matches, because the message still contains "Connection error".

**Testing**
- Added tests to `packages/ai/test/error-body.test.ts`:
  - a synthetic nested cause chain;
  - adding the code when the message lacks it;
  - no cause leaves the message unchanged;
- a real `openai` SDK call against a closed local port, which now gives
`Connection error.: connect ECONNREFUSED 127.0.0.1:<port>`.
- Without the source change, the nested-chain test and the real SDK test
fail, because the message is just `Connection error.`.
- The full `packages/ai` test suite passes, and `npm run check` passes.

Fixes #11695

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:47:23 +01:00
robopipi[bot]
4cf69332c3
fix(coding-agent): resize images in queued steering and follow-up messages (#11782)
Images in idle prompts go through `processImage`: they are resized to
the model's limits, and a coordinate hint is added to the text. Images
in messages queued while the agent was busy skipped this step. That
covers `steer()`, `followUp()`, `prompt()` with `streamingBehavior`, and
RPC `steer`/`follow_up`. Those images were sent and saved at full size.
For example, a 1200x9000 PNG stayed 1200x9000, which is over Anthropic's
8000 px limit, and because it is saved in the session it was replayed on
every later request.

What the change does:
- `_queueSteer`/`_queueFollowUp` now mark queued user messages that
carry images.
- When such a message is delivered (`message_start`), `AgentSession`
normalizes its images in place with `_normalizePromptImages`, the same
code the idle prompt path uses, and appends the resize hints to its
text. This runs before extensions and listeners see the message, before
it is saved, and before it is added to the request context.
- Doing it at delivery keeps enqueueing synchronous. A slow resize
therefore cannot leave a message queued after the run has ended. I hit
exactly that when I first tried normalizing at enqueue time. Delivery is
also when the resize uses the model current at that moment.
- The queue display still matches and removes the message, because
matching runs before the text is changed.

Testing:
- New `test/agent-session-queued-images.test.ts`: a 1200x9000 PNG is
queued while the agent is streaming, through `steer()`, `followUp()` and
`prompt(..., { streamingBehavior: "followUp" })`. The test checks three
things: each saved message has a resized image and the "original
1200x9000, displayed at 267x2000" hint; no request sent to the model
contains the original image; and the queue is empty afterwards. It fails
without the change and passes with it.
- `npm run check` passes. The `agent-session*` tests, the queue- and
image-related tests and `test/suite` pass.
- Before the change, the saved steer and follow-up messages had
`image/png 1200x9000` while the idle prompt had a resized JPEG. After
it, all three are resized.

Fixes #11738

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:36:54 +01:00
robopipi[bot]
b317575fd1
fix(ai): use Azure Foundry 1.05M context window for GPT-6 models (#11781)
Azure's OpenAI models are copied from the direct `openai` provider. That
provider caps the GPT-6 family at 272K so requests stay in OpenAI's
short-context pricing tier.

The Azure provider overrides this cap through
`CONTEXT_WINDOW_OVERRIDES`, but that list only covered gpt-5.4, gpt-5.5
and gpt-5.6. As a result, `azure/gpt-6-astra`, `gpt-6-luna`,
`gpt-6-sol`, `gpt-6.1-sol` and `gpt-6.1-terra` showed a 272K context
window, and pi compacted sessions far earlier than needed. Azure Foundry
deploys these models with a 1,050,000-token context window.

This change adds the five models to the Azure override list.
OpenAI-direct is unchanged.

Testing:
- Added a test in `packages/ai/test/supports-xhigh.test.ts` that checks
each of these Azure models has a 1,050,000 context window. It fails on
all five models without the override (5 failed) and passes with it.
- After regenerating the model data, the generated Azure catalog lists
all five models at 1050000. Before the change they were at 272000.
- `npm run check` passes.

Fixes #11742

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:34:35 +01:00
robopipi[bot]
37b38e9f65
fix(coding-agent): keep ctrl+v as a paste key on Windows and WSL (#11780)
On Windows and WSL, `app.clipboard.pasteImage` was bound only to
`alt+v`. Most Windows terminals handle Ctrl+V themselves and send a
bracketed paste. Some terminals have no Ctrl+V paste action and pass
Ctrl+V to the program as the raw `0x16` key instead; Tern's embedded
terminal is one. In those terminals nothing in pi was bound to `ctrl+v`,
so pressing it did nothing and showed no message.

This change makes the Windows/WSL default `["alt+v", "ctrl+v"]`. `alt+v`
keeps working as before. In terminals that handle Ctrl+V themselves, pi
never receives the key, so the extra binding has no effect there. No
other default binding uses `ctrl+v`. `docs/keybindings.md` is updated to
match.

Testing:
- Added a test to `test/keybindings.test.ts`. It sets `WSL_DISTRO_NAME`,
re-imports the keybindings module, and checks that the paste defaults
are `["alt+v", "ctrl+v"]`. It fails without the change and passes with
it.
- Updated the existing defaults assertion in the same file to match the
new value.
- `node ../../node_modules/vitest/dist/cli.js --run
test/keybindings.test.ts`: 6 passed.
- `npm run check`: passes.

Fixes #11765

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:33:31 +01:00
robopipi[bot]
ea0f49a365
fix(coding-agent): name the llama.cpp server and cause when it cannot be reached (#11779)
When `/login llama.cpp` could not connect to the server URL, pi showed
only `Error: Failed to save API key for llama.cpp: fetch failed`. That
happens on a refused connection, a wrong port, or `https://` against a
plain-HTTP server. Node's `fetch` keeps the real reason in
`error.cause`, and pi dropped it, so the user had nothing to go on.

`LlamaClient` now sends its requests, including the `/models/sse` watch,
through a small `fetchServer` helper. When `fetch` fails at the network
level, the helper throws `Could not reach llama.cpp server at <url>:
<cause>` and keeps the original error as `cause`. Aborts and timeouts
are rethrown unchanged. HTTP error responses are handled as before.

The same message also improves `/llama` and the model catalog refresh,
because they use the same client.

**Testing**
- Added a test in `test/llama-extension.test.ts` that calls
`LlamaClient.list()` against a closed local port and expects the server
URL and `connect ECONNREFUSED ...` in the error. It fails without the
change (`got 'fetch failed'`) and passes with it. The whole file passes
(16 tests).
- `npm run check` passes.
- Manual check, `/login llama.cpp` with `http://127.0.0.1:8099` (nothing
listening):
  - Before: `Error: Failed to save API key for llama.cpp: fetch failed`
- After: `Error: Failed to save API key for llama.cpp: Could not reach
llama.cpp server at http://127.0.0.1:8099: connect ECONNREFUSED
127.0.0.1:8099`

Fixes #11745

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:32:51 +01:00