Skip to content

Pi-Bolt 0.8.5 - #19

Merged
Kushalkhemka merged 50 commits into
pi-boltfrom
release/0.8.5
Oct 12, 2026
Merged

Kushalkhemka merged 50 commits into
pi-boltfrom
release/0.8.5

Conversation

@Kushalkhemka

Copy link
Copy Markdown
Member

Pi-Bolt 0.8.5: faster and lighter in use on macOS and Linux, with new runtimes. Release notes: docs/releases/0.8.5.md.

Pi

  • TUI: containers, transcript rows and the editor keep their lines between frames; regular mode finishes only changed lines; key IDs parsed once per key press; redraws only what may use grammars loaded on demand.
  • Streaming: OpenAI streams read with the indexOf-based SSE parser (now shared with Anthropic); streamed tool-call arguments parsed incrementally with native JSON.parse.
  • Startup: undici skipped on Bun and loaded on first use; zlib, node:net, worker_threads, highlight.js grammars and the image decoder load when first needed; the tmux hyperlink probe runs in the background.
  • Sessions: projection kept between calls; idle garbage collection after a run on Bun; truncated tool output kept once; extension events skipped when nobody listens.
  • Tools: edit previews and patches diff only the changed lines (output identical to jsdiff 8.0.4, differential tests); truncation cuts once.
  • npm installer: linear trimming of the download base address.
  • Request-JSON reuse for openai-completions was tried and reverted: it saved CPU in long sessions but held more memory than 0.8.0.

Engine (patches/webkit.patch, patches/bun.patch; runtime stamp 4f3a01358c6ef988)

Measured against 0.8.0 (M5 and a pinned Linux VM): interactive CPU −21% on both, time per turn −9% / −22%, long-conversation CPU per prompt −19% / −31%, long-conversation memory −13% / −16%.

Tests: npm run check, ./test.sh (all suites), tests/aot (61/61), tests/runtime, tests/pi on macOS and Linux with the new runtimes.

Closes #18.

The OpenAI SDK's Stream finds line and event ends one byte at a time and
decodes every line separately: 12-32 us per event. The Anthropic provider's
parser (TextDecoder + indexOf) moves to utils/sse.ts, and openai-completions,
openai-responses and azure-openai-responses read the response body with it
through iterateOpenAIStream(), which keeps what the SDK's Stream does: [DONE],
error events, aborts, early exit, logging. A "\r" at the end of a read now
ends its line at once, as in the SDK.

Differential tests compare the events with the SDK's Stream and with the
previous Anthropic parser on fixed and fuzzed streams split at every byte.
Container.render() copied every child's lines on every frame, and Box compared
every line, so a frame in fullscreen mode cost time in proportion to the whole
transcript. Containers now keep the joined lines (renderShared()) and reuse
them while no child's lines changed, keeping the unchanged leading children's
lines when one does. render() still returns a copy.

A child counts as unchanged when it returned the same array and its class
declares, with markStableRender(), that it returns a new array whenever its
lines change and never modifies one it returned (Text, Markdown, Image,
Spacer, Box, MouseRegion, plain Containers). Any other component, such as an
extension's, is compared line by line against what was joined the frame
before, so one that modifies its array in place renders as before. Box reuses
the padded lines of unchanged child lines, Spacer keeps its lines, and the
fullscreen layout reads the shared lines without a copy.
Every frame in regular mode ran applyLineResets() on every line of the
transcript and scanned every line for Kitty images, several times. Each line
that is the same string as the frame before now takes its finished string from
the frame before, which also makes the diff an identity comparison, and the
image passes run only while an image is shown or was shown the frame before.
The editor segmented and word-wrapped every logical line of the prompt on each
frame and again for cursor movement. It now keeps each line's chunks, by line,
for the current width and set of paste IDs, and wraps only lines whose text
changed.
A key press is matched against every keybinding, and each matchesKey() call
parsed the key ID and ran the Kitty and modifyOtherKeys expressions on the
input again. Parsed key IDs are now kept by ID (configured keybindings are
just other IDs), and the last input's parse is reused while the same input is
matched.
…ntent

Assistant and user messages, tool rows and bash runs now add to their
children's lines in renderShared(), so an unchanged entry returns the same
array and the transcript container skips it. The OSC 133 prompt markers are
added to a copy kept with the lines it was made from instead of to a fresh
copy every frame. A self-rendered tool row keeps its joined lines while its
parts are unchanged, and a hidden row returns one empty array.

