A database an earlier unreleased build wrote at pi.live version 2 has a run
without run.turns; a run in flight across the upgrade then counted NaN turns
and failed its next tool round with "Value contains a non-finite number and
is not strict JSON". Version 3's migrate adds turns: 0 wherever it is
missing.
GenerationHooks.afterRun asks, after a run answered, for durable tasks
(runEndTask(task, input)). The answer commits and the run's inputs settle
done first; the generation then creates the tasks as its children in a
finish phase and waits for them in drain, keeping pi.live.run (run.afterRun)
and the conversation busy, so idle waits include them, Stop aborts them and
a crash resumes them. Queued input is placed when they are terminal.
pi.live.run.first and runFailure.first name where a run's history starts,
also for a continue after a failure.
closes#11141
HookApi.entry() and entries(), and TaskRuntime.entries(): a hook or task
reads the conversation's raw history, which compaction never changes, so a
range bounded by entry IDs reads the same before and after a compaction and
in a rerun.
Conversation.submit(tx, draft) and ConversationHandle.submit(tx, draft) run
the same admission inside the caller's commit and return the submission ID,
so a host's own checks and writes and the admission are stored together or
not at all. Admission must come before the commit's first table write, once
per commit, and throws ReadAfterWrite otherwise on every path. A first
argument that is not a draft is taken as a transaction; one of another
Session, or a wrapped one, is refused. The handle form also rejects once its
invocation is aborted. The pi.durable.submit span covers the admission only.
closes#11149
An input with whenIdle: "reject" is refused with the new ConversationIdle
when the conversation has no run, before any write, so it never starts a
run. With whenBusy: "steer" it steers the current run or is refused.
Request-ID deduplication runs first.
The next object runtime moves from the private repository into pi: src/next (contracts, hosts and
objects, documents, derives, links and relays, routines and jobs, the store) with its tests and
examples in test/next, src/next-node (Node compression), and its design in docs/next (design.md, the
decision log decisions.md, the slice plan slices.md, slice specs, books and benchmarks). AGENTS.md
gets its code style. Not exported: the API changes with every slice until it has an entry point.
Addresses #5958.
Adds `changelogPath` to `PiManifest`. Safe package-relative changelog
resolution, with `CHANGELOG.md` fallback, and carries it through
`PathMetadata`/`SourceInfo`.
Added extension changelog handling like Pi’s own changelog.
If the package exposes `pi.changelogPath` or has `CHANGELOG.md`, Pi
stores the last seen package version in settings. On each fresh
interactive launch, we compare the stored version to the installed
package version. Then parses the changelog entries in between, shows an
Extension Updates section, then marks the current version as seen. The
first time an extension is seen, its version is recorded without showing
a changelog, same as Pi's own on a fresh install. `collapseChangelog`
condenses this to one line per package.
Any update that bumps the extension's version shows this on the next
launch, e.g.:
```bash
pi update --extensions
pi update --all
pi update <extension-source>
pi update --extension <extension-source>
pi update --self --extensions
pi update pi --extensions
pi update self --extensions
```
<img width="798" height="626" alt="Screenshot 2026-08-28 at 16 34 15"
src="https://github.com/user-attachments/assets/c882f01c-94b9-4857-b9b7-243d88c0d437"
/>
## Summary
- add live documentation comparisons for project-scoped and user-scoped
`/current-time` prompt templates
- require the generated command to expand to the exact requested prompt
text
- select the requested case from multi-case Vitest reports so skipped
siblings do not invalidate observations
- strengthen documentation-audit evidence and move the expensive audit
behind an explicit `eval:audit` command
- remove redundant eval oracles and test-only exports found during the
eval-suite audit
## Testing
- `npm run check`
- `./test.sh`
- `npm --prefix packages/evals test` (54 passed, also covered by
`./test.sh`)
- failed-status mutation: focused report test passed before the fixture
repair, failed after it, then passed with production restored
- prompt-template documentation eval, three repetitions per variant (6/6
pairs eligible)
- project scope: with docs 66.7%, without docs 100%; negative delta and
treatment flakiness
- user scope: with docs 100%, without docs 100%; no lift
- model documentation eval (1/1 pair eligible; treatment 100%, control
0%)
- representative live documentation audit (`cli.md` passed)
This builds the model catalog from per-provider definitions and
models.dev v2 data.
It eplacer `generate-models.ts` with one `defineProvider()` file per
built-in provider, shared model-family rules (Claude, GPT, DeepSeek,
Kimi, Google), and source adapters. models.dev is read through its v2
API. Outputs and CLI flags are unchanged.
The test copied the generator's model-ID rule and snapshotted upstream catalog IDs, so it broke on catalog changes without describing behavior beyond the code.
The system prompt lists tool snippets and per-tool guidelines in the
order of the active tools. When an extension called
`pi.setActiveTools()` with the same tools in a different order (for
example `[...pi.getActiveTools()].reverse()`), the "tools" and "rules"
sections came out different. pi then recorded a "System Prompt Update:
tools, rules" message even though the active tools had not changed. On
providers without mid-conversation system messages, this also rewrites
the prompt head and loses the cache.
`_applyToolLoadout` in `agent-session.ts` now treats the requested names
as a set:
- tools that stay active keep their current order;
- newly activated tools are appended at the end.
Reordering the same set therefore leaves the prompt unchanged. Adding a
tool only appends its bullet. The same rule covers tool lists changed by
`before_agent_start` handlers, since they go through the same function.
Testing:
- Added a test in `test/system-prompt-updates.test.ts`: it reverses the
active tools between two prompts and expects a single system message. It
fails without the change (a second message patches `tools` and `rules`)
and passes with it.
- `npm run check` passes.
- `./test.sh`: all coding-agent suites pass. The only failure is an
unrelated Anthropic model metadata test in `packages/ai`.
Fixes#11495
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
With `codemode.mode: "on"`, each declared tool gets a description suffix
that says what a script call resolves to. For an object output schema,
`describeOutput` shortens this to field names. Any other schema fell
back to the whole rendered TypeScript type, collapsed onto one line. A
tool with a union output schema therefore put its full output type,
nested fields and comments included, into every request. The reporter's
`process` tool, a union of 7 variants, added about 8,000 characters.
Collapsing the type onto one line also left `//` comments in the middle
of the text.
This change:
- **Unions:** for `anyOf`/`oneOf` schemas where every variant is an
object, the suffix lists each variant's field names, joined by ` | `.
- **`const` fields:** `describeFields` now shows the value of `const`
fields, so a discriminator like `action` stays visible. For example: ``
`{ action: "start", process } | { action: "list", processes, truncated?
}` ``.
- **Other multi-line types**, such as an array of objects: instead of
the inlined type, the suffix says `` a value; `await
describeTool("name")` shows its type ``. `describeTool()` already
returns the full declaration.
- **Single-line types** such as `number` are unchanged.
Testing:
- I added a test in `test/suite/agent-session-codemode.test.ts` that
checks the suffix for three tools: one with a union output, one with an
array-of-objects output, and one with a `number` output. It fails
without the change and passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/suite/agent-session-codemode.test.ts
test/suite/agent-session-mcp.test.ts` passes, including the existing MCP
description assertions.
- `npm run check` is clean.
Fixes#11501
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
In fullscreen mode, the startup `Model scope: …` notice was still in the
shell after pi exited. With `fullscreenExitOutput: "resume-hint"`, the
shell showed it above the resume command.
Cause: `InteractiveMode.init()` printed the notice with `console.log`
before the renderer started. In fullscreen mode that output goes to the
terminal's normal screen. Pi then draws on the alternate screen, and
when it exits the terminal switches back to the normal screen, where the
notice is still visible.
The change:
- Building the notice text moves into `getModelScopeNotice()`.
- Regular mode prints it with `console.log` before the TUI starts, as
before.
- Fullscreen mode adds it to the header container below the built-in
header. It uses a `ThemedText`, so the text is built after the terminal
reports its colors and follows theme changes.
Since fullscreen is the default `tuiMode`, this applies to every user
who has `enabledModels` set and quiet startup off.
Testing:
- Added
`packages/coding-agent/test/interactive-mode-model-scope.test.ts`. It
checks two things for fullscreen: the notice renders in the header and
`console.log` is not called, and nothing is added when startup details
are hidden.
- `npm run check` passes.
- Manual check in tmux with `enabledModels` set:
- fullscreen + `resume-hint`: before the change, `Model scope:
claude-sonnet-4-5, claude-opus-4-5 (Ctrl+P to cycle)` was left in the
shell after `/quit`. Now the line shows inside pi's header and the shell
is clean after exit.
- `tuiMode: "regular"`: unchanged, the line is still printed above the
header.
Fixes#11508
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
Starting `pi` from a directory that has been deleted crashed with an
uncaught Node `ENOENT ... uv_cwd` stack trace. This also happened for
`--version` and `--help`. The cause is the unguarded `process.cwd()`
call in `main()` (`packages/coding-agent/src/main.ts`).
`main()` now catches `ENOENT` from that call, prints one line telling
the user to change to an existing directory, and exits with code 1.
Other errors are rethrown unchanged. Argument handling is otherwise
unchanged: `--version` still needs a valid cwd, but it now gets the
readable error instead of a crash.
**Testing**
- Added `test/deleted-cwd.test.ts`. It stubs `process.cwd()` to throw
ENOENT and checks that `main()` prints the message and exits with code
1. It also checks that a non-ENOENT error is rethrown. The first test
fails without the change and passes with it.
- `npm run check` passes.
- Manual check with the built CLI. Before, all three commands below
printed the `uv_cwd` stack trace. After, each prints `Error: The current
working directory no longer exists. Change to an existing directory and
try again.` and exits 1:
```sh
d=$(mktemp -d); cd "$d"; rmdir "$d"
pi -ne --version; pi -ne --help; pi -ne
```
Fixes#11509
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
The ~anthropic/claude-*-latest aliases used Chat Completions, where pi
dropped the thinking signature and OpenRouter ignored the unsigned
thinking on replay. Route them through Anthropic Messages with adaptive
thinking, like the versioned Claude models.
refs #9534
Chord and Durable change a document one transaction at a time, so a tracker registered a WeakRef per
change for competitors that never existed on their hot path. The WeakRef targets survived scavenges
and V8 could pretenure every overlay context into old space (47.5 MB promoted against 2 MB in a
tracker loop). The tracker now keeps its open change in a plain field and moves it into the weak
registry only when a second change opens; stale semantics are unchanged. A draft dropped without
prepare or abort stays referenced until the tracker's next change or adopt (at most one per tracker).
`stored` and `rawNodes` are created on first use.
## Summary
The docs refresh (#9898) cut `docs/extensions.md` from about 3,100 lines
to 130, dropping API reference and tool guidance. #11039 restored part
of it. This PR restores the full guide and corrects it against current
source.
- **Structure:** the original sections, with the ASCII lifecycle diagram
replaced by three Mermaid diagrams (run, turn, session) and ordered hook
lists, plus a handler composition table.
- **Merged:** content added after the cut: tool exposure, calling other
tools, structured output, MCP servers, `registerToolRenderer`, virtual
models, `provider_stream_event`, `before_agent_start` `messageType`.
- **Corrected:** removed or renamed APIs (`modelRegistry.stream()`,
prompt sections are `<tools>`/`<rules>`, `builtin:read` paths,
`highlightCode`), wrong behavior claims (errors only by throwing,
`model_select` on restore, `sendUserMessage` always triggering a turn,
override warning, `truncateLine` bytes), examples that did not
type-check, links to removed `tui.md` patterns and `rpc.md` anchors, and
wrong API lists in the examples table.
- **Kept:** anchors that other docs and changelogs link to, as `<a id>`
tags.
- **Fixed example:** `reload-runtime.ts` tool queued `/reload-runtime`
with `deliverAs: "followUp"`. Queued text reaches the model as a plain
user message, so the command never ran and Pi never reloaded. The tool
now sets a flag and returns `terminate: true`; an `agent_settled`
handler sends `/reload-runtime` with `expandPromptTemplates: true`,
which runs after the settled handlers finish, while the agent is idle.
In the long run it may be better to revisit how API reference is,
provided to the agent, especially since more capable models do explore
the source without it.
## Validation
- `npm run check`
- Edited TypeScript examples type-check against current types
- All relative links and anchors in `extensions.md` resolve, including
inbound anchors from other docs
- `reload-runtime.ts`: faux-provider harness test (temporary, not
committed) shows one reload after the run settles and no extra model
request; the old version never reloads. Also verified in interactive
mode with a real model.
- Reviewed by GPT-6 Astra and GPT-5.6 Sol against source; all findings
applied
Chord's shared watch: listeners take a tagged call, { kind: "value", value, ops, retired } or
{ kind: "failed", error }; a recoverable failure keeps the watch open and the next value arrives as
[["r", value]]; WatchEnd is tagged by kind, and an aborted context ends a watch "aborted".
Durable: observers take the watch's shapes (advance(call, ctx), end(end)); onReport(report) with
report.kind and report.error, session_failed is storage_failed; subscribeCommits is observeCommits;
SessionEnd and ReportFrom are tagged by kind; FileChange is tagged and file watchers stop with
stop(ctx); CompactionFailure and CompactionStatus name their trigger (pi.live version 2 with a
migrate); the task_outdated report uses the scheduler's reasons; TaskInspection's ready is pending;
DocumentCommitChange is tagged. Stored formats are unchanged.
LiteLLM's pre-call context check rejects requests that are too large
with HTTP 400 and a message like:
```
litellm.ContextWindowExceededError: litellm.BadRequestError: litellm._pre_call_checks: Context Window exceeded for given call. ... Max Input Tokens=262144, Got=265262 ...
```
None of the `OVERFLOW_PATTERNS` matched this wording, so
`isContextOverflow()` returned false. As a result, the agent session
never ran overflow compaction and retry, and the turn failed with the
400 error.
This change adds `/context window exceeded/i` to `OVERFLOW_PATTERNS` and
documents the error in the doc comment that lists providers.
Testing:
- Added a regression test in `packages/ai/test/overflow.test.ts` with
the exact error message from the issue. It fails without the pattern and
passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/overflow.test.ts` in `packages/ai`: 32 passed.
- `npm run check` passes.
- Running `isContextOverflow()` directly on the reported message
returned `false` before this change and returns `true` after it.
Fixes#11492
Requested by @rwachtler
Co-authored-by: triagent <triagent@localhost>
A generation whose request or poll fetch was in flight at a host death counts
one failed attempt of the retry policy (backoff, the partial kept, the same
deferred handle polled again); without retries left the run ends with
runFailure interrupted. restartLimit ends every other task visibly instead of
parking it (tool error result, compactionFailure, runFailure restart_limit,
faulted extension tasks) and reports task_interrupted; retryTask, task_parked
and the restart_limit blocked reason are removed.
closes#11139
ConversationView.entries is a window over the fork-aware history with before;
conversation.watch(ctx, { from, min, max }) and observeConversation take the
window. ConversationTranscript merges windows into a client's transcript with
gaps, loadEarlier, fillGap and resume. pi.live.runFailure says why a run ended
without an answer. watchEvents and its event types are removed; the changelog
has the migration guide.
closes#11327
update the llama.cpp docs to use the llama.app installer and the new
`llama serve` command
commit by @hanouticelina
disclaimer: my julien-cto agent helped me write this
---------
Co-authored-by: Celina Hanouti <hanouticelina@gmail.com>
Co-authored-by: Mario Zechner <contact@badlogicgames.com>
When the daemon exits before `hello`, the error has only the exit code.
For example, with `loginShell: true` and a profile that aborts on a
missing file:
```
pi-env exited with code 3 before it was ready
```
`sshConnection`'s probe and deploy steps already report ssh's stderr, so
this is about the launch itself: a login profile, a daemon that can't
execute, or a `Connection` created with its own command, which has no
probe or deploy step, so auth and network errors also end up here.
This keeps a short stderr tail until `hello` and adds it to startup
errors:
```
pi-env exited with code 3 before it was ready
remote stderr:
profile: /opt/toolchain/env.sh: No such file or directory
```
The tail is capped at 20 lines / 2000 chars, with terminal escapes and
control characters stripped. Ready sessions are unchanged.
I hit it while testing `loginShell` against an sshd in Docker. Tests are
in `test/connection.test.ts`, and `npm run check` and `./test.sh` pass.
Display math (`$$…$$`, `\[…\]`) renders as several 2D rows: numerator,
fraction bar and denominator are separate lines. When the layout was
wider than the Markdown content width, `render()` wrapped each row with
`wrapTextWithAnsi` on its own. Only the overflowing row broke, so the
rows below moved down and the equation no longer lined up. For example,
`dx` landed between the fraction bar and the denominators.
The `latexBlock` case in `Markdown.renderToken()` now checks the
rendered layout against the available width. If any row is too wide, it
uses the raw LaTeX source, the same fallback already used when parsing
fails. The raw source then wraps like normal text. Layouts that fit
render exactly as before.
**Testing**
- Added a regression test in `packages/tui/test/markdown.test.ts` using
the equation from the issue. At width 40 it renders the 2D layout
unchanged. At width 30 it falls back to the raw `\[ … \]` source, with
no fraction bars and no line wider than 30. The test fails without the
fix and passes with it.
- `node --test test/markdown.test.ts`: 93 pass. `node --test
test/latex.test.ts`: 111 pass.
- `npm run check` reports no errors in the touched files. The only
errors are 6 type errors in `packages/ai/test`, caused by stale
generated model data, and they are the same on `main` without this
change.
- Reproduction at width 30, before the fix:
```
∂² u ∂² u n 1 1
──── + ──── = ∑ ── +∫ f(x)
dx
∂ x² ∂ y² i=1 i² 0
```
After the fix, the raw source, wrapped:
```
\[
\frac{\partial^2 u}{\partial
x^2} + \frac{\partial^2
...
\]
```
Fixes#11483
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
When writing a session entry failed (ENOSPC, EBUSY, ETIMEDOUT on network
drives), `SessionManager._appendEntry` had already added the entry to
memory and made it the current leaf. The process kept running, and the
next entry written to disk pointed to a parent that was not in the file.
On resume, pi follows the history only back to that gap: everything
before it disappears. In #11460 that also dropped the first system
message, which declares the tools, so resume turned off every built-in
tool.
`_appendEntry` now undoes the in-memory append if writing fails, then
rethrows. The error reaches the caller as before; later entries chain to
the last entry actually on disk.
Reports of this problem:
- #11460: ENOSPC; resume dropped all built-in tools.
- #10166: failure injected with EBUSY; the next entry on disk points to
a missing parent.
- #10661: ETIMEDOUT on Windows + NFS; resumed sessions showed only the
last few messages. That report also describes NUL holes in the file,
which this change does not address.
**Testing**
- Two tests in `test/session-manager/file-operations.test.ts` make
writes fail with real file permissions: a read-only session file (failed
append) and a read-only session directory (failed first file creation).
They check that the entry is not kept, the leaf is unchanged, the next
entry chains to the last written one, and the reopened session's branch
contains both entries. Both fail without the change and pass with it.
- `test/session-manager/` and `test/default-tools-setting.test.ts` pass
(140 tests).
- `npm run check` passes.
Fixes#11460
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>