Repository navigation
Pi-Bolt 0.8.5 - #19
Merged
Merged
Conversation
…M_REGISTRY with a linear scan
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.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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.
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)
…or (#10810) (cherry picked from commit dbc7ff8)
(cherry picked from commit 78a3c10)
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
JSON.parse.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/pion macOS and Linux with the new runtimes.Closes #18.