Rebuilding an assistant message (invalidate(), streaming) takes its Markdown
components again when their text and options are unchanged, so they keep
their parsed tokens. The edit row renders its diff again only when the diff or
the theme changed, and keeps unchanged rows' components. The collapsed shell
preview styles and wraps only the lines it shows; the lines before them are
counted, with the same result as wrapping all of them.
When the syntax grammars outside the eager set finished loading, the whole UI
was invalidated, so every transcript entry was rendered again from nothing.
The loader now reports the names asked for before that are languages now, and
does not report at all when none is (such as `console`). Only transcript
entries that may show code in one of them are invalidated: messages whose text
names one (or that Markdown transformers change), tool rows whose renderers
highlight it (built-in renderers declare whether they highlight, and in which
file's language), and every entry that does not say, such as extension
components. Everything outside the transcript is invalidated as before.
Each parse of arguments that are still streaming in ran JSON.parse,
repairJson and partial-json's parser, which looks at every character in
JavaScript: 0.77 s for a 400 KB write with the JIT off.

- Text that starts an object but does not end with "}" (or an array and
  "]") cannot be complete, repaired or not: the repair pass is skipped.
- parseStreamingJsonWhileStreaming keeps a scan per tool call of the open
  containers, strings and the last complete member, continued from where
  the previous text ended; the text is completed by closing what is open
  and parsed with JSON.parse. Texts where partial-json reads differently
  (incomplete numbers at the end, "[ ]", "__proto__" keys, malformed JSON)
  are still left to it. 0.77 s -> 0.05 s for the 400 KB write.

A differential test compares every prefix of fuzzed, malformed and real
tool-call arguments with the previous parser.
…ished messages

Every request sends the whole conversation, and the SDK ran JSON.stringify
over all of it each time: late in a long session megabytes of tool results
escaped again for text that had not changed.

convertMessages marks the long strings of a request (tool results, user
prompts, assistant text, reasoning, tool-call arguments) with an object of
the message they come from. stringifyRequestBody() writes the body as
JSON.stringify does, taking the JSON of a marked string from a cache held by
that object, used only while the string is the same value (===); anything
it cannot write the same way (a member with toJSON, an error) is left to the
SDK. The body goes to the SDK as a string with a content type, which it
sends as it is, so headers, retries and timeouts are unchanged. Entries no
request has used for 16 requests are dropped, so the cache holds about the
long strings of the conversation being sent.

Tests compare stringifyRequestBody() with JSON.stringify on fuzzed bodies,
and the bytes and headers of the request with what the SDK
sends, on first and cached requests, with compat options and onPayload hooks
that change or replace the body.
… on first use

Bun's fetch does not use undici dispatchers, so configuring one there had no
effect, and importing undici cost startup time and memory. On Node the
dispatcher is configured as before; undici now loads when it is configured.
… used

Zip archives, Herdr reports and image resizing are not needed at startup;
their built-in modules now load with process.getBuiltinModule() on first use.
…t use

Running the 21 grammar definitions took about 1 ms at every start, also when
nothing is highlighted.
The check whether tmux forwards OSC 8 hyperlinks ran tmux through /bin/sh
synchronously while the theme was set up, 10-40 ms of every start in tmux. It
now runs with execFile without waiting: hyperlinks stay off until tmux answers,
and an answer that turns them on redraws the UI once (onCapabilitiesChange).
On Kitty-protocol terminals (Kitty, Ghostty, WezTerm, Warp) photon loaded at
every start for extension images, and two callers each queued a full redraw.
A stand-in transcoder now loads it the first time a non-PNG image is drawn,
with one pending redraw.
The projection was rebuilt from the whole leaf path several times per turn
(requests, the compaction check, the footer's context usage). SessionManager
now keeps it, extends it on appends and rebuilds it after compactions, context
edits, branching or a replaced session; callers get copies. The context size
estimate is kept until the projection changes, and the session name is kept up
to date instead of being searched for on every frame.
After an agent run, once Pi has been idle for 500 ms (no run, no compaction,
no keystroke), run a full collection if the heap grew by 32 MB since the last
one. bench/long_session.py gets --think-ms to leave such idle time between
prompts.
…ines

Normalization has an ASCII fast path, the normalized file is shared by all
edits of a call, and the line diff runs jsdiff 8's search without splitting or
comparing the unchanged prefix and suffix. Output is identical to jsdiff's
(randomized comparison in test/edit-diff-fast-path.test.ts).
truncateHead and truncateTail measure lines in place and cut the kept range
once; truncateMiddle counts removed characters without an array of them.
emit() built a context and a snapshot for every streamed delta even when no
extension handles the event.
read, ls, find, grep and bash put the whole TruncationResult into the result's
details, so the kept text was held (and saved with the session) twice: as the
result's content and as details.truncation.content. Details now carry the
truncation fields with an empty content; renderers only read the counts.
…property lookups (#18)

Thanks to @carsteneu for the x86-64 ahead-of-time fixes in #18.
Reusing the JSON of long strings saved CPU in long sessions but held more memory than 0.8.0 when prompts follow each other without a pause.
Copilot AI balanced review requested due to automatic review settings October 12, 2026 03:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T03:51:17.974525Z f735e65 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…uncher in front

Git for Windows' bin\bash.exe is a launcher that sets MSYSTEM, EXEPATH, PLINK_PROTOCOL and a PATH prefix, then starts
usr\bin\bash.exe: a second process start, about a third of every bash tool call. The bash tool (and a !command config
value) now starts usr\bin\bash.exe with what the launcher would have set, where Git's layout says so unambiguously (one
subsystem with git.exe, usr\bin\bash.exe with its msys-2.0.dll; checked again at each use), and the launcher otherwise.
An environment with both PATH and Path is prefixed where the child reads it; the home's bin is HOME's, else HOMEDRIVE+HOMEPATH,
else the profile's, and only a full path. 10 calls: 1,595 -> 1,442 ms on an i5-1335U (-15 ms a call).
resolveWindowsCommand checked every PATH folder for each PATHEXT extension (5-10 ms for an npm or git started by name). Its
answer is kept by PATH, PATHEXT and name, and taken again only when that file is gone. Still PATH's absolute folders only.
The search of the folder and the search below it are independent, and each is a process start (about 20 ms on Windows):
they now run together, and their results are merged as before.
…on Windows

Defender checks each file of a newly installed package the first time it is read, and the extension is compiled once into
its cache: seconds once, then about half a second. Not a reason to exclude Pi from Defender.
…er cache in a folder of its own

- The sockets' sweep timer could not be stopped with libuv (Windows): once Pi had made a request, it woke every 4 seconds
  for as long as it ran. It stops with the last socket now (oven-sh/bun#37096): 25-28 context switches in 40 s idle after
  a request -> 15-21.
- A task left queued after a tick wakes the event loop's poll on Windows instead of waiting for the next event
  (oven-sh/bun#42968).
- On Windows the runtime transpiler cache's default folder is @t@pi-bolt, beside stock Bun's @t@. Entries are named by
  their source and the two runtimes' differ in version, so each took the other's for stale and rewrote them: pi-subagents
  started after stock Bun had run it took 3,994 ms, now 472 ms. Linux and macOS keep @t@.

All three are Windows-only (cfg(windows), or usockets' libuv backend, which only Windows builds). The engine tree is Bun
cce01a9504cb775970d5c54296a006fd3a851301 on release/0.8.5's; WebKit is release/0.8.5's.
…ash tool keeps Git's launcher in front

The job is attached once the shell is running. The launcher's own start leaves the time for that before bash has started
anything; started directly, bash could start a program outside the job first, which stopping the command then reached only
through taskkill (one ping left in 29 Escapes under heavy load). The default build, which has no job objects, keeps starting
Git's bash directly.
davidbrai and others added 13 commits October 12, 2026 11:19
Long Mistral streams (e.g. extended thinking) were aborted once timeoutMs
elapsed, even while data was still arriving. The timeout now covers only
waiting for response headers; body stalls are left to the HTTP client idle
timeout.

fixes #10609

(cherry picked from commit 6b07b4e)
fixes #10633

(cherry picked from commit 6fb2e78)
The nested worktree dedup compared the context file's realpath with the
main repo's file, so a worktree AGENTS.md symlinked to the main repo's
copy was skipped along with it. Compare the walked directory's realpath
plus the file name instead.

fixes #10681

(cherry picked from commit f1b2e77)
…ered

Without metadata there is no issuer to compare an RFC 9207 iss parameter with, so the check was skipped and a malicious MCP server could receive a code from another authorization server. Reject the response before the code exchange instead.

Reported by Alexios Stefanopoulos (@AizenvoltPrime).

(cherry picked from commit c5f5b32)
Replayed thinking is often not part of the live context (for example,
radius drops it, and Anthropic drops blocks that do not match their
request), but serializeConversation included all of it. A Pi Mobile
session at ~996K of 1M context tokens produced 1.38M-token summarization
requests that failed on every attempt. Providers only return summaries of
the reasoning anyway; visible text, tool calls, and tool results record
what happened.

(cherry picked from commit 15e7bdc)
fetch accepts it as a body under the DOM lib too. From pi-private e97c4a60e.

(cherry picked from commit 0f4b146)
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).

(cherry picked from commit 091bcc0)
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.
(cherry picked from commit edb2eb9)
…latform

With stock Bun's @t@ shared, each runtime rewrote the other's entries: a start with three extensions after stock Bun had run them took about 200 ms longer on Linux and macOS.
@Kushalkhemka
Kushalkhemka merged commit b93895d into pi-bolt Oct 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants