Sends copied text to the terminal with OSC 52 after the local clipboard
write, as over SSH, for remote clients without SSH variables such as
multiplexer attaches and remote IDE daemons.
closes#10826, closes#10835, closes#10114
A misplaced options line was ignored, so its timeout_ms never applied and
the script ran without a deadline. parseCodemodeSource now throws
CodemodeSourceError before the script runs.
fixes#10631
RemoteExecutionEnv.listFiles resolves the path and lists through the
daemon; a daemon without the listFiles feature yields not_supported.
LocalExecutionEnv is NodeExecutionEnv plus listFiles through a daemon on
the same machine, so local and SSH sessions list with the same code.
nodePlatform maps hello's Rust names to Node's (windows -> win32,
macos -> darwin, x86_64 -> x64, aarch64 -> arm64).
Lists a directory tree like fd --hidden --exclude .git without following
symbolic links, with the ignore crate of fd and ripgrep for git's ignore
rules. Records travel as payload (kind byte, relative / path, NUL); the
listing stops at maxEntries, maxBytes or timeoutMs with truncated, and a
cancel stops it between entries. hello reports features: ["listFiles"]
so clients can tell older daemons apart.
An environment may list the files, directories and symbolic links below a
directory: relative / paths, depth first in name order, without .git and
what git ignores in work trees, bounded with a truncated flag. Callers fall
back to their own walk when it is missing or fails with not_supported.
The env conformance suite checks it with the new listFiles option.
Retry known strict tool capacity rejections once with preferred tools using their original non-strict schemas. Preserve required constraints and payload hooks, and never retry a started stream.
closes#10409
A just-removed auth.json.lock directory can stay delete-pending on Windows
while another process holds a handle, making mkdir fail with EPERM. Retry
this briefly (capped separately from ELOCKED) so real permission errors
still fail fast.
fixes#10801
Print mode now writes a warning to stderr and exits 1 when the final
response stopped at the model's output token limit. The partial text
is still written to stdout.
fixes#10799
A tool call that was aborted or timed out while waiting for a shared OAuth
refresh was still retried by StreamableHttpTransport once the refresh
finished, so the server executed a call the caller had already given up on.
McpClient now passes a per-request signal to McpTransport.send(), aborted
once the request settles (cancelled, timed out, answered, or closed).
StreamableHttpTransport checks it before each dispatch. The shared refresh
itself is not aborted, so other requests waiting on it still succeed.
fixes#10598
The codemode row shows "Elapsed" while the script runs and "Took" once
it finishes, like bash rows. The timer logic moves from the bash renderer
to render-utils, and scripts send an initial update so the timer starts
before the first nested call.
closes#10708
Wheel events that no component handles now scroll the transcript instead
of reaching the focused overlay's handleInput as raw input. Overlays that
scroll with the wheel must implement handleMouse.
fixes#10769
MCP 2026-07-28 allows structuredContent to be any JSON value, including
arrays and null. callTool() rejected non-object values, failing the whole
tool call. CallToolResult.structuredContent is now unknown, and MCP tools
without an output schema declare structuredContent as unknown to codemode.
refs #10416
Servers with resources registered no tools until resources/list and
resources/templates/list finished, so a slow listing (a large drive)
kept their tools out of the first prompt. The lists only fed counts in
/mcp and pi mcp list; both now show that a server offers resources.
The resource tools still list and read resources on demand.
fixes#10526
Entries like !~/skills/pdf, +~/x and -~/x in the extensions, skills,
prompts, and themes settings were compared with a literal ~ and never
matched.
fixes#10765
Chord: README rewritten for the new runtime; notes.md and requests.md removed, their open limits and
decisions folded into spec.md section 12; guide and spec status lines updated. Durable: drops the
pico-v5 handoff and delta findings, renames the Pico spec, and uses current Chord terms. Delta README,
issue categories, and PI_RADIUS_GATEWAY updated.
Moves chord2 (contracts, objects, cells, roots, plugins, hot reload, memory/IPC/WebSocket links)
from p2 into packages/chord, with its tests (now on vitest) and docs. The root export is the new
runtime plus the shared base (Context, JsonValue, JsonRepresentation, Draft, copyJson, isJsonValue,
replicatedState and its types). ./node now holds the host manager, child control, process locks,
and nodeLoader. ./context and ./delta are unchanged; ./bundler is removed.
replicatedState stays a standalone module with the same exports. It no longer exposes the exact-op
subscription the removed service provider used, so listener failures of mutable states are always
reported asynchronously. The package depends on typebox instead of esbuild. The wire format stays JSON.
Deletes packages/client, packages/protocol (CBOR codec), packages/server, coding-agent's
src/experimental (except source-resolver.ts, which source runs still preload), src/cli/experimental,
src/client, the example plugin, and their tests. coding-agent no longer depends on @earendil-works/chord.
The installer lock drops esbuild, so its platform-package sanity check goes too.
A wait is rejected when it closes a cycle in the wait-for graph: waits of
tasks that are not abort-marked, and holds of live ordinary owned work. The
rejection names the cycle. Replaces the owner-chain rule, so a background task
may wait on its conversation's owner. Approach and tests from #10533 by
@odysseus0.
fixes#10411
On each OpenRouter refresh, combine the catalog (built-in models plus a newer pi.dev catalog) with GET /models/user for the user's key: keep only the listed chat models and take their context size, max output and prices from it. Image and classifier models are kept. The combined list is saved and replaces the OpenRouter catalog, including offline.
- ai: add personalizeOpenRouterCatalog and RefreshModelsContext.baseUrl, so the request follows a base URL configured in models.json (regional endpoints)
- ai: share OpenRouter price conversion with the generator; negative prices (router models such as openrouter/auto) become 0
- coding-agent: add withPersonalizedCatalog next to withRemoteCatalog
- docs: OpenRouter section in providers.md
fixes#10353
- ToolRegistration.deferred: not offered to the model until modelTools names it
- modelTools gains { remove?, add? }; control.addTools writes names to add, so
loaded tools follow the others in load order and a load only appends
- Agent.deferred lists deferred tools not loaded yet
- @earendil-works/pi-durable/tool-search: the coding agent's tool_search (BM25)
- the codemode section leaves deferred tools out
joinAdjacentText checked endsWith on the growing joined string, which
makes V8 flatten the concatenation on every item. Collect each text run
in an array, check the separator against the last non-empty part, and
join once. Same fix in the durable code mode copy.
closes#10678
Runs started by pi.sendMessage(..., { triggerTurn: true }) skipped before_agent_start. After their first tool call, the next-turn refresh fell back to the base prompt options and patched away the sections handlers had added, changing the system prompt mid-run and breaking the provider prompt cache.
These runs now go through the same setup as prompt(): compaction check, before_agent_start, tool loadout, nextTurn messages, handler messages, and the system patch. BeforeAgentStartEvent is a union on messageType ("user" or "custom"); custom events carry the starting message.
The session counts as busy from the start of setup, so messages sent while handlers run go into the starting run instead of starting a second one, and an abort requested meanwhile aborts the run once it starts.
closes#10267, closes#5581, closes#10639, closes#10289
partly fixes#10606
pi-codemode no longer exports MCP_TYPESCRIPT_PREAMBLE or mcpStructuredContentSchema(); output schemas render as their own types, as in the coding agent.
Tools that reach other tools declare it with reachesTools(input); MCP uses it to wait for the servers a call needs and to decide which codemode or tool_search tool to activate, instead of parsing codemode scripts and comparing parameter schemas. Namespaces get aliases, replacing codemode's MCP-specific __ suffix rule.
pi-codemode no longer knows MCP: MCP_TYPESCRIPT_PREAMBLE and mcpStructuredContentSchema() are removed, and the MCP result schema describes itself, with the server's local $refs relocated so they resolve as before.
- structuredOutput is always the tool's own value; a schema tool's error text
without one becomes a tool_error diagnostic on nested results
- code mode: a schema tool's error result with its value resolves (bash nonzero
exit), schema-less tools resolve with their text, as in the coding agent
- codemode prompt section with tool declarations by namespace within an inline
budget; searchTools/describeTool/describeNamespace from the start data
- result layout of the coding agent: header, text markers, console block,
truncation within max_output_tokens and the tool's output limits, spill and
image files in a recorded output directory (a rerun overwrites them)
- models.*: catalog globals; classify/generateImages as hidden internal tools
(nested calls with usage), at most four at once
- Lark grammar of the script source as constrainedSampling
A host that closes its CodemodeSandbox before the Harness, as on a graceful
shutdown, made the running code mode call store "Script error (aborted):
Sandbox closed" as its result, so the script never resumed. A run the sandbox
aborts without the call asking for it now stores nothing and ends with the
call's invocation; the next process replays the script.
details.calls (CodemodeToolDetails): each tool call the script made, in call
order, with status ok/error/cancelled, duration, and cut arguments and error;
calls still running or not yet launched when the script ended are cancelled.
Written once with the result: live progress is pi.live.nestedTools.
bench:codemode compares a code mode script with a plain tool making the same
nested calls: 1,000 parallel calls 0.82 vs 0.78 ms/call, sequential 1.52 vs
1.10 ms/call (two more commits per call: the journal entry and the count).
A codemode tool whose scripts call the conversation's tools as nested calls and
survive a restart. The script runs deterministically (pi-codemode start()); each
result is journaled before it is delivered. A rerun starts the script again from
the stored start data, delivers the journaled results in order (nested calls
reattach by key c<id>), checks every recorded call count, holds calls the
journal lacks until replay ends and checks them against earlier attempts' keys,
then continues live. A call launches only once the count of the drain that made
it is committed, so a rerun can prove which calls are new. Divergence, a changed
module version or sandbox fingerprint, and a passed deadline end the call with a
stored error; a decided outcome is returned without a VM. store()/load() keep
values per conversation, rewindable and forked as of the fork's entry.
pi-codemode: CodemodeRun.finished, set when the outcome is decided.
Property comments now carry constraints the TypeScript type cannot
express (integer, bounds, lengths, pattern, format, default, ...) as
@ tags, read from nullable anyOf branches too. Types over the size
limit are rendered again without tags before becoming unknown.
closes#10707
For durable code mode: a host can rerun a script after a restart and get the
same calls with the same arguments. start() runs the script with a virtual
clock (startedAt, then each result's time, never backwards), randomness from a
seed, UTC, no WeakRef/FinalizationRegistry, and the run's own tools and
globals; every call goes to onCall with the VM's call id, and the host answers
with run.deliver(seq, now, id, outcome). The VM applies results strictly in
seq order whatever the transport does, reports its fingerprint (engine
versions, prelude hash, limits) before the script runs, the calls issued after
each result (onIdle), and the result it was processing when the script ended
(result.during). Rejections can carry properties for the script's Error.
execute() is unchanged.
Preserve terminal reply strings until their terminator while retaining bounded recovery for ambiguous legacy Alt-key input.
Related: #10256, #10657. Missing OSC prefixes and fragmented DA1 replies remain separate issues.
The delta tracker planned every structural array change with a walk over all
base elements; inserts and removals that keep the remaining elements in order
now get the same plan from the changed ranges alone. applyImmutable built an
appended array as a copy that the insert then grew, a second copy; an append now
builds it with one concat. Ops, values, immutability and errors are unchanged.
32,000 one-item appends, one commit each: 11.3 s -> 1.4 s
(npm run bench:document-append in packages/durable).
Programs calling a tool with structuredOutputSchema got isError and no text when
its error result omitted structuredOutput; they now get its bounded output,
collapsed like a schema-less tool's, so a code mode script sees the error text.
A thrown error's message stays in its tool_error diagnostic.
Radius omits providerThinkingLevel from pi-messages done events, so replayed
signed thinking has no effort marker and Anthropic drops it (block_binding
drop_block). Compares follow-up input tokens with and without the thinking
block against an Anthropic control.
The process group in front of the pty when it is not the program's own (a job its shell runs), with the leader's name on macOS and Linux. Exposed as RemoteTerminal.foreground().
The daemon gets pty, ptyWrite and ptyResize: raw terminal output as events, input in arrival order, SIGWINCH on resize, and the process group killed on cancel or a lost connection. openTerminal() is the client, localDaemon() the daemon for this machine. Unix only; Windows answers ENOTSUP.
A generation waiting on a blocking threshold compaction sent its request
whatever the outcome. When summarization failed, the context was still above
the blocking threshold: a session at 996K of a 1M window got an empty length
answer on every turn and nothing showed the error. The generation now fails
with model_error (Compaction failed: <error>) when its compaction failed,
faulted, was orphaned, or was aborted directly; a compaction that found
nothing to compact or was declined still lets the request go ahead.
pi.live.compactionFailure keeps the newest failure (task, reason, error)
until a compaction produces a summary. The agent events' snapshot carries it,
and compaction_end carries error for a failed compaction.