Follow-up to #11440 for the latest comment on #11331.
Problem: the ranker scored a tool as one bag of words. A query term in
the tool's name counted like a mention in its description, and the
server's instructions were copied into every tool's text, which
lengthened it and lowered its score. Models often add words to a tool
name, so `peekaboo app` ranked `mcp__peekaboo__app` 14th: other peekaboo
tools mention "app" in shorter descriptions, and its own description
says "applications".
Fix: `Bm25Ranker` scores three fields separately:
- name: a query term in the name counts 3x its IDF; the description part
of BM25 is at most 2.2x, so a name match beats any number of description
mentions;
- description and schema text: BM25 as before, length-normalized on this
field only;
- namespace name, description, and instructions: 0.5x IDF per matching
term, without length normalization.
A tool whose whole local name (last `__` segment) is in the query gets a
bonus worth one more match of every query term, so it mostly wins
(`peekaboo app`, `describe app`, `read thread_read state`). A query that
is exactly a tool's name still ranks it first (#11440). Applies to
`tool_search` and codemode `searchTools()`.
Not included: the comment's second point (loading a deferred tool on a
direct call instead of `Tool X not found`). That error comes from
`prepareToolCall` in `packages/agent`, which has no hook for unknown
tools; it needs an agent-loop API change.
Fixes#11592.
## Problem
WSL starts Windows programs in the current directory, translated to
`\\wsl.localhost\<distro>\<cwd>`. When that path exceeds MAX_PATH, every
Windows program fails to start with `Invalid argument`. pi inherits its
cwd when spawning `powershell.exe`, so in a deep directory:
- clipboard image paste returns `null` (the PowerShell backend fails,
stderr is discarded)
- copy via `powershell.exe` (WSL without WSLg) fails
- opening the browser for sign-in fails (`xdg-open` on WSL runs
`explorer.exe`)
Reproduced on WSL (FedoraLinux-44): `powershell.exe` and `cmd.exe` work
from a 225-char cwd and fail from 230. With an image on the Windows
clipboard, `readClipboardImage()` returns the PNG from `/tmp` and `null`
from a 245-char cwd; with this change it returns the PNG from both.
`LongPathsEnabled=1` does not help.
## Changes
- `runClipboardCommand` and `openBrowser` run from `/` on POSIX. These
tools never use the cwd (temp files are passed by absolute path), and
`/` is always the shortest UNC path. `tmpdir()` was not used because
`$TMPDIR` can itself be deep.
- The external editor keeps pi's cwd on purpose, so project-relative
paths and integrated terminals keep working. A Windows editor such as
`code` can still fail from a deep WSL directory, as with git's
`core.editor`. A comment documents this.
- External editor failures were silent: the UI redrew over the error
immediately. pi now shows `Failed to launch external editor: <cmd>`,
`External editor exited with code N; prompt unchanged`, or the signal
name. This shows in the status line for the main editor and inline in
the extension editor dialog.
## Testing
- New tests: clipboard commands run from `/` (POSIX); an editor that
cannot be launched is reported (POSIX); the existing failure test checks
the message.
- Verified on WSL by running the real modules: clipboard image paste
from a deep cwd, and editor success, exit code 1, launch failure, and
SIGTERM.
- On Windows, the 3 existing `external-editor.test.ts` tests already
fail on `main`: the editor command is split on spaces, so `C:\Program
Files\nodejs\node.exe` breaks. Not addressed here.
This PR is AI-generated.
Fixes#7756.
Problem: `detectInstallMethod()` classifies any path containing `/pnpm/`
as pnpm. An install that only lives under `PNPM_HOME`, for example a
wrapper under `$PNPM_HOME/global-nub/5/node_modules/...`, is detected as
pnpm. `pi update` then correctly refuses to self-update (the path is not
under `pnpm root -g`), but `getSelfUpdateUnavailableInstruction()`
printed:
```
This installation is not managed by a global pnpm install. Update it with the package manager, wrapper, or source checkout that provides it.
```
Fix: name the package manager only when it owns the install (managed but
not writable). All other cases fall through to the neutral message:
```
Update @earendil-works/pi-coding-agent using the package manager, wrapper, or source checkout that provides this installation.
```
`detectInstallMethod()` is unchanged: every user-facing path already
goes through the ownership check, and returning `unknown` would trip the
Windows "only supported for npm and pnpm" guard with a worse message.
The update banner already says `Run pi update` and no longer suggests
`pnpm install -g`.
The pi.dev installer is not affected: it verifies npm ownership with
`npm ls -g` and never infers pnpm from paths.
Tests: regression test in `test/config.test.ts` (fails without the fix).
`npm run check` passes.
This PR is AI-generated.
Plugin discovery, esbuild builds (optional peer dependency), vm evaluation with
pluginFile and mapStack, dependency installs, a watcher with dirty marks and
rollout across hosts; host.load and host.reload per plugin with the closure rule;
examples 35-39 as folders with real plugin directories.
Closes#11532.
The docs and CLI help did not say which working directory a resumed
session runs in. That directory controls tools, `@path` references,
project configuration, and context files.
## Changes
- `docs/sessions.md`: states that a session keeps the working directory
it was created in, adds a table mapping each entry point to its working
directory, and describes what happens when that directory no longer
exists. Removes a paragraph that repeated the `--session`/`--fork`
descriptions.
- `docs/cli.md` and `--help`: clearer `--continue`, `--resume`,
`--session`, `--session-id`, and `--fork` descriptions. The help only
mentions the directory where it is not obvious (`--fork`).
- `/resume` and `/import` command descriptions, mirrored in
`docs/slash-commands.md`. `/import` notes that it preserves the session
ID and working directory.
| Entry point | Working directory |
|---|---|
| `--continue` | The current directory |
| `--resume`, `/resume`, `--session`, `/import` | The session's working
directory, which can differ from the current directory |
| `--fork`, or a `--session` ID from another directory that you choose
to fork | The current directory, in a new session |
Behavior was verified against `createSessionManager` in `src/main.ts`,
`SessionManager.open`/`continueRecent`/`forkFrom`, and
`AgentSessionRuntime.importFromJsonl`.
Adds a `beforeProviderRequest` generation hook so extensions can replace
the provider request body before it is sent. It is the durable
counterpart of coding-agent's `before_provider_request`.
Durable calls `streamSimple()`, so apps can't set provider-specific
fields such as Anthropic's `thinking.display: "updates"` (#10804)
without wrapping `Models`. Now it's an extension:
```ts
hook(GenerationTask, {
beforeProviderRequest: ({ model, payload }) => {
if (model.api !== "anthropic-messages") return undefined;
// change payload
return { payload };
},
})
```
The owner's audit decisions (decisions.md, "Simplification audit"): cut the reload-only
installing state (staging is synchronous) and the static registry cycle edges (the
runtime guard stays); retry propagation by a visited set and a hop budget (retry.hops)
instead of operation ids and through lists; a pure swap sends holds a fresh bound; one
rule after a live failure (every member followed until the admitted commits published);
parked_long for any long park, naming the first blocker in declaration order with its
state; one seal token per attempt. Keys follow the old instance during a restart's drain
like members. src/next: about 255 code lines fewer.
In pi-durable, a Code Mode script that called the built-in `read` on an
image got `""` back, so `image(await tools.read({ path: "pixel.png" }))`
threw a TypeError. `read` declared no `structuredOutputSchema`. Code
Mode resolves a schema-less tool with only its text, and drops images on
purpose. `read`'s output for an image is a single image block with no
text, so the script got an empty string. The coding agent fixed the same
problem for #10251.
With this change, `read` declares a `structuredOutputSchema`: a string,
or an image block `{ type: "image", data, mimeType }`. It returns the
matching `structuredOutput`:
- for a text file, its text, so scripts get the same value as before;
- for an image, its `ImageContent`, which `image()` accepts.
The "Read image file" note stays an info diagnostic, as before. The
README section on structured output and the changelog are updated.
**Testing**
- New test in `test/codemode.test.ts`. A script reads a real PNG and a
text file through `tools.read()`, checks the block's type, MIME type,
and data, and passes the block to `image()`. It checks that the script
succeeds, the image is saved and shown, and the text read still returns
its text. The test fails without the change: the script errors because
`image()` gets `""`.
- `test/tools-read-differential.test.ts`: the reference implementation
now includes the new `structuredOutput` field.
- `npm run check`.
- `test/codemode.test.ts`, `codemode-describe`, `tools`, `tools-images`,
`tools-read-differential`, `harness-tools`, `harness-hook-reads`,
`harness-read-only`, and `spec-usage` all pass.
Fixes#11015
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
`sanitizeOutput()` in `@earendil-works/chord/output` deleted the ESC
byte (0x1b) along with the other control characters, but it left the
rest of each escape sequence in place. Colored tool output such as
`\x1b[31mERROR\x1b[0m` came back from `OutputBuffer.snapshot()` as
`[31mERROR[0m`. Because the ESC byte was already gone, later ANSI
stripping could no longer recognize the leftover fragments.
`sanitizeOutput()` now removes whole 7-bit escape sequences before it
removes single control characters. It handles:
- CSI sequences (colors, cursor and erase commands)
- string sequences (OSC, DCS, APC, PM, SOS) terminated by BEL or `ESC \`
- short `ESC x` forms such as `ESC ( B` or `ESC 7`
`snapshot()` sanitizes the concatenated retained text, so a sequence
split across pushed chunks is also removed.
**Testing**
- Added a regression test in `packages/chord/test/output.test.ts`. It
covers SGR colors, cursor and erase CSI, charset and short escapes, OSC
8 hyperlinks and an OSC title, plus an `OutputBuffer` fed a color
sequence split across three chunks. The test fails without the change
and passes with it.
- `test/output.test.ts` and `test/output-skip.test.ts` pass.
- Biome is clean. The only `tsc` errors in `npm run check` are in
`packages/ai` tests and are unrelated to this change.
- The durable package test results are the same with and without the
change: 71 test files fail on both, and every individual test that runs
passes.
- Reproduction from the issue: before the change both lines printed
`"[31mERROR[0m\n"`; after it, both print `"ERROR\n"`.
Fixes#11604
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
After a rescan, the env daemon's file watcher sent every changed path in
a single `change` event. When a big batch of changes made that event
larger than the 16 MiB frame limit, `write_frame` rejected it. The
daemon's only output writer then shut down. The daemon process kept
running but sent no more replies or pings. Requests hung until the
client's 30 s silence limit dropped the connection and reconnected.
Now the watcher builds the event with `change_event`. If the serialized
path list would not fit in a frame (`MAX_PAYLOAD`), it sends the
existing `{ kind: "change", overflow: true }` event in its place. The
client already maps that to an `overflow` file change, so the wire
format does not change and the connection keeps working.
Testing:
- Added Rust unit tests in `watch.rs`. One checks that a small path list
is sent as `paths`. The other checks that a list larger than
`MAX_PAYLOAD` becomes `overflow`.
- `cargo fmt --check`, `cargo clippy --locked --all-targets -- -D
warnings` and `cargo test --locked` pass in `packages/env/daemon`.
- The `packages/env` vitest suite passes.
- Reproduced with the real daemon: I watched a directory and renamed in
a directory of 6,000 files with long paths, about 20 MB of path JSON.
Before the change, `fileInfo` requests timed out from the flush until
the client reconnected about 30 s later. After the change, the watcher
reports `overflow` right away and `fileInfo` keeps working. With 3,000
files, which fits in a frame, the paths are still reported as before.
Fixes#10516
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
A Session no longer holds its line while a commit is stored. Commits that
arrive while one storage call is in flight go out together in the next call,
each with its own seq and outcome; a job runs until it touches a key an
unpublished commit wrote, then waits for that commit's publication. Outcomes
are delivered in line order; publication is in line order, one commit per
macrotask. The scheduler's decisions run as regions that record the keys they
read and rerun after a wait, with side effects only after a clean check.
Storage.commit(batches, receive, context) replaces commit(writes, adopt,
context): prefix semantics (Durable's own checks refuse a batch and keep the
ones before it; any other failure rejects the call), receipts per stored
batch, optional publishing() scope. Memory, SQLite, JSONL, IndexedDB and
StoreStorage implement it natively; StoreStorage through the new
ChordStore.commitSeries (one store transaction per call), replacing
commitInBatch.
A normal run makes no storage reads: bounded record cache, conversation
tails, task-document index, kept context ranges. A tool slot's running status
and a generation's attempt are stored with the next commit (12 instead of 14
commits per run).
One Harness with 128 conversations over storage with a 4 ms round trip: about
5 -> 440 agent runs per second. Design: packages/durable/docs/group-commit.md.
oauth.clientRegistration defaults to auto: pi's Client ID Metadata Document
when the authorization server supports it for public clients, otherwise
dynamic client registration. Registered clients stay in use. pi mcp add
gains --oauth-client-registration.
closes#11426
host.reload(facets, ctx): staging into a staged table with a recording self (all or
nothing), one commit (types, registries, contributions; per-name wakes; installing
holds), and each object's fate: pure swap in place, native restart after its drain,
retry on the new code, or its end by its type's removal, with a report. Old calls
finish on old handles (R15 for documents and tables), routines hand over per object,
jobs keep their versions (accepts, blocked { version }, persisted runs per R3),
registries swap providers and contributions in one batch, holds are kept,
re-subscribed or unbound with failed { waiting } (model §5), accepts maps older
consumers' state, arguments, results and keyed members.
References are the only indirection (owner, 2026-10-29): a consumer's watches and
derives attach through the reference, which passes them straight to the object
instance's own document or derive, moves them to a new instance (one frame) and keeps
its last values while unbound; MemberSource, keyed-members and the per-version
hand-over are removed. Older consumers read through the instance's shared mapping
per version. Examples 33-34, reload benchmark.
## Summary
Treat `TERM_PROGRAM=Orca` as Kitty-image capable in `pi-tui`'s terminal
capability detection, so image components render inline instead of
falling back to text when running inside Orca.
- Recognize Orca as supporting Kitty inline images, true color, and OSC
8 hyperlinks.
- Preserve the existing tmux and screen precedence guards.
- Gate images on Orca's capability advertisement: require both
`TERM_PROGRAM=Orca` and Orca's `ORCA_IMAGE_PROTOCOL=kitty` signal before
enabling Kitty output, so existing Orca releases that do not yet
advertise image support safely keep the text fallback.
- Add a regression test and the package/CLI changesets.
## Related
- Requested by maintainer approval in
[earendil-works/pi#7357](https://github.com/earendil-works/pi/issues/7357)
(lgtm).
- Depends on the Orca-side capability advertisement, which has landed:
[stablyai/orca#19512](https://github.com/stablyai/orca/pull/19512)
(merged 2026-09-18, `09073086a8`) exports `ORCA_IMAGE_PROTOCOL=kitty` on
the daemon, local, and relay PTY paths, and forwards it across the WSL
boundary. Supersedes
[stablyai/orca#11706](https://github.com/stablyai/orca/pull/11706),
which was closed in favor of #19512.
## Testing
- `pnpm --filter @moonshot-ai/pi-tui test` — focused regression suite
passes.
- `pnpm --filter @moonshot-ai/pi-tui typecheck` — passes.
Fixes#10925.
## Problem
`splitIntoTokensWithAnsi()` attaches pending escape codes to the next
visible character. The code that closes a styled word therefore lands on
the following space: `**commodo** consequat` tokenizes as
`\x1b[1mcommodo`, `\x1b[22m `, `consequat`.
`wrapSingleLine()` detected whitespace with `token.trim() === ""`, which
is false for `"\x1b[22m "`. When the styled word ended exactly at the
wrap width, that space was treated as a word and moved to the next line:
```
before: ["aaaa \x1b[1mbold", "\x1b[1m\x1b[22m next", "word"] (width 9)
after: ["aaaa \x1b[1mbold\x1b[22m", "next word"]
```
Because the closing code moved to the next line, the style also stayed
open into the right padding of the wrapped line. Separately,
`getLineEndReset()` closed only underline at line end, so strikethrough
that spans a wrap continued into the padding.
## Fix
- Detect whitespace tokens after stripping escape codes.
- When a whitespace token is dropped at a wrap, append its escape codes
to the wrapped line and update the tracker, so the style closes there
and is not re-opened on the next line.
- Close strikethrough (`\x1b[29m`) at line end, like underline.
`getActiveCodes()` re-opens it on the next line. Background is
unaffected.
- Whitespace wider than an empty line that carries codes (e.g.
background spaces followed by a trailing reset) keeps only the codes.
Without this, treating it as whitespace skipped the long-token path, and
the trailing reset blocked the final `trimEnd()`, leaving a line wider
than `width`.
- When a long word follows, the line before it is now trimmed before the
line-end reset is added. Otherwise the reset (now also emitted for
strikethrough) blocks the final `trimEnd()` and leaves trailing spaces.
## Tests
- `wrap-ansi.test.ts`: boundary closing code, dropped space with a color
reset, strikethrough reset and restore across a wrap, strikethrough
reset with background preserved.
- `markdown.test.ts`: padded Markdown with a background at an exact
boundary for bold and strikethrough.
- The new tests fail without the fix. With the old code, rendering the
issue text through `UserMessageComponent` (dark theme, padding 1) at
widths 40-120 produced 5 bad lines at width 78. With the fix it produces
none.
- Fuzzing over 1.2M random styled inputs with SGR and OSC 8 codes found
no line wider than `width`, and no case where the old wrapper laid out
text the same as the unstyled input but the new one does not.
- Benchmark (`wrapTextWithAnsi`, width 77, about 5-18k chars, median of
6 processes) against `main`: plain -5%, light styling -0.5%, heavy
styling +1%, long strikethrough +0%, all within noise. The whitespace
check scans character codes and stops at the first visible text instead
of allocating a stripped copy.
- `npm run check` passes. All `packages/tui` tests pass. `./test.sh`
shows no new failures; the remaining failures are environment-specific:
the clipboard tests also fail on `main` here, and the durable, env
(ssh), and chord failures are in packages that do not depend on
`pi-tui`.
Fixes#10488
## Problem
On Windows, `c:\Users\me` and `C:\Users\me` are the same directory, but
pi compared paths as exact strings (`===`, `startsWith`, map and set
keys). `realpathSync` keeps the input casing, so canonicalizing does not
help.
The reported case, with cwd `c:\Users\me\project` and home
`C:\Users\me`:
1. Ancestor discovery finds `c:\Users\me\.agents\skills`. The exclusion
of the user dir `C:\Users\me\.agents\skills` does not match, so global
skills load as project skills.
2. The same dir loads again as user skills. Dedup does not match either,
so every skill is reported as a name collision.
3. `hasTrustRequiringProjectResources` treats `~/.agents/skills` as a
project resource that requires trust.
The same pattern affected other places:
- Saved trust decisions in `trust.json` were not found when cwd casing
differed, so pi asked for trust again.
- With a custom session dir, `--continue`, resume by id, and the session
list hid sessions recorded with different cwd casing.
- Git worktree `AGENTS.md` shadowing failed when cwd casing differed
from git's paths.
- Local package matching (`pi remove c:\pkg` vs configured `C:\pkg`) and
package identity across global and project settings.
- Extension preload matching, skill scope classification, resource
source info, the config and session selector keys, and the session
import self-check.
Separately, home-to-`~` shortening used an unanchored `startsWith(home)`
in four places, so `/home/me2/x` was shown as `~2/x`.
## Fix
New helpers in `utils/paths.ts`, all based on one comparison key:
- `getPathComparisonKey(path)`: the resolved path, lowercased on
Windows. It does not touch the filesystem.
- `pathsEqual(a, b)`
- `isPathWithin(parent, child)`: true for equal paths or paths inside
`parent`, matching whole path segments.
- `shortenHomePath(path, home?)`: replaces a leading home dir with `~`.
The helpers pick `path.win32` or `path.posix` from `process.platform` at
call time, so tests can exercise real Windows paths (drive letters,
UNC).
`getResourcePathKey(path)` in `source-info.ts` is the dedup key for
resources. Synthetic identifiers (`builtin:<name>`, `<inline:...>`) stay
unchanged, because they are not filesystem paths and their lookup is
case-sensitive. File paths go through realpath plus the comparison key.
The helpers are used only for comparison; stored and displayed paths are
unchanged. Trust store lookups and `set` match keys case-insensitively
on Windows.
Unchanged on purpose:
- The package install-root guards (`resolveManagedPath`,
`pruneEmptyGitParents`, the `node_modules` dependency check) keep exact
string checks. Both sides come from the same root string, and exact is
the stricter option.
- Checks based on `path.relative()` (`getCwdRelativePath`, the changelog
containment check) were already correct: Node's `path.win32.relative`
ignores case.
NTFS can enable per-directory case sensitivity, but it is rarely used
outside WSL interop, so it is ignored (see the comment on
`getPathComparisonKey`).
## Tests
- `test/paths.test.ts`: Windows drive-letter, UNC and root cases, POSIX
case sensitivity, segment-boundary containment, and home shortening,
including root and UNC homes.
- `test/source-info.test.ts`: file paths fold case on Windows; builtin
and inline identifiers keep their case.
- `test/trust-manager.test.ts`: the `.agents/skills` exclusion with a
lowercase `HOME`, plus saved trust decisions matched and replaced across
casing (simulated win32).
An end-to-end test of the reported loader scenario is not feasible on
Linux: the loader resolves paths with POSIX `node:path`, and the
filesystem is case-sensitive.
`npm run check` passes. The coding-agent suite passes, except for 13
failures that also occur without this change: clipboard tool detection
in this environment, and one test that needs built `dist/` output.
A temporary -e git source with a ref was treated as pinned and never refreshed,
so git:host/repo@main stayed on its first checkout. Refresh refs too, skipping
only a commit-hash ref that is already checked out.
Temporary -e npm: sources were installed once and reused forever. Like
temporary git sources, check npm on load (unless offline) and reinstall
with name@latest or the configured spec when a newer version exists,
keeping the cached install if the refresh fails.
A saved Do not trust decision was ignored when the project had no
trust-requiring resources, so ctx.isProjectTrusted() returned true.
The saved decision now takes precedence over auto-trust.
closes#10947
Some Apple Terminal setups send ESC+CR for Shift+Enter, which parsed as
alt+enter and queued a follow-up. The native Shift fallback now covers
ESC+CR as well as plain CR.
fixes#11559
When an image needs resizing, `resizeImageInProcess` builds a list of
encodings: PNG first, then JPEG at decreasing qualities. It returned the
first one under `maxBytes`. Since PNG was always first, it was used
whenever it fit, even when it was many times larger than the JPEG. The
doc comment says "Try both PNG and JPEG formats, pick the smaller one",
and the code worked that way before v0.62.0. Because of this, photos
were sent as multi-MB PNGs, which made requests large enough to fail or
time out.
This change compares PNG with JPEG at the configured quality at each
size step and tries the smaller one first. It tries the other one next,
and the lower JPEG qualities only after that. Screenshots and other
images where PNG is smaller still go out as PNG.
**Testing**
- Added a regression test to `test/image-processing.test.ts`: a
generated photo-like 1200×800 JPEG resized to 1000 px must come back as
`image/jpeg`. The test fails without the fix (PNG is returned) and
passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/image-processing.test.ts test/image-resize-callers.test.ts`: all
pass.
- `npm run check`: no issues in the changed files. It does report type
errors, all in `packages/ai/test` (model ID unions), which this change
does not touch.
- Reproduction with a photo-like 2400×1620 JPEG (460 KB base64):
- before: `image/png`, 4,008,480 bytes base64, at 2000×1350
- after: `image/jpeg`, 255,316 bytes base64, at 2000×1350
Fixes#11551
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>
## What was wrong
`pi update --extensions` first looks up the newest version of each npm
package with `npm view`. If that version is newer than the installed
one, pi runs `npm install <name>@latest`. The two commands read
different registry documents, and each is cached separately. Shortly
after a publish, `npm view` can already report the new version while
`npm install` still resolves the old one. npm then exits successfully,
pi prints "Updated packages", and the startup banner lists the same
package again on the next launch. The user can't tell that nothing was
actually updated.
## What the change does
- `shouldUpdateNpmSource` now returns the installed version and the
registry version along with its decision. Both are carried on
`NpmUpdateTarget`.
- After `npm install`, `updateNpmBatch` re-reads each package's
installed version. If the registry lookup succeeded and the version did
not change, it emits a `progress` event with the message: `<source> is
still X (registry reports Y); npm registry metadata may be stale, try
again in a few minutes`.
- The package CLI now prints `progress` events in yellow. Until now
nothing emitted them.
Install behavior is unchanged: pi still installs `@latest`.
## Testing
- New test in `test/package-manager.test.ts`: two packages are flagged
for update and only one is actually upgraded. The warning is emitted
only for the unchanged one. The test fails without the change.
- `test/package-manager.test.ts` passes (129 tests), and `npm run check`
passes.
- Ran the built CLI with an `npmCommand` stub whose `view` reports
`1.0.1` and whose `install` changes nothing:
```
Updating npm:demo-pkg...
npm:demo-pkg is still 1.0.0 (registry reports 1.0.1); npm registry
metadata may be stale, try again in a few minutes
Updated packages
```
Fixes#11536
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>
When a Zhipu/Z.ai (`open.bigmodel.cn`) plan hits its usage cap, the API
returns HTTP 429 with error code `1308`. The cap resets after hours, but
`isRetryableAssistantError()` still marked the error as retryable,
because the bare `429` in the message matched the transient-error
pattern. The agent-level retry then re-sent a request that could not
succeed and slowed down the final error. With default settings that is 3
attempts, about 14 s.
This change adds the Zhipu `"code":"1308"` error code to the
non-retryable provider-limit patterns in
`packages/ai/src/utils/retry.ts`, next to the existing quota and
subscription-limit entries. The pattern matches the JSON code field, not
a bare number, so request IDs or byte counts that contain 1308 do not
trigger it.
Not changed: the provider-level HTTP retry (`retry.provider.maxRetries`,
off by default). It still retries every 429 based on the status code
alone.
Testing:
- `npm run check` passes.
- `test/retry.test.ts` passes.
- Calling `isRetryableAssistantError` directly on the reported message,
`429 {"code":"1308","message":"已达到 5 小时的使用上限。…"}`, returned `true`
before the change and returns `false` after.
Fixes#11537
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>
At startup, interactive mode printed every skill diagnostic under the
heading `[Skill conflicts]`. Most of these are validation warnings for
skills that still load, for example `description exceeds 1024 characters
(1508)` or name-format warnings. The heading made them look like name
collisions.
This change renames the heading to `[Skill warnings]`. Validation and
loading behavior are unchanged. The prompt and theme headings are left
as they are.
**Testing**
- Updated the assertion in
`packages/coding-agent/test/interactive-mode-status.test.ts` ("still
shows diagnostics on quiet startup when requested") to expect `[Skill
warnings]`. It fails against the old heading and passes with the change.
All 37 tests in that file pass.
- `npm run check` reports six type errors in `packages/ai` tests about
model IDs. Unchanged `origin/main` reports the same six errors, so they
are unrelated to this change. It reports nothing in `coding-agent`.
Fixes#11521
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>
In regular TUI mode, user and assistant messages wrote OSC 133
(`A`/`B`/`C`) shell-integration markers straight to the terminal.
Terminals that display OSC 7501 program status (Rex, iTerm2 3.7.5+) read
`133;A` as a new shell prompt and drop the current status. As a result,
the `working` indicator disappeared right after a run started and only
`done` showed up at the end. Fullscreen mode was not affected because it
already strips these markers before writing.
## Change
- `TuiMainScreen.doRender()` (`packages/tui/src/tui-main-screen.ts`) now
strips OSC 133 markers from the start of each rendered line before
compositing and writing. It uses the same pattern as fullscreen
(`OSC133_ZONE_PREFIX`).
- The message components still produce the markers. Fullscreen uses them
internally for prompt navigation and already strips them before writing,
so its behavior is unchanged.
- In regular mode, the terminal's own jump-to-prompt shortcuts no longer
stop at pi messages (added in #1805).
## Testing
- New test in `packages/tui/test/tui-render.test.ts`: no `ESC]133;`
reaches the terminal across a full render and a differential render,
with both BEL and ST terminators. It fails without the change and passes
with it.
- These pass:
- `packages/tui` `npm test`
- `test/user-message.test.ts` and `test/assistant-message.test.ts` in
coding-agent
- `npm run check`
- I ran `pi -ne --tui-mode regular` with `PI_PROGRAM_STATUS=1` and a
streaming test provider, capturing all terminal output with `script`.
- Before: `7501 working`, then repeated `133;A`/`133;B`/`133;C`, then
`7501 done`.
- After: only `7501 idle` → `working` → `done`, and no OSC 133.
Fixes#11519
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>
A reference is a request that outlives its provider: a missing provider or a version that does not fit leaves
it unbound, waiting for the name (failed { waiting }, a new FailureCause kind), local and remote alike. An
attempt parks until its dependencies are live, with no timer and no backoff, woken on every state change of a
dependency; its readyTimeout runs only for its own steps. Requests wait only for the host's publication (the
startup deadlock is gone); load resolves at the first outcomes of its host-lifetime objects. Getters wait through
waiting until their readyTimeout. Registry readers are waiting dependencies, woken at every usable value; a last
good value is dropped when a contribution it was built from leaves. Unbound holds count for keep-alive and are
revoked; retry passes on to failed blockers once per operation (operation id and visited list on the wire); the
dependency deadline is gone and parked_long reports a long wait on a remote attempt.
In sessions created by `createAgentSession`, compaction, branch
summaries and bug-report summaries resolved the stored OAuth credential
in `AgentSession._getSummarizationRequestAuth`, then passed the access
token on as `options.apiKey`. The SDK's stream function calls
`modelRuntime.streamSimple`, which resolves auth again. Because it was
given an explicit `apiKey`, `resolveProviderAuth` took the API-key
override branch instead of using stored OAuth. Built-in providers accept
the token either way. A provider whose API-key path differs from its
OAuth path, such as one that refuses explicit keys, failed every summary
while normal prompts worked.
`createAgentSession` now marks its stream function with
`markRuntimeStreamFunction`. This records it in a module-level `WeakMap`
keyed by the function, mapped to its `ModelRuntime`.
`_getSummarizationRequestAuth` skips pre-resolution when the agent's
stream function is marked for the session's own runtime, so that runtime
resolves auth once, as it does for normal prompts. The check runs after
virtual-model routing. Bare `streamSimple` and custom `streamFn`s still
receive resolved credentials, so the fixes for #6324 and #7579 are
unchanged. If a caller replaces `agent.streamFunction`, the old behavior
applies.
**Testing**
- Added `test/suite/regressions/11520-sdk-summary-oauth-auth.test.ts`.
It builds an SDK session on a provider that has stored OAuth and also an
API-key method, runs a branch summary, and asserts two things: no
`getAuth` call carries an `apiKey` override, and the provider receives
the OAuth access token. It fails without the change and passes with it.
- `npm run check` passes and `./test.sh` passes. I also ran the related
tests: 6324 branch-summary auth, compaction model overrides,
auto-compaction cancellation, SDK stream options, virtual models, and
the compaction and branch-summary tests.
- Manual reproduction: an SDK script with an `openai` OAuth credential
that logs `ModelRuntime.getAuth` during `navigateTree(..., { summarize:
true })`. Before the change there were two calls, the second with
`apiKey = <oauth token>`. After the change there is one call, with no
override.
Fixes#11520
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>