Changelog

What's new.

Veyyon's own releases, newest first, with what's queued for the next one at the top. The /changelog command in the CLI opens this page, and launch can check for a newer version. The full history, including everything inherited from oh-my-pi before the fork, lives in the CHANGELOG on GitHub.

Unreleased

next release
Added
  • The browser tool's open takes context, an isolated cookie jar that every headless tab naming it shares and no other tab sees, closed with its last tab (#947).
  • The browser tool's save_state action and tab.storageState() write every cookie of a tab's context and the localStorage of its open origins to a Playwright-compatible state file with mode 0600, and open's storage_state and tab.loadStorageState() load one into a headless tab before the first request, writing localStorage once so a key the site later clears stays cleared; a spawned or connected browser's session can be saved but not overwritten by a load (#947).
  • The browser tool's open with a url returns the loaded page's aria snapshot, whose refs a run can act on, when it is at most 6,000 characters, so a small page needs no separate call to read it; a larger page's size is stated instead, and the open's card draws its own rows as before.
  • The browser tool's tab.uploadFile takes a button, label or other control that opens a file chooser and hands the chooser the files, refuses an input that takes no files, and fails naming a control that opens no chooser.
  • The browser tool's tab.ref() and tab.id() can be called on without awaiting the handle first: await tab.ref("e5").click(); a call left unawaited on a ref that resolves to nothing no longer ends the tab.
  • The browser tool's tab.ariaSnapshot(), and an open's page snapshot, nest each iframe's content under its line, cross-site frames and frames within frames included, with refs like f1e3 that tab.ref, aria-ref= selectors, fill and click act on inside the frame; a click there is refused, naming the cover, when the page above the frame covers its point.
  • A browser action's selector may be a snapshot line's role "name" form, such as tab.fill('textbox "Email"', …) or tab.click('button "Save"'), or the attribute forms textbox[name="Email"] and role=button[name="Save"], which act on the element with that role and that exact accessible name instead of failing as invalid CSS; input[name="q"] stays CSS.
  • A browser action's selector may be a comma list that mixes CSS with aria-ref=e5, a snapshot line's [ref=e5], role "name" or text/… alternatives, and acts on the first alternative in written order that matches; a list with no match, and a selector that does not parse, fail within 2 s instead of waiting out the action's 8 s; tab.select and tab.press's selector take refs.
  • A browser action's selector may end in Playwright's :has-text("…"), such as button:has-text("Sign in"), which acts on the first element its CSS matches that holds the text, case and spacing aside, or on the innermost element holding it when no CSS precedes it; :has-text() nested in another pseudo-class stays refused.
  • The browser tool's tab.reload() loads the tab's current URL again with goto's waiting and deadline, fetching a page that a form's POST produced with a GET instead of submitting the form a second time; a selector passed as an object names tab.id(n) and tab.ref("eN").
  • A browser run whose fetch fails on a relative URL such as /api/items says that run code executes outside the page and gives the tab.evaluate form that makes the page's own request with its cookies.
  • Added browser.naturalInput (default off): browser clicks, hovers and drags move the pointer along a curved, eased path and rest before pressing, typing pauses between keys, short fills are typed key by key, and off-screen elements are scrolled to with the mouse wheel; off restores the instant input.
  • The browser tool reports a bot challenge it finds after open and run, naming its vendor, whether it clears on its own, needs a person or blocks the browser, and the evidence, once per page.
  • The browser tool waits up to 20 seconds, or the call's timeout when shorter, for an interstitial bot check such as Cloudflare's to clear, and states where the page led or that it did not clear.
  • The browser tool's open takes visible: true | false, which moves a tab between headless Chromium and a browser window with its page, cookies and localStorage, so a person can solve a challenge and the run continues.
  • The browser tool's open takes profile: "<name>", a persistent Chromium profile under the agent directory whose cookies, storage, cache and history survive sessions; a profile another process holds is refused, naming its lock.
Changed
  • A replace-mode edit whose old text is not in the file finds its closest candidate about 18 times faster (1.6 s to 90 ms on a 7,400-line file), and its streamed preview no longer repeats the search on every argument chunk, which held the terminal for seconds while a near-miss edit streamed.
  • The browser tool sends a run's displayed and returned objects to the model as one line of compact JSON instead of two-space indented JSON, which is 36–41% fewer characters for a tab.observe() of a real page; the card still lays them out indented.
  • A browser run no longer sends a return value that it already displayed, which was 20% of all run-result text in bench sessions.
  • The browser tool's tab.ariaSnapshot() and an open's page snapshot leave out bare generic wrappers (layout <div>s with no name, text, state or pointer), 7–30% of a real page's snapshot.
  • The browser tool's snapshots leave out a name that a row, list item, column header or other non-control node computes from the children listed under it, and [cursor=pointer] on links, buttons and other roles clickable by definition, 10.9% of the snapshot characters in calibration runs; controls keep their names and every ref stays.
  • A browser run that reaches for document, window or another name the page defines fails with an error that says the name exists in the page and to use it inside tab.evaluate, instead of a bare "is not defined".
  • The headless browser presents the host's own OS, architecture and browser brands, the same in the page, every worker kind, HTTP headers and client hints, instead of claiming Windows on Linux hosts; deviceandbrowserinfo.com/are_you_a_bot now reports it human where it reported a bot.
  • The headless browser's WebGL renderer, screen size, window chrome and mouse-event screen coordinates match the host OS and agree between the page and its workers, and WebGL never reports SwiftShader.
  • navigator.hardwareConcurrency is capped at 8 in the page and every worker alike, and navigator.languages and Accept-Language are the browser's own.
  • A browser click presses a point inside its element other than the exact centre, checked again once the pointer arrives, and tab.fill types a one-line value of up to 24 characters key by key while browser.naturalInput is on, inserting longer values in one edit.
  • A browser tab opened again without visible or profile keeps the window and profile it is on.
  • tab.select selects by the keys a person types at a focused <select> (Chromium's type-ahead, including Cyrillic and CJK labels) and adds a multiple select's further options by a click with Control or Command held, so the page's input and change events are trusted; a disabled option, a value no option has and a customizable select are still set by script, and selecting the options already selected sends no events.
  • tab.fill on a date, time, date-time, month or week input presses each field of its editor and types its digits, and on a range presses Home, End, PageUp, PageDown and the arrow keys, so the page's input and change events are trusted; a colour, and a range more than 40 keys from its value, are still set by script.
  • A reused browser open with a viewport and no scale emulates scale 1.25 as a first open does, instead of 1.
  • The browser tool no longer scans every evaluated script for a sourceURL suffix the pinned puppeteer never appends; no user-visible change.
  • The browser tool description (1,815 estimated tokens, from 1,440) states that run code executes outside the page, that raw page.evaluate cannot see page globals, that a click never presses a cover and that refs chain, that a state file loads only into a headless tab, that iframe content nests in a snapshot with f1e3 refs, which selector forms act, what profile and visible do, and that a Challenge: line needing a person is handed off rather than solved, and asks the model to act on an open's snapshot without reading the page again, to act and read back in one run, and to act through tab actions instead of probing hit targets or page scripts first.
  • The retry recovery helpers moved from session/runtime/retry-runtime.ts to session/failed-turn.ts; no user-visible change.
  • Session switching and its rollback moved from session/agent-session.ts to the session/runtime/session-switch.ts collaborator; no user-visible change.
  • The settings card's agent roster, model-chain picker and shared submenu pieces moved out of settings-selector.ts into agents-submenu.ts, model-chain-submenu.ts and settings-submenus.ts; no user-visible change.
  • Branching, the /btw branch and tree navigation moved from session/agent-session.ts to the session/runtime/session-branch.ts collaborator; no user-visible change.
  • The handoff document and the new session it seeds moved from session/agent-session.ts to the session/runtime/session-handoff.ts collaborator; no user-visible change.
  • The active tool set, the base system prompt with its invalidation record, and the MCP, RPC host, SSH and vibe tool refreshes moved from session/agent-session.ts to the session/runtime/tool-activation.ts collaborator; no user-visible change.
  • The collab guest dialogs, the hook dialog queue and the extension widgets moved out of extension-ui-controller.ts into collab-guest-dialogs.ts, dialog-queue.ts and hook-widgets.ts; no user-visible change.
  • The login, logout, account card and saved-reset flows moved out of selector-controller.ts into account-flows.ts, and the session picker, resume and delete into session-flows.ts; no user-visible change.
  • The /usage and /session panels and the /memory command moved out of command-controller.ts into usage-report-view.ts, session-info-view.ts and memory-command-controller.ts; no user-visible change.
  • The tool-call cards, the compaction and retry loaders with the pinned turn error, the idle compaction and recap, the IRC cards, the replaceable job and todo blocks and the speech hooks moved out of event-controller.ts into tool-call-cards.ts, turn-recovery.ts, idle-follow-ups.ts, irc-cards.ts, displaceable-blocks.ts and turn-speech.ts; no user-visible change.
  • Paste intake, the /queue batch and dequeue, submission routing and the Esc chain moved out of input-controller.ts into composer-paste.ts, queued-submissions.ts, prompt-submission.ts and composer-escape.ts, and a detached abort from those paths is logged under the new module's name; no user-visible change.
  • The composer, plan review, plan annotation, /todo edit and hook-editor external-editor handoffs share one editInTerminal, so /todo edit now runs the editor on the controlling terminal like the composer's ctrl+g instead of on the inherited stdin.
  • Plan mode, the plan-review overlay, plan approval and vibe mode moved out of interactive-mode.ts into plan-mode-controller.ts and vibe-mode-controller.ts, and the local-artifact copy into a fresh approved session moved to internal-urls/local-protocol.ts; no user-visible change.
  • The anchored todo board, the agent HUD, their shared motion clock, the todo auto-clear and the agent-change rebuild moved out of interactive-mode.ts into anchored-blocks.ts; no user-visible change.
  • The ACP plan-approval resolve types its input as ResolveParams, as the terminal plan mode does; no user-visible change.
  • The LSP server selection, diagnostics wait, project diagnostics, rename_file and the edit/write writethrough moved from lsp/index.ts into lsp/servers.ts, lsp/diagnostics.ts, lsp/workspace-diagnostics.ts, lsp/rename-file.ts and lsp/writethrough.ts, so the edit and write tools no longer load the lsp tool; no user-visible change.
  • The extension handler time budget and the lspmux liveness probe use settleWithin from @veyyon/utils instead of their own deadline racers; no user-visible change.
  • The ephemeral side-channel turn behind /btw, /omfg, the idle recap and the composer prediction moved from session/agent-session.ts to session/agent-session-ephemeral-turn.ts; no user-visible change.
  • The synchronous !command and environment config-value resolver moved from config/model-registry.ts to config/resolve-config-value-sync.ts; no user-visible change.
  • The goal-continuation todo context and the magic keyword notices moved from session/agent-session.ts to session/agent-session-turn-notices.ts; no user-visible change.
  • The Julia kernel's execute-request encoder is the exported buildExecutePayload in eval/jl/kernel.ts instead of an inline constructor option; no user-visible change.
  • A prompt, a steer, a follow-up and a synthetic follow-up build their text-and-image content and the text-only model's image description through one session helper; no user-visible change.
  • The model registry's persisted resolved-models.json stage codec moved from config/model-registry.ts to config/resolved-model-stage.ts; no user-visible change.
  • The model registry builds custom-model and provider-override headers through createLiveConfigHeaders directly instead of two single-use wrappers; no user-visible change.
  • The live config-header builder moved from config/model-registry.ts to config/live-config-headers.ts; no user-visible change.
  • Model override, custom model and provider compat application moved from config/model-registry.ts to config/model-patch.ts; no user-visible change.
  • The session appends a custom message without a turn, and merges queue-chip text into a queued message's details, through one helper each instead of four and two copies; no user-visible change.
  • The last-assistant-message readers moved from session/agent-session.ts to session/agent-session-message-shapes.ts; no user-visible change.
  • Doc comments that module splits left above the wrong declaration (stageModels, AUTO_THINKING, ConfiguredThinkingLevel) are back on their definitions; no user-visible change.
  • Source comments that named a moved or deleted symbol's old module now name its current one; no user-visible change.
  • Dropping a failed assistant turn and its never-ran tool results from active context moved from session/agent-session.ts to session/runtime/turn-context-check.ts, beside the function that puts one back; no user-visible change.
  • The status line reads the session's provider and OAuth account through its typed members instead of a reflective record with a fallback for a session shape that no longer exists; no user-visible change.
  • The GitHub cache reads its three settings by their declared paths instead of through an untyped cast; no user-visible change.
  • MCP request params, the reloaded ssh tool and the argument display read through their declared types instead of as unknown as casts; no user-visible change.
  • Settings reads in the status line, extension dashboard, speech input, browser, Hindsight memory, advisor roster, task agent settings, extension wrapper and ACP extension listing use their declared types instead of casts; no user-visible change.
  • Permission intent and memory consolidation parsing read their arguments through the isRecord guard instead of repeating it as a cast; no user-visible change.
  • The inline tool-output budget no longer re-checks for a non-finite tools.artifactSpillThreshold, which the settings read already replaces with its default and reports; no user-visible change.
  • The session composition root drops a redundant field comment to stay within its measured size; no user-visible change.
  • Session creation applies the web search and image provider preferences through the one reader the session scope uses; no user-visible change.
  • Settings reads drop the fallbacks and kind re-checks the typed read already guarantees, and provider preferences read their declared types; no user-visible change.
  • The read tool's default line limit is bounded through the shared clampLow helper; no user-visible change.
  • Internal modules import output notices, tool status icons, formatters, the memory root, the active skill set, the IRC switch and the GitHub fetchers from the modules that define them instead of through compatibility re-exports; no user-visible change.
  • The github tool's Actions run model, fetches, outcome rules and watch reports moved from tools/web/gh.ts to tools/web/gh-run-watch.ts; no user-visible change.
  • Extension and custom slash-command resolution and the command context a session builds without an extension runner moved from session/agent-session.ts to session/agent-session-commands.ts; no user-visible change.
  • The process agent lifecycle manager is rebuilt when the process agent registry is replaced, so it never terminates or parks agents through a registry nothing reads; no user-visible change.
  • Deleted tool-renderer, output-notice, image and LSP helpers that nothing called (renderCollapsedOutputLines, formatMeta, formatEmptyMessage, formatTitle, the rendered-string memo, appendParseErrorsBulletList, the styled truncation notices, SUPPORTED_INPUT_IMAGE_MIME_TYPES, getServerForFile, hasCapability); no user-visible change.
  • The benchmark CLIs share one time-to-first-token predicate, the guided-goal turn and the memory passes share one per-attempt credential refresh, the model registry and the sign-in list share one reader of disabledProviders, and the legacy plugin loader resolves package imports and exports targets through one function; no user-visible change.
  • Removed the parameters autoresearch branch handling and the release installer accepted and never read; veyyon update --force still reinstalls the running version, with no user-visible change.
  • The auth broker command imports the credential store from @veyyon/ai/auth-storage-sqlite, with no user-visible change.
  • Modules import the timeout, whitespace, advisor transcript name and slash-command category helpers from their owning modules; the utils/fetch-timeout re-export module is removed, with no user-visible change.
  • The LSP client and diagnostics, the web search tool and the web-search command import the language-id, grouped-diagnostic and search-provider metadata helpers from their owning modules instead of the lsp/utils and search/provider re-exports, which are removed except getSearchProviderLabel; no user-visible change.
  • Built the OpenRouter and Gemini image-generation URLs from the @veyyon/catalog/provider-endpoints constants instead of repeated host literals; no user-visible change.
  • 96 modules that read only the active theme import it from theme/theme-binding instead of loading the theme engine, so 60 of them no longer reach theme JSON loading, highlighting or mermaid; no user-visible change.
  • Removed two section comments in the github tool module that described fetchers now in gh-fetch.ts; no user-visible change.
  • The status line's context gauge finds its usage anchor by walking back from the newest entry instead of scanning the whole branch, so drawing it on a 97,000-entry resumed session takes 0.19 ms instead of 2.1 ms per frame.
  • Writing a message after a custom entry adds only the new entries to the session's message-timestamp index instead of re-reading the whole branch, so each such write on a 97,000-entry session takes 0.49 ms instead of 5.4 ms.
Fixed
  • A tools.approvalMode in a config file that is not one of its values reads as ask instead of the default auto, and every configured value that does not match its declared type is shown at startup with the file to edit.
  • A failing !command for a provider API key or header in models.yml names the setting it was resolving in its log report, as the other config values already did.
  • ctrl+g in a hook or ask-input editor whose $VISUAL/$EDITOR cannot start shows the failure under the text instead of ending the session, and with no editor configured shows that instead of doing nothing.
  • A provider error that ends a turn is logged at warn with the provider's message, model and error classification, and a failure's retry classification is logged once instead of once per recovery check (#935).
  • A headless browser launch reads the binary's product and version again after a --version probe that failed or timed out, instead of launching every later tab of that binary without a matching --user-agent, which named itself HeadlessChrome.
  • A credential-free web search that returns before its soft or hard deadline cancels the deadline's timer instead of holding the embedding process open until the deadline passes.
  • Disposing an advisor during the one-second delay before it retries a failed review ends the delay instead of holding a timer, and the embedding process, until it runs out.
  • A web search cancelled while its headless-browser fallback is opening a page closes that page once it opens instead of leaving it open in the shared browser, and a search cancelled during the fallback's retry delay ends at once instead of after the delay.
  • When several sessions start the launch process broker for one directory at once, one broker runs instead of a second one that read the first one's lease before its claim was written and took it as stale.
  • With browser.naturalInput on, a click whose delay an op deadline or abort cuts short releases the button at once instead of holding it for the rest of the delay, which failed the tab's next press with 'left' is already pressed.
  • A browser :has-text() selector matches the text Playwright matches, so a page whose <title>, inline <script>, <style> or <noscript> holds the words acts on the element showing them instead of timing out on an element with no box, and a submit input's value and a shadow root's text count as their element's text.
  • tab.fill on a time or datetime input whose min and max narrow the hour field, such as min="13:00" max="17:00", types the hour as a person does instead of setting the value by script with untrusted input and change events.
  • With browser.naturalInput on, a click through a tab.id() or tab.ref() handle whose element the page replaces while the button is held is made again on the replacement instead of reporting success with no click sent.
  • The browser tool removes the temporary Chrome profiles a crashed or killed process left in the system temp directory before it makes the next one.
  • A headless browser tab opens while another user of the host has a browser open or left a temporary profile in the shared temp directory, instead of every launch failing with EACCES on that user's profile.
  • A browser open with visible whose move fails or is interrupted after the tab left its browser, such as on a page that cannot load, puts the tab back on its browser with its page, context, cookies and localStorage instead of losing it, and a move whose tab has a page holding a dialog open fails within the call's timeout instead of after a minute.
  • A browser click or hover on an element in a cross-site frame, including a frame inside a frame, waits for the frames to draw first, so a press right after the page loads reaches the element instead of the document above its frame while reporting success.
  • With browser.naturalInput on, a tab.id() or tab.ref() handle whose element the page re-renders while the wheel scrolls to it scrolls to the replacement instead of failing as detached, and loading the browser tool no longer evaluates arktype before its first schema is built.
  • After a sign-in form in a headless browser tab, clicks and keys reach the page again: the browser runs with the password manager's save offer and breach check turned off, whose prompts Chrome drew over the tab a few seconds after sign-in and which blocked every later action without an error.
  • A session without the browser tool no longer reads about it: the read description names the browser only when the tool is on, and the reminder to verify an edit names only the check tools (bash, eval, debug, browser) the session has.
  • The browser tool's tab.fill replaces a value in one trusted text insertion, so a React or Vue field's state follows it, the empty value included, and a fill takes about 1.5 ms where the locator fill waited 33 ms for a stable bounding box; it fills contenteditable elements, sets date, time, colour and range inputs with input and change, accepting a value such an input stores in another form (#FF0000, 2026-09-26T10:30:00, 50.0), refuses checkboxes, radios, file inputs, <select> and read-only fields with the call that handles them, and refuses an element that cannot take focus instead of typing into the field that has it.
  • A browser click, hover or drag in a headless tab other than the most recently opened one completes instead of stalling for eight seconds and failing as hidden or covered: each run brings its tab to the front.
  • A browser tab.click by selector or aria ref, and a handle's click() and hover(), no longer press whatever covers the element: a cover that goes within 2.5 s is waited out, one that stays fails the call naming it with nothing pressed, and a disabled control is waited for until it is enabled. A handle's click(), hover() and fill() run under the deadline of the run that calls them and end with it.
  • A promise that browser run code does not await and that rejects, such as page.click("#missing") without await, fails its run as Unhandled rejection (missing await?), or is logged once that run has ended, instead of ending the tab's worker so the next run hung until the tab was killed.
  • A browser tab closed twice at once, or opened again while it closes, is torn down once, instead of releasing its context and browser twice and closing them under other open tabs; a tab closed while a timed-out run's worker is being replaced no longer keeps that new worker running.
  • A browser run stuck in synchronous work past its deadline, such as an execSync or a busy loop, gets its worker replaced and keeps its page, its fields and its session for the next run, instead of killing the tab.
  • A failed browser run's text, what it displayed and its error, is held to the same inline budget as a result's, keeping its head and its tail, instead of sending an execSync's stderr or a dumped page whole on that turn and every turn after it.
  • A browser tab.waitForNavigation() written after the click that navigates, with only reads or wait between them, returns once that page has loaded instead of timing out on a navigation that committed before it started listening.
  • Opening a second headless browser tab no longer resets navigator.platform in the first to the host's native value.
  • Workers the headless browser starts no longer run from blob: URLs, which exposed their location and broke relative importScripts.
  • Opening a visible browser on a Linux host with no display fails with a message naming DISPLAY and WAYLAND_DISPLAY instead of Chromium's launch error.
  • A browser run stuck in synchronous work no longer holds new tabs and page workers in every tab until the stuck command returns: the main thread holds each tab worker's browser connection and closes it when it replaces the worker, so during an execSync with 8 seconds left a new tab opens in 301 ms instead of 8,077 ms and another tab's page worker answers in 32 ms instead of 8,022 ms.
  • A browser tab whose stuck run had its worker replaced keeps its host-true identity, page scripts and viewport, and still loads state files, activates for each run and closes its page on close, instead of reporting empty client hints and a default-size window on its next page.
  • A headless browser tab opened with its own viewport reports a window around that viewport, instead of the shared window's width around it, which devtools detectors read as an open DevTools panel.
  • The headless browser screen leaves the host's taskbar or menu bar out of its work area and opens the window inside it, instead of a work area the size of the whole screen, and the functions its page scripts patch refuse a prototype cycle as native functions do, with chrome.runtime stubs that are not constructors: CreepJS rates the browser 31% like headless and 0% stealth instead of 38% and 40%.
  • The browser tool's bot-challenge check reads the page in puppeteer's isolated world, instead of making about 30 document.querySelector calls and a text walk in the page's own world after every open and run, where a page that replaces DOM methods saw them.
  • A visible browser tab emulates no viewport: its page lays out in the window's content area at the display's scale and follows the window when a person resizes it, viewport resizes the window to hold that content area, and a stuck run's replacement worker keeps the window's size, instead of a 1365x768 page at scale 1.25 whose bottom 87 px the window did not show; viewport.scale on a visible tab is refused.
  • The browser open result and tab.observe() report a visible or attached tab's content area and scale as its viewport, instead of 1365x768 at scale 1.25.
  • tab.observe() no longer fails with Cannot read properties of null (reading 'nodeType') while a date, time or colour picker is open; the picker's own controls are left out of the list.
  • A browser tab.drag() or multiple tab.select() cut short by its deadline or an abort releases the mouse button or modifier it pressed, instead of leaving it held so every later click on the tab failed with 'left' is already pressed or arrived as a Control-click.
  • A browser text/ click releases its page handles and its deadline timer when it returns, instead of leaking a handle per clickable ancestor on every poll.
  • A browser the browser tool spawned is killed on session end or a failed attach when the native addon cannot load, instead of being left running.
  • A foreground bash command that finishes before the auto-background threshold, a client-terminal command that exits before its timeout, an edit whose LSP diagnostics arrive inside the 500 ms inline wait, MCP startup, an LSP or DAP adapter shutdown, the GPU probe, the speech recorder's start wait, an async job delivery wait, the browser's selector hint, file-chooser and frame-draw waits, memory dispose and session shutdown clear their deadline timer when the work finishes, instead of leaving it armed for the rest of the window (up to 300 s for the bash threshold) and holding the process open.
  • A hook or custom command's ctx.ui.terminal.custom factory is typed (tui, theme, keybindings, done), the arguments the terminal passes, and the qna and handoff hook examples call done instead of the keybindings manager.
  • A custom command on a session built without an extension runner redacts through ctx.obfuscateProviderText instead of throwing a TypeError.
  • A discovered custom tool's factory-scoped ui and hasUI receive the host's UI, instead of staying a no-op context and false in every mode.
  • The image-generation, speech and Exa tools join a session's custom tools without a type cast; no user-visible change.
  • Fixed the task card crashing or drawing NaN% confidence when a reviewer yields a verdict whose explanation, confidence or correctness has the wrong type; the card now draws the last well-formed verdict.
  • Fixed a resumed session restoring a goal whose recorded status is not one this build writes, which ran as neither active nor finished while its badge showed it running; a usage record with such a status no longer overwrites the goal's counters.
  • Fixed a scalar where a settings group belongs in config.yml (such as tools: on) moving the whole file aside to config.yml.corrupt; the scalar is ignored and every other setting in the file still loads.
  • Setting the todo reminder limit no longer turns todo reminders back on, and turning reminders off no longer drops the limit; the limit is todo.reminderLimit, and a stored todo.reminders.max moves there on load.
  • Startup configuration warnings state what a setting of the wrong kind or outside its allowed values reads as, and report such values in --config overlay files as well as the profile config.
  • The task tool's context argument streams into the pending preview as it arrives in batch mode instead of advancing in throttle-sized steps.
  • The update check, release-list and checksum fetches, release binary download, Smithery requests and Hindsight requests bound the response body with their deadline and clear its timer when the request finishes, instead of leaving it armed afterwards.
  • /mcp smithery-login falls back to API-key entry after five minutes when a browser-login poll request stalls, instead of hanging.
  • The /mcp add wizard repaints a finished connection check and the review step that follows it without waiting for a key, and Esc or a newer check during the check or its pause keeps the step the wizard is on instead of jumping to the review step.
  • Fixed the /tree and /branch pickers cancelling themselves 100 ms after opening over an empty list, which also cancelled twice when dismissed inside that window; they now stay open on their empty state until dismissed.
  • The ! command block draws every streamed output window instead of skipping any that arrive within 50 ms of the previous one, which left the last line before a quiet period undrawn and joined the lines on either side of a skipped window; the ! and % blocks also no longer overflow the call stack on an output window of about a million lines.
Removed
  • The headless browser no longer returns a made-up font list from queryLocalFonts.
  • Removed the tinyModelClient alias of tinyTitleClient and the OPENROUTER_BASE_URL alias in the Perplexity search provider; no user-visible change.
  • Removed the parseSha256Sidecar re-export from @veyyon/coding-agent/cli/update-cli; import it from @veyyon/natives/sha256-sidecar.

1.5.8

2026-10-16 latest View on GitHub ↗
Changed
  • A read refused because its lines are unchanged and still in context is now a tool error naming the earlier output's [path#TAG] header, rendered and sent to the model like any other failed read instead of as a read card holding the refusal text.
Fixed
  • A session started with --tools, and every spawned agent with a tool list, no longer logs a false "TTSR rule is scoped to a tool that does not exist" warning for each built-in rule scoped to a tool the session did not build.
Breaking
  • Model roles are removed: modelRoles, modelTags, cycleOrder, custom roles, the @role, pi/role and * selectors, the --smol and --slow flags, the VEYYON_SMOL_MODEL, VEYYON_SLOW_MODEL and VEYYON_PLAN_MODEL variables and the /settings Roles tab no longer exist; defaultModel holds the model chain a session starts on, and agent.helpers.<id> (advisor, plan, vision, commit, title, memory, autoThinking, unexpectedStop, speech, completion) sets the model and effort of each feature's own model call.
  • The /settings Roster page lists a Helpers section above the Subagents section, and a helper with no model or effort set runs the session's model and effort.
  • Ctrl+P cycles the scoped models, or every available model when none are scoped, instead of cycling roles.
  • The eval completion() helper runs the completion helper when model is omitted, the session's active model for "default", and otherwise reads model as a model selector.
  • retryFallbackChains keys are default, a model selector or a provider/* wildcard, matched in that order of specificity: exact selector, then wildcard, then default.
  • The @veyyon/coding-agent exports formatRoleChip, resolveRoleAssignments and retryFallbackChainsForRoles are removed, and resolveRetryFallbackRole is renamed resolveRetryFallbackKey.
Added
  • A config written before this release migrates on load: modelRoles.default (or interactive) becomes defaultModel, the advisor, plan, vision, commit, tiny and smol roles become the matching helpers' models with @role references expanded, mnemopi.llmMode: smol becomes online, and each dropped key is reported once in the log.
  • An extension provider's oauth.refreshToken(credentials, signal) receives an AbortSignal that aborts when the refresh is abandoned.
Changed
  • An agent with no model or effort set on its roster page or in its definition runs the spawning session's live model and effort instead of the default model role at medium effort; bundled scout, sonic and librarian no longer pin an effort, model: inherit on a lane overrides a definition's model, and /account status counts the session's provider as an agent provider.
  • Resuming or switching to a transcript reads its starting service tier through the same function a new session uses; no user-visible change.
  • The /settings model hub's "Default & fallbacks" view and Default Model row edit defaultModel, and clearing the Default Model row keeps a --model launch as the session's model.
  • Advisor messages name agent.helpers.advisor.model and the Agents → Roster → Helpers → Advisor page.
Fixed
  • An agent whose effort is inherited or configured runs at the level its own model accepts, and its badge shows that level, rather than the parent's level for a model that lacks it; a parent with no effort leaves the agent at its own default instead of none.
  • An agent that resolves no effort of its own shows the effort its session settles on in its badge, and the /settings agent roster shows an agent that runs the session's model as session model, which leaves room for a read-only source marker.
  • The Runs line on a /settings agent page reads the session's model and effort for an agent with neither set, and session effort instead of inherit effort when only the model is set.
  • The plan-approval slider no longer offers the plan model as a second choice when it is the default model at the same effort.
  • Mnemopi online memory runs the session's model when no memory helper model is set, instead of the persisted defaultModel.
  • The /settings Roster page shows eight rows before it scrolls and a shorter description, so its Enter to configure · Esc to go back footer stays on screen in a 40-row terminal.
  • A turn whose model no account's plan includes, and no fallback model can take over, ends with the provider's refusal instead of retrying it.
  • The notice for a move accounts.loadBalancing withheld names a plan that does not include the model as well as a spent quota window, and the failover notice states why the previous account could not serve.
Added
  • session.backgroundLimit (default 3, range 1-20) caps how many conversations run in the background at once, whether /new, RPC or ACP moved them there; a handoff past it stops the oldest background conversation and names it in the handoff message.
  • RPC new_session with background: true keeps a streaming session's turn running in the background and attaches the client to a new session, switch_session to a background conversation's transcript re-attaches the live session, get_background_sessions lists background conversations, cancel_background_session stops one, and the data.background handoff reports streaming: false for a session that moved with no turn running.
  • ACP _veyyon/sessions/background moves a streaming session to the background, where its pending session/prompt resolves when the turn ends and no session/update is sent; session/load or session/resume re-attaches it live, _veyyon/sessions/background/list lists background conversations, _veyyon/sessions/background/cancel stops one, and closing the connection stops every conversation it moved there.
  • /resume marks conversations running off-screen as running, and ctrl+x stops the selected one without resuming it.
  • A bash approval card offers Approve "<pattern>" for session, which allows later calls reporting the same pattern (git status * for git status -s) without prompting; a call with quoting, expansion, a pipe, a redirect, an environment variable, a working directory, or a guard flag offers no pattern row, and a program that runs another command (sudo, xargs, env) is granted only its exact command.
  • The lsp tool's incoming_calls and outgoing_calls actions list the functions that call the symbol at the cursor, or that it calls, with each call site, as the language server resolved them.
  • Composer predictions show the message you are likely to send next as dim text in the empty composer after each turn, inserted by Tab and sent only by Enter; composer.predictions.mode selects off (the default, which sends no request), chatgpt-pro, which requests a ChatGPT Codex prediction on GPT-6 Astra or GPT-6.1 Sol only with the token of a ChatGPT Pro plan Codex login, never a routed or API-key credential, may count against that account's usage limits, and otherwise sends nothing and shows as greyed out in /settings, or custom, which uses the first model with credentials in composer.predictions.model, chosen from any provider in the settings model picker; a stored composer.predictions.enabled: false loads as off.
  • A composer prediction ends with a dim · tab to accept hint showing the configured Tab key; the hint leaves with the suggestion when you type or press Tab, and a row too narrow for both drops the hint before it hides the suggestion.
  • read refuses a re-read whose every displayed row is still shown byte for byte under the same header by an earlier read result in context, returning a one-line pointer to that output instead of the content; no argument overrides the refusal, a pruned, shaken or compacted earlier result no longer counts, and no read is refused while an extension handles the context event.
  • The account card's signed-out login note offers f to forget it, which stops the card and /account from reporting a login you do not intend to sign back in.
Changed
  • A provider request carries a payload hook only while an extension handles before_provider_request, so a session with no such handler skips cloning and re-serializing every request body, which cut request building on a 300-turn transcript from 0.90 ms to 0.26 ms per request.
  • The lexical bracket scan behind read windows and edit previews, used for a source tree-sitter cannot parse, dispatches on character codes, skips a block comment with one search and scans each mode in its own function in utils/block-context.ts, so a lookup on a 9,815-line TypeScript file with a syntax error takes 0.85 ms instead of 7.74 ms, and on a 4,083-line .txt file 1.20 ms instead of 12.70 ms (median of 15 alternating rounds of 40 lookups); every boundary line is unchanged.
  • The status line fits its footline in one QuietRowFit pass in status-line/quiet-row.ts that keeps each half's joined text and width instead of re-joining the halves after every shed, so composing a default footline takes 999 ns instead of 1,454 ns at 160 columns and 9.46 µs instead of 12.15 µs at 60 columns, where it sheds zones (median of nine rounds of 20,000 calls); the drawn row is unchanged.
  • StatusLineComponent builds a frame's segment context through #locationContext, #gitFacts and contextGaugeReadings, and renderLocation and the model segment build their text in per-step functions; the drawn row and the git lookups it starts are unchanged.
  • The launch-facts recorder compares a recorded entry field by field instead of serializing both copies, so a status-row redraw that changes no recorded fact files in 338 ns instead of 1,119 ns, and a git status that finds the recorded tree in 528 ns instead of 1,313 ns (median of seven alternating processes of 450,000 calls).
  • The end-of-turn context check moved out of AgentSession into the TurnContextCheck session collaborator, and the starting service tier into session startup; no user-facing change.
  • The composer.predictions.mode row in /settings is named "Suggest Next Message" instead of repeating its "Composer Predictions" heading.
  • The retry ladder decides each attempt through #parkUsageLimit, #switchModel and #closeUnrecoverable in session/runtime/retry-runtime.ts; every retry, model or credential switch, wait and event is unchanged.
  • The settled edit card parses a hashline payload for its file only when neither the call nor the result names one, so re-rendering the card of a 2,000-line hashline edit takes 0.95 µs instead of 274 µs (median of seven rounds of 2,000 renders), and the call, result and multi-file cards are built in per-step functions in edit/edit-view.ts; every card is unchanged.
  • The /models hub maps a Roles-view key to its command through rolesCommandFor, routes each pointer report through its own wheel, motion and click step, and draws Roles-view and sidebar rows in per-row functions in modes/terminal/components/selectors/model-hub.ts; every frame, key and pointer action is unchanged.
  • The extension loader resolves each import of an extension's module graph through graphSpecifiers, resolveGraphEdge and resolveBareDependencyEntry in extensibility/plugins/legacy-pi-compat.ts; every module it hooks and every native-addon loader it rewrites is unchanged.
  • The lsp tool's rename_file action resolves its target, asks each server, merges their edits, applies them and notifies the servers in per-step functions in lsp/index.ts; every preview, applied edit, discarded overlap and notification is unchanged.
  • The patch parser reads each hunk in place instead of copying the rest of the diff for every hunk and every @@ line, so a 400-hunk patch parses in 0.71 ms instead of 1.78 ms (median of three runs of 15 alternating rounds), and it reads a hunk's header, nested @@ markers and body in per-step functions in edit/diff.ts; every parsed hunk and every parse error is unchanged.
  • The PDF converter reads a page's content stream through skipStringLiteral, tokenEnd and skipInlineImageData in export/markit/converters/pdf/extract.ts; the tokens of every stream that does not end in < are unchanged.
  • The PPTX converter reads its slide order, each slide and each slide's pictures and notes in per-step functions in export/markit/converters/pptx.ts, and the PPTX and XLSX converters read a part's relationships through readPartRelationships in export/markit/converters/opc-relationships.ts; a deck or workbook whose parts sit at their conventional names converts to the same markdown.
  • readPartRelationships resolves a root-level part's targets with the package root as their base without a separate root case; converted output is unchanged.
  • The launch result card reads each op's rows, the logs frame and the collapse cap in per-step functions in tools/shell/launch-view.ts, and a pty-backed logs card no longer scans the text it does not show, so a 1 MiB log renders in 0.0014 ms instead of 0.54 ms (median of 200 alternating rounds); every card is unchanged.
  • The emoji, GitHub reference, internal URL and prompt-action autocomplete providers declare their results as AutocompleteSuggestions; no user-visible change.
  • The assistant text reveal and the tool argument reveal run on one RevealFrameClock; both still tick at 30 frames per second only while text is held back.
  • The stdio, streamable-HTTP and SSE MCP transports answer a server-to-client request through one answerServerRequest in mcp/types; each answer is unchanged.
  • Tool result text joining and the web tool display projection use template literals and optional chains; their output is unchanged.
  • The custom tool file-extension list in discovery/helpers.ts carries a not-a-tool-name: marker, because its "bash" names an extension rather than the bash tool; no user-visible change.
  • createAgentSession reads the caller's telemetry config in two fewer statements; its behavior is unchanged.
  • The tools.intentTracing entry in system-prompt-builder/gate-registry.ts states the resolveIntentField resolver createAgentSession passes to the agent; no user-visible change.
  • /dump and /advisor dump raw write each message role through its own function in session/session-dump-format.ts; the transcript is unchanged.
  • The RPC server answers each command through its own entry of RPC_COMMAND_HANDLERS in modes/rpc/rpc-commands.ts, and builds its extension UI context in modes/rpc/rpc-ui-context.ts; every response is unchanged.
  • RPC set_host_tools checks that a host tool's parameters is an object through isRecord from @veyyon/utils; the accepted and rejected definitions are unchanged.
  • The task tool schedules its spawns through SpawnScheduler in task/spawn-scheduler.ts, runs each spawn through task/spawn-run.ts and builds its spawn advisories in task/spawn-advisory.ts; every result is unchanged.
  • The task tool validates a batch call and builds its approval lines in per-shape functions, SpawnScheduler admits a background spawn in its own step, and executeBash releases a stopped run and annotates an interrupted one in their own functions; every result is unchanged.
  • A launch builds no ICU time zone cache, number formatter or collator before the first frame: the terminal guard reads the local date from the C library, the tts tool description prints its character limit literally, and prompt templates sort variable names by code unit, so the source launch paints its first frame in 58.5 ms instead of 60.5 ms and its ready run peaks at 217.1 MiB RSS instead of 220.4 MiB (median of 30 and 11).
  • A launch builds no ICU time zone cache or grapheme segmenter: the logger, the session's tool signature and the date the session states to the model read local time from the C library, and the grapheme segmenter is built on first use, so the compiled binary reports ready in 180.3 ms instead of 188.0 ms and paints its first frame in 37.0 ms instead of 38.4 ms (median of 30), and 8 s after launch holds 249.5 MiB RSS instead of 254.3 MiB, 118.5 MiB anonymous memory instead of 119.7 MiB and a 260.4 MiB peak instead of 264.8 MiB (median of 10).
  • A launch evaluates yaml on the first settings file edit or secret declaration parse, @opentelemetry/api on the first traced span, and diff on the first diff it generates or renders, instead of with the module graph, so an RPC launch of the compiled binary evaluates 1,876 modules instead of 1,991 and holds 190.6 MiB RSS instead of 194.6 MiB, 93.4 MiB anonymous memory instead of 97.2 MiB and a 199.1 MiB peak instead of 203.7 MiB (median of 31).
  • A launch with secrets.enabled parses the bundled environment-keyword list with Bun's YAML parser and evaluates yaml only for a keyword file a user wrote, so an RPC launch of the compiled binary with secret obfuscation on holds 158.5 MiB RSS instead of 161.7 MiB, 74.9 MiB anonymous memory instead of 77.1 MiB and a 169.2 MiB peak instead of 172.8 MiB, on 210 ms of CPU instead of 220 ms (median of 31).
  • A session created without a telemetry config starts no OpenTelemetry span and evaluates no @opentelemetry/api module, instead of tracing every turn through the global tracer so that span text passed through the secret obfuscator, so an interactive session of the compiled binary spends 225 ms of CPU on turns two to six instead of 240 ms (median of 12) and holds 306,933 heap objects instead of 309,753 after four turns (median of seven); a session given a config still obfuscates secrets in its spans.
  • A launch evaluates arktype when the first schema is built instead of in the runtime warmup ahead of the session: shipped source imports arktype values from @veyyon/ai/utils/schema/arktype, the completion bridge and the custom command API's arktype member evaluate it on first use, and the warmup has no arktype stage, so the compiled binary names the model 183.6 ms after launch instead of 191.2 ms with createAllTools taking 7.4 ms instead of 13.7 ms (median of 15), and a session idle before its first turn holds 159.0 MiB RSS instead of 166.7 MiB (median of five); 40 s after a first turn it holds 178 MiB instead of 176 MiB with the same JS heap.
  • Loading a models config unchanged since its last accepted load evaluates no ArkType module, because the validator fingerprint reads the ArkType release from package metadata instead of the $ark registry that the eager arktype import installed.
  • A print, RPC or ACP session holds its at-rest context reading, which builds every active tool's schema, until its first prompt, as an interactive session already did, and the CLI's jitless ArkType setting is held until the first schema is built, so an RPC launch of the compiled binary with a provider key evaluates no ArkType module before its first turn: it evaluates 1,741 modules instead of 1,856, reports ready in 187.1 ms instead of 203.3 ms on 250 ms of CPU instead of 340 ms, and holds 170.9 MiB RSS instead of 190.2 MiB, 76.9 MiB anonymous memory instead of 93.6 MiB and a 184.2 MiB peak instead of 197.9 MiB (median of 31).
  • The presentation summary resolves the server-side compaction transport through @veyyon/agent-core/compaction/remote-compaction instead of @veyyon/ai/providers/openai-compaction, so loading it no longer evaluates the Codex client; no user-visible change.
  • A print, RPC or ACP launch evaluates no terminal renderer: main.ts loads the session picker only when --resume opens it, the launch command loads the launch card only when it paints, and the ask tool reads its card width from chrome/modal-geometry.ts instead of the modal shell, so main.ts reaches 1,467 modules instead of 1,582 and an RPC launch of the compiled binary reports ready in 221 ms instead of 228 ms with 190.6 MiB RSS instead of 196.7 MiB and a 205.3 MiB peak instead of 212.2 MiB (median of 20).
  • A streaming assistant message finds a Mermaid fence by jumping between runs of three backticks or tildes instead of splitting its whole text into lines on every delta, so a 108 KiB reply streamed in 24-character deltas spends 26 ms in message updates instead of 152 ms, and 735 ms instead of 892 ms with a 120-column render after each delta (median of three).
  • With smooth streaming on, an assistant message counts, slices and renders a provider delta on the next reveal frame instead of on every delta, and a delta reads only the blocks not yet visible to decide a layout repaint, so a 200,000-character reply streamed in 24-character deltas costs 780 ms of process CPU per turn instead of 940 ms (median of 15 turns).
  • With smooth streaming on, assistant text and streamed tool-call arguments reveal at the rate they arrive, held behind the arrivals by a lead sized to the gaps between them, instead of by an eighth of the waiting units every frame, so a reply arriving in 120-character batches every 400 ms reveals 8 to 11 characters a frame instead of 4 to 19, one arriving 4 characters every 200 ms reveals one character at a time instead of up to three, and one arriving 200 characters every 100 ms is shown 200 ms after its last delta instead of 1,000 ms (bench/reveal-pacing.bench.ts).
  • The transcript reports every leading row a re-rendered block left unchanged as stable instead of none of the block's rows, so the terminal engine re-reads a streamed reply from its first changed row instead of from the reply's top on every frame, and a 1,200,000-character reply streamed in 48-character deltas costs 2.69 s of process CPU per turn instead of 2.87 s (median of three), a 400,000-character reply 1.00 s instead of 1.05 s (median of five).
  • A streamed delta or tool event rebuilds the anchored todo board only when the agent's motion state, the terminal size or the theme changed since the board was last drawn, instead of on every event, so a 400,000-character reply streamed under a live three-phase board costs 1.18 s of process CPU per turn instead of 1.59 s (median of four), and the same reply with no board is unchanged.
  • A spinner step on a streaming write, edit or apply_patch card draws the card again only when a row it drew reads the frame, because a framed block reads the frame for its header and arrival row when it composes its rows and the card keeps its serialized arguments between builds, so a step of a 1,500-line streaming write costs 89 µs instead of 357 µs and of a streaming edit 15 µs instead of 230 µs (median of three), and a turn that streams a 1,500-line write in three deltas over 20 seconds costs 1.09 s of process CPU instead of 1.40 s (median of six).
  • A streaming tool call's argument reveal parses the call's JSON only when bytes arrive outside the string argument its decoder reads, and a tool card serializes a call's arguments when a block's input is read instead of on every revealed step, so revealing a 128 KB write parses 128 KB of JSON instead of 27 MB in 6 ms instead of 55 ms (median of seven), and a turn that streams a 1,500-line write costs 2.82 s of process CPU instead of 2.96 s (median of nine).
  • A rule with an ast-grep astCondition matches a streaming write or edit call again only after its source has grown by a quarter and by at least 1,024 characters, runs one match per call at a time with only the newest snapshot queued behind it, and always matches the completed call before its reminder is delivered, so a turn that streams a 1,500-line write under one such rule runs 14 matches over 756 KB of source instead of 3,814 over 233 MB, and costs 0.34 s of process CPU instead of 123.8 s with a 131 MiB RSS peak instead of 336 MiB.
  • The status line runs git status for its dirty marker when a turn ends, after a ! or % command and when HEAD changes, and otherwise once every 10 seconds, instead of on any render a second after its last lookup, so a 13-second streaming turn in a checkout of this repository spends 110 ms of CPU in child processes instead of 555 ms and 1.48 s of process CPU instead of 1.55 s (median of four).
  • A running agent's live preview appends each streamed delta to its output tail and derives its last eight lines when it reports progress, at most every 150 ms, instead of splitting the 8 KB tail on every delta once the agent has streamed 8 KB, so a turn that spawns one agent streaming a 400,000-character answer costs 760 ms of process CPU instead of 830 ms (median of nine turns).
  • On a terminal with the Kitty image protocol, a tool card finds the images of each streamed partial result in the result instead of building the card's views for it, so a bash card streaming a 50 KB build log costs 259 µs of CPU per drawn frame of four chunks instead of 779 µs, a test log 415 µs instead of 965 µs and a log of wrapped lines 208 µs instead of 373 µs (median of seven).
  • A tool card keeps the wrapped rows of each windowed section instead of only the last one drawn, and finds the lines a tail buffer moved up when it cut its front, so a streaming bash card wraps only the lines that arrived since the frame before: a frame of a 50 KB log of wrapped lines renders in 193 µs of CPU instead of 588 µs and of a test log in 388 µs instead of 441 µs (median of seven).
  • A command's 50 KB output tail drops the characters a chunk pushed out by walking them off its front instead of encoding the whole tail into a buffer and decoding it back, so keeping the tail of a streaming test log costs 19 µs of CPU per four chunks instead of 83 µs, of a build log 10 µs instead of 24 µs and of a log of wrapped lines 12 µs instead of 26 µs (median of seven).
  • A streaming bash card reads the progress-run shape of each output line by character code instead of with five regular expressions, reads the home directory once per os.homedir function instead of once per shortened span, no longer searches its already shortened output rows for the home directory, and computes the render context's output when a renderer reads it, so a frame of four chunks of a 50 KB build log costs 197 µs of CPU instead of 259 µs, of a test log 320 µs instead of 423 µs and of a log of wrapped lines 167 µs instead of 199 µs (median of nine).
  • A command's 50 KB output tail holds the chunks appended since its last cut concatenated after the kept front instead of joining both into a new string on every read, so a streaming bash card that receives 64 chunks per drawn frame costs 391 µs of CPU per frame of a build log instead of 766 µs, of a test log 407 µs instead of 942 µs and of a log of wrapped lines 387 µs instead of 582 µs (median of seven), and at four chunks per frame a build log costs 182 µs instead of 197 µs (median of nine).
  • An interactive session loads the /mcp, /ssh, /tan and /todo command controllers on the command's first run, the speech-to-text recorder and recogniser on the first push-to-talk, the DAP client on the first debug call, the emoji table on the first :name completion and the GitLab Duo Workflow client on its first turn, so an idle session evaluates 220 KiB less source and holds 303,171 heap objects instead of 314,768 and 55.10 MiB of heap and extra memory instead of 55.76 MiB, with banner time unchanged (median of seven).
  • An interactive session loads the /secret, /mcp and /ssh handlers, the /plugins manager, the /memory mm Hindsight helpers, the autoresearch run screen and launcher and the /review <pr> GitHub client on first use, so an idle session evaluates 34 fewer modules and 542 KiB less source and holds 295,277 heap objects instead of 299,223, 54.51 MiB of heap and extra memory instead of 54.98 MiB and 242 MiB RSS after collection instead of 245 MiB, with banner time unchanged (median of seven).
  • The compiled binary's legacy-extension table imports each retained host module's exports by name and builds a module's export record when an extension first imports it, instead of importing all 1,057 modules as namespaces, so the binary's chunks declare 27 module namespace objects instead of 1,031 and 1,395 export getters instead of 8,914, and an idle session holds 284,103 live objects instead of 325,240, 38.2 MiB of heap instead of 41.8 MiB and 246 MiB RSS after collection instead of 255 MiB (median of five).
  • An interactive session at rest arms no working-clock heartbeat, stops its loop watchdog after 10 quiet seconds and its idle trim after the trim, and samples its stack once a second, each until the next keystroke or frame, so the runtime's idle collector drops to its slow rate: the linux-x64 binary idle 90 seconds after launch records 131 thread wakeups in 60 seconds instead of 4,062 and spends 20 ms of CPU instead of 160 ms (median of three).
  • Capability discovery finds the repository root by checking each ancestor of the working directory for .git instead of listing and keeping every ancestor directory, and concurrent loads share one lookup; twenty concurrent lookups from a directory under a 1,700-entry directory take 0.31 ms instead of 2.50 ms and leave 32 KiB on the heap instead of 259 KiB, and from a home directory outside any repository take 0.30 ms instead of 1.85 ms and leave 33 KiB instead of 185 KiB (median of 15).
  • A session compares a SHA-256 digest of its tool names, descriptions and MCP server instructions to decide whether a tool change rebuilds the system prompt, instead of keeping the joined text, so each main session and live subagent holds 87,568 fewer bytes of heap once its tool set has been applied twice with the default tools.
  • On Linux the binary's embedded module pages are released on the first 5-second window under 5% CPU after work instead of after 30 quiet seconds, so an idle session 15 seconds after launch holds 190 MiB RSS instead of 270-286 MiB, and a session with turns 12 seconds apart holds 209-216 MiB after its second and third turns instead of 290-295 MiB, at unchanged turn latency.
  • The compiled binary holds one copy of Zod instead of two, because the schema converter no longer requires a CommonJS copy beside the one the package barrel hands to extensions; the binary is 2.3 MiB smaller and an idle session holds 3 MiB less RSS after collection (270 to 267 MiB, median of five).
  • The resolved model stage (resolved-models.json) stores each distinct compat record once and each model an index into that table, so on a profile with 5,646 models the file is 0.81 MB instead of 2.10 MB and a launch restores the model registry in 10.2 ms instead of 22.2 ms, with 4.5 MiB less RSS growth (median of 11); a stage written by an earlier release is rebuilt once.
  • 261 class members that read no instance state are module functions and constants instead of #private members; JSC copies every private name of a class into each function its body creates, so with the 73 moved in the other packages the binary shrinks by 815 KiB and an idle session holds 580 KiB less RSS after the idle trim (median of five).
  • The isolation runner loads the commit message generator, its model helper and the commit prompts on the first AI-written isolation commit instead of when the task tool loads, so an idle session holds 390 fewer live objects and 0.10 MiB less heap (median of five) and a source session evaluates 16 fewer modules.
  • The veyyon:// handler imports the embedded documentation index on its first resolve or completion instead of at launch, so a compiled binary no longer links the 1.4 MB documentation literal into the startup graph, and an idle session holds 0.8 MiB less RSS five seconds after launch (median of seven); the JS heap and the RSS after the idle trim are unchanged.
  • The Mermaid render cache imports lru-cache/raw like every other module, so a session evaluates one lru-cache build instead of two: the binary shrinks by 296 KiB and an idle session holds 537 fewer live objects (median of seven).
  • AgentSession runs its retry ladder through session/runtime/retry-runtime.ts and its secret redaction and display expansion through session/runtime/session-secrets.ts, and holds 404 private names instead of 487, so its compiled bytecode drops from 9,474 KiB to 7,646 KiB across the three modules and loading it from a --bytecode binary costs 18,660 KiB RSS instead of 21,424 KiB (median of nine).
  • The streaming reveal counts and slices each run of ASCII text one code unit per cluster and passes only the clusters that contain a non-ASCII character to Intl.Segmenter, with identical cluster boundaries, so revealing a 400,000-character reply costs 1.5 ms of grapheme work instead of 21.0 ms for ASCII prose and 11.1 ms instead of 21.3 ms for prose with CJK or emoji clusters (median of three).
  • The status line keeps the scratch-directory classification of the working directory until the working directory, the home directory or the temporary directory changes, instead of resolving each root with realpath on every render, so a status row renders in 9.2 µs instead of 21.9 µs warm (median of three).
  • AgentSession runs compaction through session/runtime/compaction-runtime.ts, compaction-summarizer.ts and compaction-recovery.ts, and the retry ladder's model switches through session/runtime/retry-fallback.ts, and holds 372 private names instead of 410, so the compiled bytecode of the session and its retry and compaction modules drops from 7,568 KiB to 6,926 KiB and loading them from a --bytecode binary costs 33,144 KiB RSS instead of 39,144 KiB (median of 15).
  • AgentSession runs the prewalk and plan-yolo model handoffs through session/runtime/model-handoff.ts and holds 362 private names instead of 372, so the session's compiled bytecode in a --bytecode binary drops from 6,712 KiB to 6,484 KiB and loading it costs 32,240 KiB RSS instead of 32,512 KiB (median of 15).
  • AgentSession runs plan mode, its approved-plan reference and its decision reminders through session/runtime/plan-mode-runtime.ts and holds 355 private names instead of 363, so the session's compiled bytecode drops from 6,153 KiB to 5,994 KiB and loading it from a --bytecode binary costs 32,068 KiB RSS instead of 32,180 KiB (median of 15).
  • AgentSession writes finished messages to the session log through session/runtime/message-persistence.ts and holds 340 private names instead of 354, so the compiled bytecode of the session and its message persistence drops from 6,012 KiB to 5,736 KiB and loading them from a --bytecode binary costs 31,652 KiB RSS instead of 31,852 KiB (median of 15).
  • AgentSession computes the context figure, the prompt snapshot of a run in flight and the history-rewrite usage anchor through session/runtime/context-accounting.ts and holds 335 private names instead of 340, so the compiled bytecode of the session and its context accounting drops from 5,676 KiB to 5,580 KiB and loading them from a --bytecode binary costs 31,388 KiB RSS instead of 31,512 KiB (median of 15).
  • AgentSession holds the memory backend's session state and delivers recalled memory through session/runtime/memory-context.ts and holds 325 private names instead of 335, so the compiled bytecode of the session and its memory context drops from 5,540 KiB to 5,381 KiB and loading them from a --bytecode binary costs 31,088 KiB RSS instead of 31,336 KiB (median of 15).
  • AgentSession routes requests under its provider session id and inherited prompt cache key, and closes provider transport sessions, through session/runtime/provider-sessions.ts and holds 313 private names instead of 325, so the compiled bytecode of the session and its provider sessions drops from 5,347 KiB to 5,175 KiB and loading them from a --bytecode binary costs 30,864 KiB RSS instead of 31,024 KiB (median of 15).
  • AgentSession runs the tool-call loop guard and the Gemini reasoning-header interrupt through session/runtime/loop-guards.ts and holds 306 private names instead of 313, so the compiled bytecode of the session and its loop guards drops from 5,044 KiB to 4,927 KiB and loading them from a --bytecode binary costs 30,592 KiB RSS instead of 30,744 KiB (median of 15).
  • AgentSession runs the empty-stop and unexpected-stop retry cycles through session/runtime/stop-retries.ts and holds 298 private names instead of 307, so the compiled bytecode of the session and its stop retries drops from 4,994 KiB to 4,838 KiB and loading them from a --bytecode binary costs 30,328 KiB RSS instead of 30,516 KiB (median of 15).
  • AgentSession records usage headers and turn cost, reads usage reports and saved resets, and runs Codex auto-redeem through session/runtime/provider-usage.ts and holds 295 private names instead of 298, so the compiled bytecode of the session and its provider usage drops from 4,795 KiB to 4,736 KiB and loading them from a --bytecode binary costs 30,160 KiB RSS instead of 30,256 KiB (median of 15).
  • AgentSession runs the stale-result and overflow prunes, image drops, shake and duplicate-result elision through session/runtime/history-rewrites.ts and holds 285 private names instead of 289, so the compiled bytecode of the session and its history rewrites drops from 4,686 KiB to 4,572 KiB and loading them from a --bytecode binary costs 29,976 KiB RSS instead of 30,188 KiB (median of 21).
  • AgentSession tracks the terminal yield through session/runtime/yield-tracker.ts and sends the rewind, verification and review reminders a settling turn owes through session/runtime/finalize-reminders.ts, and holds 276 private names instead of 285, so the compiled bytecode of the session and its yield and finalize modules drops from 4,460 KiB to 4,334 KiB and loading them from a --bytecode binary costs 29,720 KiB RSS instead of 29,888 KiB (median of 15).
  • AgentSession holds the --auto-approve flag, the /yolo bypass, the kept per-tool approval decisions and the ACP permission prompt with its standing answers in session/runtime/session-approvals.ts and holds 270 private names instead of 276, so the compiled bytecode of the session and its approvals drops from 4,303 KiB to 4,209 KiB and loading them from a --bytecode binary costs 29,604 KiB RSS instead of 29,676 KiB (median of 15).
  • AgentSession shapes every provider request, with its tool-call id map, the directory outbound paths render relative to and the bytes each request left out, through session/runtime/provider-wire.ts and holds 265 private names instead of 270, so the compiled bytecode of the session and its provider wire drops from 4,179 KiB to 4,139 KiB and loading them from a --bytecode binary costs 29,484 KiB RSS instead of 29,584 KiB (median of 15).
  • A compaction moves the payloads of the history it summarized out of memory once it is recorded, reading each entry back from the session file on first use.
  • An interactive session no longer loads the HTML export template, stylesheet, viewer script and tool renderers until /export runs: /share builds its snapshot from export/session-data.ts, cutting the idle heap and extra memory of the compiled binary from 104.8 MiB to 102.9 MiB.
  • The compiled binary keeps the embedded veyyon:// docs index off the heap until the first veyyon:// read, and with the bundled model catalog read from its embedded file on demand the idle heap and extra memory drop from 102.9 MiB to 95.9 MiB; dist/cli.js ships the catalog as dist/models.json.
  • The advisor renders its project-context prompt from the context files when it first starts instead of at session creation, and with the shared prompt copies and record-only entries read from the session file, a session holding 40 finished spawned agents after ten turns uses 150.6 MiB of heap and extra memory instead of 175.5 MiB and 479-484 MiB RSS instead of 488-497 MiB.
  • An interactive session loads the settings, model, account, session, tree and dashboard cards when the first one opens, so an idle session holds 546,422 heap objects instead of 551,514 and 304 MiB RSS after a full collection instead of 309 MiB, with peak RSS 332 MiB instead of 338 MiB (median of five).
  • The Mermaid ASCII render cache holds at most 512 Ki UTF-16 code units of source and render, least recently used first out, instead of every frame of every streamed diagram for the life of the session, so streaming 20 diagrams retains 4.3 MiB instead of 32.4 MiB.
  • A launch reads a models.yml the validator accepted before from accepted-models-config.json beside models.db while the file, its path and the validator are unchanged, instead of building the models-config schema, so an idle session holds 75.9 MiB of heap and extra memory instead of 81.9 MiB, 496,284 heap objects instead of 528,818 and 293 MiB RSS after a full collection instead of 303 MiB, and draws its banner in 0.243 s instead of 0.348 s (median of five warm launches).
  • The compiled binary precompiles every Handlebars prompt template at build time and revives it on first render instead of parsing and walking it, so a session builds its system prompt in 6.4 ms instead of 16.1 ms, createAgentSession takes 77.2 ms instead of 88.1 ms and the status line names the model 303 ms after launch instead of 315 ms (median of 11), and an idle session holds 71.5 MiB of heap and extra memory instead of 72.2 MiB (median of five).
  • The order a session's disposal runs in moved from createAgentSession to orderSessionDisposal in session/top-level-sessions.ts; no user-visible change.
  • The collab guest's ask dialog loop is split into one method per question kind and one per guest pick in extension-ui-controller.ts; no user-visible change.
  • A conversation /new left running is disposed once its turn ends and no background job it owns is running, releasing its browser tabs, eval kernels, advisor, spawned agents and roster entry and writing its session_exit record, instead of holding them until the process exits.
  • A session builds the advisor's tools on the advisor's first review or /advisor configure instead of at startup, so a session whose advisor never runs constructs no second copy of every built-in tool: the compiled binary's idle heap and extra memory drop from 58.8 MiB to 57.6 MiB across 578 loaded modules instead of 606, startup RSS from 334 MiB to 328.5 MiB, the first frame lands 13 ms sooner (median of six), and three turns spawning four agents each peak at 406-409 MiB RSS instead of 415-418 MiB.
  • The launch tool's output renderer loads @xterm/headless on its first screen replay, through the loader the interactive bash tool already used, so a session that replays no PTY screen no longer evaluates it at startup, saving 1.02 MiB of heap and 5.5 ms of module evaluation.
  • MCP servers stay connected until the last top-level session using them is disposed, so a conversation /new left running no longer orphans them at exit, and disposing one of several top-level sessions (a background conversation or an ACP session) no longer tears down the agent lifecycle or the title and embedding workers the others use.
  • ctrl+x in /resume and a /new past session.backgroundLimit cancel the stopped conversation's background jobs.
  • /new, /resume and a handoff abort a spawned agent of the previous conversation that is mid-turn before disposing it, deepest generation first, instead of disposing it with its bash, eval and advisor work still running.
  • A config with compaction.strategy: "off" beside compaction.enabled: true migrates to compaction.enabled: false, and compaction.enabled is the only setting that turns auto-compaction off.
  • The HTML export's session tree prints each tool call with the same label as the terminal tree, for every registered tool, instead of its own switch whose find and ls cases matched no registered tool and printed most calls as raw JSON; a newline in an op, action or name argument no longer breaks a tree row in either tree.
  • tools/fs/read.ts is split into per-concern read-*.ts modules with no change to the read tool's schema, output or exports.
  • The session retry loop, the async job manager, the launch broker and the Gemini web search provider take their exponential delay from exponentialBackoffDelay in @veyyon/utils; each loop's base, ceiling and jitter are unchanged.
  • The resolve pending row clamps its reason at 80 columns instead of 72, and the web_search and gh cards expand tabs in query and label text.
  • Production code strips ANSI through stripAnsi from @veyyon/utils; Bun.stripANSI is called only by that helper.
  • A session that renders its personality from a project .veyyon/personalities/<name>.md file raises a personality warning stating the file, and personality warnings reach the session's notice channel once per session instead of writing to stderr on every system prompt rebuild.
  • Restoring resolved-models.json at launch verifies a CRC-32 instead of a SHA-256 digest, cutting model registry construction from 22.86 ms to 20.69 ms (median of 21 interleaved launches), and the first launch after upgrading rebuilds the snapshot once.
  • The static model stage restored from resolved-models.json shares equal compat records the way a catalog build does, so a warm launch holds one record per distinct compat shape instead of one per model.
  • The model settings domain reads the thinking vocabulary from thinking/constants.ts instead of thinking/index.ts, so the launch card, editor and status row evaluate 355 modules instead of 358 before the composer accepts a keystroke, skipping 0.85 ms of module evaluation (median of nine).
  • A launch whose --model names its provider (provider/id, or an id with --provider) looks the model up among that provider's models instead of building all 4,499 catalog models first, skipping 48 ms of catalog construction, so a compiled --model provider/id launch draws its first frame in 310 ms instead of 333 ms (median of 13 interleaved launches).
  • The /debug CPU profile borrows the process's sampling profiler from the stall sampler and hands it back when the profile ends, instead of starting and stopping the profiler itself and leaving the stall sampler's profile stopped.
  • Assistant turns and tool cards create their image, conversion and rail-frame collections on first use and share one rail-frame table instead of each constructing its own, and a tool card's block producer holds its listeners in a shared empty array until one subscribes, so a rebuilt 26,806-entry transcript holds 2,547 Maps, 1 Set and 0 WeakMaps instead of 37,211, 29,120 and 7,858, retaining 64.1 MiB and 1.10M objects instead of 68.3 MiB and 1.17M.
  • A tool card builds its block on the frame that draws it instead of on each call, result, spinner start and stop handed to it, and reads its animation, freeze, displacement and repaint policies without building its views, so a rebuilt 81,240-row transcript builds each card once and its rebuild drops from 113.2 ms to 91.7 ms (median of five) with an unchanged first frame.
  • Assistant answer and thinking segments, custom and skill messages, summaries, collab prompts and hook dialogs paint their prose through one style object per theme colour instead of a new one per component, so a rebuilt component reuses the rows its predecessor rendered from the shared Markdown render cache, and a resumed 38 MB transcript draws its first frame in 218.3 MiB and 194 ms instead of 223.4 MiB and 219 ms (206.1 MiB and 183 ms with the Box row change).
  • A provider request's secret scan walks the payload with one frame per container that copies its keys or values only when the transform changes one, instead of queueing an event per key and value and re-measuring every unchanged string, and the literal-secret matcher steps its root state through a code-unit table and skips the second literal pass when no rule changed the text, cutting the scan of a 1,200-turn request with two configured secrets from 156.2 ms to 22.9 ms and an identity walk of it from 57.2 ms to 3.7 ms, with identical output across 400,000 generated payloads and 480,000 generated texts.
  • A rebuilt transcript's highlight prefetch holds each source until every card that draws it has read it, including sources the 256-entry highlight cache held when the prefetch ran, instead of dropping it for that cache on its first read, so a command run again or a rebuild after an earlier one no longer re-highlights evicted sources one at a time; a 13,470-entry transcript draws its first frame with 0 instead of 284 such highlights (512 on later rebuilds), from 214.5 ms to 200.2 ms.
  • A session reads its persisted MCP tool selection at construction and on each MCP tool refresh from the one entry that records it instead of rebuilding, deobfuscating and argot-expanding the branch's whole message list, cutting createAgentSession on a 26,806-entry resume from 40.2 ms to 35.4 ms.
  • A resumed read result numbers its card rows by scanning the result text in place instead of slicing and regex-matching each row, cutting SessionManager.open on a 76 MB session from 179.9 ms to 143.9 ms with identical cards across 400,000 generated result texts.
  • The CLI configures ArkType to validate by interpreted traversal instead of compiling a validator for every schema, cutting a compiled launch that opens a session from 372 ms to 316 ms and its heap by 11 MiB; a tool-argument rejection of a described closed set now reports the field's description followed by (accepted: …) instead of ArkType's list of the members.
  • The debug tool runs each action through its own handler in a table keyed by action instead of one 398-line switch, so an action added to the schema without a handler fails the type check; every action's text and details are unchanged across a gdb session.
  • The launch parser sets each value-less flag through the table the profile bootstrap reads as its list of value-less flags, instead of an if-chain that had to be kept in step with that list by hand, and splits a token's flag, extension, string, optional and positional handling into single-purpose functions instead of one 183-line loop; every result and refusal is identical across 300,000 generated argument lists.
  • The edit tool reads its mode descriptions from the tool prompt rows instead of the whole prompt registry, so a session that builds it no longer loads the commit-agentic, bench and prompt-registry modules it never renders, 9 fewer modules on a session start.
  • A session binds its own inline extensions (autoresearch and the custom tool bridge) without the api.pi package namespace, so a session with no author extension no longer loads the whole package barrel, cutting createAgentSession on a 13,470-message resume from 267 ms to 213 ms and its heap by about 5 MiB; an author's extension still receives api.pi.
  • A session checks whether its working directory is a project root once when it discovers the project, and again only when its cwd moves, instead of on every system prompt build, cutting a rebuild in the veyyon checkout from 1.16 ms to 0.23 ms and in a repository holding other projects from 75 ms to 0.25 ms with identical blocks.
  • A system prompt build looks up its inputs, names its tools, fills its template data, checks its eval and file overrides and assembles its blocks in single-purpose steps instead of one 520-line function, and reuses the paragraph split of a context file, rule or appended prompt it already compared instead of re-formatting the text for every comparison, cutting a rebuild with a 48 KB AGENTS.md from 1.56 ms to 1.23 ms with identical blocks.
  • init_experiment resolves its settings, writes the session, arms the runtime and builds its report in single-purpose steps instead of one 258-line execute, and reads the worktree status and path prefix concurrently when checking for harness changes, cutting a clean-branch init from 3.94 ms to 3.39 ms with an identical report, session row and tool set.
  • run_experiment and log_experiment read the worktree status and path prefix concurrently through one repository resolve instead of one after the other, cutting each read on a 400-file repository from 2.20 ms to 1.65 ms.
  • A spawned agent's session records no raw SSE frames, which only the displayed top-level session's debug viewer and report bundle read, so five turns spawning four agents that each stream 60,000 characters leave 84.9 MiB of heap and 36.6 MiB of extra memory instead of 95.5 MiB and 40.8 MiB, and peak at 576 MiB RSS instead of 597 MiB (mean of three).
  • The eval tool builds its parameter schema once per set of enabled languages and report_tool_issue once per tool list, instead of once per session, so building both for 3,000 sessions registers no ArkType node instead of 87,000 and retains 1.8 MiB instead of 9.5 MiB.
  • The lsp tool runs a file-bound action through one method that starts the server, opens the file and resolves the cursor, then one function per action (location lookups, references, hover, code actions, document symbols, rename, reload), instead of one 388-line method; no user-visible change.
  • The composer's submit runs its extension input handlers, collab guest routing, shell shortcuts, steering prompt and auto-title in single-purpose methods, and the streaming and between-turns paths share one steering prompt with draft restore instead of two copies; no user-visible change.
  • The bash tool runs a call's preparation, interception check, internal URL expansion, working directory resolution, spawn admission and its background, managed, client-terminal and local runs in single-purpose methods instead of one 480-line execute; a trivial command and a 20,000-line command run in unchanged time.
  • A rebuilt transcript highlights the code and diff context of every tool card in one parallel native batch before its first frame instead of one card at a time during it, cutting a resumed session's first frame from 497 ms to 357 ms, and from 14.2 s to 7.5 s with compacted history expanded, with byte-identical rows.
  • A tool card whose presentation changes with nothing subscribed builds its block when it is next read instead of at the change, so disposing a transcript no longer builds a sealed block for every card, cutting a transcript reset from 31.4 ms to 1.6 ms, and from 2,070 ms to 26.7 ms on a 64k-card transcript.
  • A collapsed tool card's progress-run folding keys each output line once instead of twice at every run boundary and skips the diagnostic lookup for a line whose first token has no progress shape, and home-path shortening returns a line that does not contain the home directory before running its pattern, cutting the two over 1.52M lines of 44,117 tool results from 207.8 ms to 141.9 ms and from 118.6 ms to 85.9 ms with identical rows.
  • veyyon session stats folds each entry through a reducer with one method per entry kind instead of one 340-line loop, cutting the report on a 104,969-entry session from 36.4 ms to 30.0 ms with an identical report.
  • The legacy agent settings migration runs as one step per retired area over a shared key reader, cutting a legacy-heavy config's migration from 15.0 µs to 10.9 µs per load; a current-format config is unchanged.
  • A compaction pass finds the entry it just wrote by the id the append returned instead of copying the session's entries and scanning them for its summary text, which cuts that step on a 238,086-entry session from 6.53 ms to 0.002 ms.
  • Automatic compaction runs its candidate loop, per-candidate retries, progress check and follow-up scheduling in single-purpose methods, and shares the hook offer and the write step with manual compaction; no user-visible change.
  • A tool card's display rebuild runs its call phase, result phase, custom and multi-file views, images and not-executed notice in single-purpose methods; resume render time on a 63,915-component transcript is unchanged.
  • The first-frame replay's description of its recorded environment states that a file-driven change such as a new defaultProfile is corrected by the row diff rather than by a file stat; no user-visible change.
  • A truncated read, search or run_experiment result records only its truncation counts in the session file, not a second copy of the kept text, so new results take less disk and memory and a resume parses less.
  • The lsp tool dispatches each workspace-scoped action (status, diagnostics, rename_file, capabilities, request, workspace symbols, workspace reload) to its own handler, and definition, type_definition and implementation share one lookup; no user-visible change.
  • A goal session records the token and time a tool call spends as a small goal_progress entry instead of a full copy of the goal, so the session file holds the objective once per goal change rather than once per tool call and a resume parses less.
  • A read whose displayed rows count up without a break records no per-row line-number list in the session file, since the card numbers those rows from the first line; a read whose rows jump or skip still records the list.
  • Goal records on a session branch parse through the shared isRecord guard instead of a local copy; no user-visible change.
  • A read result's session file line omits the card's copy of the file text when the result's own numbered rows or plain text rebuild it, and the session restores it on load, which cut the recorded read results in local sessions from 4.27 GB to 2.89 GB.
  • A tool start marker in the session file omits the start time and the argument summary the entry timestamp and the assistant message already hold, so the resume warning for an unanswered call reads the arguments from that message.
  • An edit result's session file line omits the post-edit file text when its pre-edit text and numbered diff rebuild it byte for byte, and the session restores it on load, which cuts the post-edit copies recorded across local sessions from 671.18 MB to 3.79 MB.
  • A search result's session file line omits the card's copy of the matched rows, the path list fileMatches already holds, and the wrapper's repeat of the sub-search's truncation counts when the rest of the line rebuilds them, and the session restores them on load, which cuts the recorded search details in local sessions from 331.33 MB to 248.91 MB.
  • An eval result's session file line omits each cell's output and the top-level status events when the result's text and the first cell hold them, and a job result's line omits each job's result and error text the result's text holds, and the session restores them on load, which cuts the recorded eval details in local sessions from 497.77 MB to 253.78 MB and the job details from 100.84 MB to 37.80 MB.
  • Session spend, which /session, get_session_stats and goal accounting read at every turn start, tool completion and agent end, tallies the history behind the compaction boundary once per boundary instead of on every read, which cuts a read on a 201,156-entry session from 29.25 ms to 0.92 ms.
  • A prune, shake, image drop, recovered retry marker, compaction tail elision or dead-end warning rewrites the session file only from the earliest entry it changed, instead of the whole file, which cut the persist step after a one-entry prune on a 376 MiB session from 1.2 s to 135 ms.
  • The session's advisors, their delivery routing and their interrupt latches run in a session collaborator, and advisor stats and overflow compaction in their own modules; no user-visible change.
  • Tool discovery state (the MCP and local selections, built-in names, default MCP selections and the search index), checkpoint state (the open checkpoint, its pending rewind report and the last completed rewind), user shell and Python runs, the work a turn schedules after prompt() returns and the IRC records a streaming turn has not yet taken run in session collaborators; no user-visible change.
  • With edit.streamingAbort on, the check on a streaming edit patch scans each diff line once instead of rescanning the whole diff on every delta, which cuts a turn streaming a 91 KiB, 2,000-line patch in 23,293 deltas from 7,852 ms to 212 ms, and from 124 ms to 64 ms with the setting off.
  • A streaming write or bash card highlights, numbers and wraps only the lines that arrived since its last frame instead of the whole source on every argument delta, which cuts drawing a 600-line write streamed in 754 frames from 27,125 ms to 376 ms and its last frame from 67.7 ms to 0.38 ms.
  • The per-agent run monitor (progress, abort and soft request-budget handling, usage totals and output capture) runs in task/run-monitor.ts, split out of task/executor.ts, with one handler per agent event; no user-visible change.
  • The status line's message fingerprint and usage-window reading run in per-role and per-window helpers; no user-visible change.
  • An agent's yield finalization, reminder ladder, session setup and teardown run in single-purpose helpers in task/executor.ts, and a child renders its own system prompt section once per run instead of on every system prompt rebuild, which saves 114.5 µs per rebuild for deep; no user-visible change.
  • The session's agent event handler routes each event type to its own method, records a finished assistant message as the settle's last message once before its first await instead of again after persistence, and splits post-run maintenance into yield settle, failure recovery and stop-time continuation passes; no user-visible change.
  • Text search resolves its scope, runs the native and virtual searches, pages the matches, and renders and fits its output in single-purpose functions; no user-visible change.
  • Raw settings migrations run as an ordered list of one function per retired setting family instead of one 600-line function; no user-visible change.
  • CLI startup resolves the launch model, prewalk and plan-yolo targets, settings overrides, the --fork/--resume session, the resume picker, extension flags, the initial prompt, the setup wizard, update notices and the mode handoff in single-purpose functions instead of functions of 272, 720, 229 and 102 lines; no user-visible change.
  • A session's secret runtime (the loader, the expansion lease, the reload queue, the unreadable-vault refusal and the tool-argument expansion) runs in SessionSecretRuntime in secrets/session-runtime.ts instead of in closures inside createAgentSession; no user-visible change.
  • Startup model and thinking-level selection (session restore, the settings default, deferred --model patterns, the fallback role chain, the first authenticated model and the metadata refresh) runs in StartupModelSelection in session/startup-model.ts instead of inline in createAgentSession; no user-visible change.
  • The edit tool's patch mode computes a hunk's replacements (the direct match, trimmed-context variants, fuzzy fallback and overlap checks) and re-indents a hunk's new lines (tab-to-space, space-to-tab and matched-line alignment) in single-purpose functions instead of functions of 341 and 215 lines; no user-visible change.
  • A session discovers its context files, workspace tree, repository context, skills, rules, watchdog files and advisor configs through one discoverProjectInputs path at startup and after a working-directory change, starts rules discovery with the other project scans instead of after the session context loads, and clears the 5-second workspace tree deadline timer when the scan finishes instead of leaving it armed.
  • A session's tool session (the file mutation counters, the active-tool set, the host notifier and the advisor's view) is built in session/tool-session.ts instead of inline in createAgentSession; no user-visible change.
  • A session's MCP startup (discovery, the deferred connect and the reactive tool, prompt and notification wiring) runs in session/factory-mcp.ts, and the project its system prompt renders, the re-discovery after a working-directory change and the appended-prompt assembly run in ProjectPromptInputs and helpers in session/prompt-inputs.ts, instead of inline in createAgentSession; no user-visible change.
  • The debug tool's load check stops probing PATH at the first debug adapter whose command resolves instead of resolving all 14 configured adapters, which it ran for the session's tools and again for the advisor's, cutting a bare session's construction from 7.30 ms to 5.02 ms on a host with gdb installed.
  • The advisor's toolset is built in session/tool-session.ts, and the context gauge a new session records at rest and the one the status row draws come from one measureContextGauge in config/compaction-strategy.ts, instead of a copy inline in createAgentSession; no user-visible change.
  • A tool card builds its display when a frame draws it instead of on every change, so a resume stops highlighting each rebuilt call only to replace it with its result, which cuts rebuilding and drawing a 4,712-block transcript from 999 ms to 603 ms.
  • The includeWorkspaceTree prompt gate states the project discovery scan that fixes it for the session instead of a read in sdk.ts that no longer exists; no user-visible change.
  • The read tool dispatches a path through single-purpose steps (internal URLs, archive, SQLite and PDF image members, directories, images, notebooks, converted documents, binary files, summaries, multi-range and single-range text), and a range read collects, truncates, formats and annotates its window in helpers of their own; no user-visible change.
  • A resumed read result builds its card text from its numbered rows when a card first draws it instead of when the session opens, a read group with previews off (the default) keeps no card text, and rewriting an unchanged resumed read writes back its stored tag without building the text, which cut the heap of a 38 MB, 1,280-read session after opening from 92.3 MiB to 67.6 MiB, its string cells from 915,658 to 106,754 and its open from 253 ms to 192 ms, its heap after the first frame from 171.1 MiB to 144.4 MiB, and its first whole-tail rewrite from 377 ms to 221 ms and 150.2 MiB to 120.5 MiB retained, with byte-identical file output.
  • A session's custom tools and tool registry are built in session/factory-tools.ts, its extensions and custom commands load in session/startup-extensions.ts, its Codex prewarm and language-server warmup start in session/startup-background.ts, its argot arm and start records run in session/startup-records.ts, its owned background-job manager is built in session/async-jobs.ts, and the secret lease each in-flight request was admitted under is tracked in SecretRequestLeases in secrets/request-leases.ts, instead of inline in createAgentSession; no user-visible change.
  • The transcript's frame walk places each block's rows, seals committed snapshots and carries the live-region boundary in single-purpose steps over one reused cursor, and a tool card's display resolves its policies in presentation/tool-execution.ts and its renderer views and generic fallback in presentation/tool-card-views.ts, which cuts a fully reused frame of a 20,000-block transcript from 0.407 ms to 0.306 ms and a committed-replay frame from 0.457 ms to 0.303 ms with identical rows.
  • A settings-screen change applies its live effect through a table keyed by setting path in modes/terminal/controllers/setting-effects.ts instead of a 261-line switch, and the effects for names no setting has (autoCompact, theme, thinkingLevel and fourteen statusLine* spellings) and for settings with no screen row are gone; no user-visible change beyond the two fixes below.
  • A URL read sends its alternate-markdown, .md suffix, content-negotiation, alternate-feed and llms.txt probes concurrently, keeps the highest-priority rendition that answers and aborts the rest, and downloads a document's bytes once for the converter and the binary notice, cutting an article read from 93.3 ms to 63.1 ms and a script-shell page read from 216.6 ms to 93.7 ms.
  • The same-file reload comparison lives in session/provider-replay-projection.ts and compares each message's replayed fields without deep-copying them first, and skips a message both lists share, cutting it on a 433-message branch from 0.47 ms to 0.13 ms.
  • A running veyyon process that has used under 5% CPU in each 5-second window for 30 seconds discards its compiled JavaScript code and returns free memory to the OS, cutting an interactive session's resident memory at idle from 361 MiB to 332 MiB after eight turns and from 325 MiB to 303 MiB after startup, at a cost of 37 ms on the next turn.
  • An idle veyyon binary on Linux also unmaps the clean resident pages of its embedded modules when it discards its compiled code, cutting an interactive session's resident memory at idle from 317 MiB to 226 MiB after startup and from 342 MiB to 251 MiB after eight turns, with no measurable change to the next turn.
  • The settings screen reads a record setting's override layers with isRecord from @veyyon/utils; no user-visible change.
  • A process keeps the CPU model its system prompt shows without the /proc/cpuinfo text it was read from, releasing 61 KB of heap for the process life on a 32-thread host.
  • A kept-alive spawned agent's session options and a parked agent's reviver hold only the run fields they read, and each progress snapshot copies its tail lines out of the streamed text, so a finished agent no longer keeps its run's assistant text, the spawning tool call's progress callback and abort signal, or the streamed message a tail line was cut from; after five turns spawning four agents each and the 20 agents parked, heap and extra memory is 61.2 MiB instead of 64.2 MiB and live objects 547,990 instead of 549,635 (median of three).
  • The CPU model the system prompt shows is copied through detachedString from @veyyon/utils; no user-visible change.
  • A session whose tools, commands and extensions are all first-party no longer evaluates Zod: the search tool declares its parameters with ArkType, which lists its type values as files, structure, text, and the command, custom-tool and extension loaders read api.zod from the package namespace only for author code, cutting the idle heap and extra memory of the compiled binary from 83.7 MiB to 81.9 MiB and live objects from 546,500 to 528,700 (median of five).
  • Built-in tools build their ArkType parameter schemas on the first read of parameters instead of when their modules load, and session creation reads no inactive tool's parameters for discovery keys or prompt metadata, so a default session registers 834 schema nodes instead of 1,841 and the compiled binary's idle heap and extra memory drop from 75.9 MiB to 72.2 MiB, live objects from 496,323 to 457,494 and RSS after a full collection from 292 MiB to 289 MiB (median of five).
  • The compiled binary renders the default system prompt from its precompiled statement templates with prompt.renderSequence instead of parsing and compiling their joined text at every session start, and evaluates no Handlebars compiler module, so buildSystemPrompt takes 3.0 ms instead of 6.0 ms and createAgentSession 44.4 ms instead of 47.6 ms (median of 13), and the idle heap and extra memory drop from 71.7 MiB to 70.1 MiB, live objects from 458,538 to 455,377 and RSS after a full collection from 288 MiB to 285 MiB (median of seven).
  • The bundled rules are parsed once per process and every session's TTSR manager shares one compiled set of their conditions, scopes and globs, so loading and registering them takes 0.02 ms instead of 0.5 ms per session and a live spawned session retains 245.6 KiB of heap instead of 277.0 KiB and 1,723 objects instead of 2,606 (median of three runs over 20 sessions).
  • ArkType schema nodes create their assert, pipe, rootApply and allows closures on first read instead of at construction, so a node traversed only as part of a larger schema retains half the function cells and the compiled binary's idle heap drops from 48.7 MiB to 47.8 MiB and live objects from 438,905 to 422,703 (median of seven).
  • An ArkType schema node without metadata serializes itself once and holds that JSON object and string as both its inner and its full serialization, so the compiled binary's idle heap and extra memory drop by 271 KiB, from 68,550 KiB to 68,279 KiB, and live objects from 422,696 to 421,155 (median of five).
  • A rebuilt transcript allocates no closure per assistant turn or tool card beyond the function a tool's view draws through: rail frames, framed blocks and cached views are class instances, cards, producers and tool-result images report through listener objects, inline images share one theme, and an assistant turn repaints through its host, so a resumed 600-turn session holds 120,613 KiB of heap and extra memory instead of 123,380 KiB, 639,759 live objects instead of 702,893 and 385 MiB RSS instead of 389 MiB (median of five).
  • A tool card computes no edit preview once its call has a final result, and a transcript rebuilt from history sets each card's result before any preview starts, so resuming a 600-turn session whose 521 edits target files that still exist no longer reads and diffs each of them: the session settles in 625 ms instead of 1,773 ms with 1.50 s of CPU instead of 3.83 s (median of nine), and holds 116.5 MiB of heap and extra memory instead of 129.1 MiB and 383 MiB RSS after a full collection instead of 531 MiB (median of five).
  • The at-rest reading a top-level session files for the next launch is measured by measureAtRestLaunch in session/startup-records.ts instead of inline in createAgentSession; no user-visible change.
  • createAgentSession runs as the ordered steps of SessionStartup in sdk.ts, with the agent identity and provider prompt-cache key in session/startup-identity.ts, the per-request hooks in session/startup-request-hooks.ts, the credential-disabled relay in session/startup-credential-relay.ts, the tool-discovery mode in session/tool-discovery.ts, and the providers.openaiWebsockets reading in config/openai-websockets-mode.ts; no user-visible change.
  • An MCP prompt command parses its key=value arguments and joins its reply's text through two helpers in session/factory-mcp.ts instead of inline; its output is unchanged.
  • Codex web search folds its response stream into the answer, sources, model and usage through CodexAnswerCollector in tools/web/search/providers/codex-answer.ts, and maps a failed response through the shared provider HTTP error classifier; its results and errors are unchanged.
  • The 98 bundled themes and the two root themes are embedded as files and the one a run asks for is read and parsed on demand, instead of every theme's JSON text held as a module string, so the binary shrinks by 240 KiB and an idle session holds 47.29 MiB of heap and extra memory instead of 47.76 MiB and 134.3 MiB of anonymous resident memory instead of 136.2 MiB (median of eight).
  • A context file in which no @ import resolves is kept as the string discovery read instead of a copy rebuilt from its segments, so a session in a project whose AGENTS.md is 48,191 characters holds 94 KiB of whole-text copies of it instead of 282 KiB.
  • An ArkType union in a jitless scope, as the CLI configures every scope, builds its discriminant, which only a compiled validator reads, on first read instead of at construction, and a union in any scope serializes it on first read, so a session start discriminates no union instead of 275, the compiled binary's warm:arktype startup phase takes 25.3 ms instead of 27.6 ms (median of 25), and an idle session holds 36.09 MiB of heap and extra memory instead of 36.38 MiB and 395,674 live objects instead of 398,201 (median of five).
  • An interactive launch takes the session's at-rest context reading, which builds every active tool's schema, once the frame that draws the first keystroke in the composer is committed or when a prompt is submitted, instead of inside session creation, and the status row draws the resting gauge the last launch recorded until then, so a cold createAgentSession takes 104.5 ms instead of 144.2 ms (median of 16) and an idle session holds 41.37 MiB of heap instead of 44.76 MiB, 335,831 live objects instead of 370,870 and 123.8 MiB of anonymous resident memory instead of 133.3 MiB (median of five).
  • ArkType builds each string.* keyword (string.email, string.url, string.date.iso and 50 more) when a schema first names it instead of when ArkType loads, so loading ArkType in the jitless CLI takes 11.5 ms instead of 21.3 ms and 2.0 MiB of heap instead of 3.9 MiB (median of 21), and an idle session holds 39.58 MiB of heap instead of 41.37 MiB, 314,847 live objects instead of 335,846 and 119.3 MiB of anonymous resident memory instead of 123.6 MiB (median of four).
  • A resumed session at rest states its context gauge, and the /context and /compact figures of the slash popup, with the non-message size its last response recorded in place of a measurement, instead of building every active tool's schema to measure it, so resuming a 600-turn session draws the model in 221.6 ms instead of 246.2 ms and spends 1.14 s of CPU instead of 1.20 s (median of 11), and the session at rest holds 70.8 MiB of heap instead of 73.8 MiB, 521,322 live objects instead of 553,019 and 348 MiB of resident memory after collection instead of 384 MiB (median of five).
  • The launch-facts record is defined in config/launch-facts.ts instead of modes/launch-facts.ts, so the session reads and writes it without importing the terminal modes; no user-visible change.
  • The launch-facts record clamps its context percentages with clamp from @veyyon/utils instead of a local helper; no user-visible change.
  • The in-flight prompt count and macOS power assertion, extension event forwarding, the title refresh on a re-plan, and working-directory re-scoping run in four session collaborators instead of as AgentSession members, which cuts the class from 265 private names to 186, so an idle session holds 10.67 MiB of private-name tables instead of 13.22 MiB and 96.3 MiB of anonymous resident memory instead of 102.0 MiB (mean of two).
  • The edit tool's write path, with the LSP writethrough and the ACP filesystem bridge, is defined in edit/modes/patch-execute.ts, which the streaming edit preview does not import, so the launch module graph holds 21 fewer modules (301 KiB of source) and a one-turn -p run takes 287.2 ms instead of 293.0 ms with 229.2 MiB peak RSS instead of 231.3 MiB (median of 61).
  • A tool execution block's input serialization is defined in presentation/tool-input.ts instead of presentation/tool-execution.ts; no user-visible change.
  • A session takes the Anthropic fast-mode reset, the Claude device id, the Codex transport details and compaction context, and the Gemini thought-signature accounting from leaf modules of @veyyon/ai, and loads the Codex client inside the background websocket prewarm, so the Anthropic, Codex and Google provider clients load on a session's first request to them instead of at launch: the launch module graph holds 21 fewer modules (691 KiB of source), and an idle session holds 251,352 live objects instead of 254,549, 50.12 MiB of heap and extra memory instead of 50.48 MiB and 234 MiB RSS after collection instead of 237 MiB (median of seven).
  • A turn reads the session's spend only while a goal is charged for it, and the spend tally reads each summarized assistant turn and task result back from the session file without keeping it in memory, so the first turn after resuming a 154 MiB, 53,819-entry synthetic session takes 250 ms of CPU instead of 490 ms and peaks at 354 MiB RSS instead of 458 MiB, and /session on it settles at 237 MiB RSS instead of 282 MiB (median of 3 alternating runs).
  • A streamed chunk's frame sizes the home anchor's fills from the rows it keeps for the status rows, the composer and its footline instead of rendering them again, so the first streamed answer of a session (4,000 characters at 400 characters a second in 8-character deltas) takes 285 ms of main-thread CPU instead of 353 ms and 555 ms of process CPU instead of 680 ms (median of 6 alternating runs); a turn whose transcript already fills the screen is unchanged.
  • A running session holds the text a read card, an eval cell output or a job result repeats from its tool result's content once, as a resumed session does, instead of a second copy in the result's details, so a 120-turn session that reads a distinct 8 KB file each turn holds 590 KiB of file text instead of 871 KiB and 27.45 MiB of JS heap after collection instead of 27.69 MiB (three runs each).
  • A session checks whether a message is already on its branch against a sorted typed array of the branch's message timestamps, comparing persistence keys only for entries with the same timestamp, instead of a Set of every message's persistence key, so after two turns on a resumed 600-turn synthetic session the persistence collaborator retains 688 bytes of heap and a 43 KiB typed array instead of 516 KiB of heap, and a 5,000-message branch's index adds 125 KiB to the heap instead of 1,182 KiB.
  • The profile, ~/.agent and extension-package providers load commands/, rules/, prompts/ and tools/ through one shared loader per layout, and an unreadable tools/ directory in the profile reports a discovery warning, as the other three layouts already did.
  • An extension tool call resolves its approval policy, raises its approval card, sends tool_call and rewrites its result in four steps instead of one 350-line method; approval, vetting and results are unchanged.
  • The secret obfuscator validates its registry, installs plain and regex entries, builds regex replacements and restores placeholders in single-purpose methods, and its message, content and provider-context walks share one identity-preserving map; no user-visible change.
  • Structure search resolves its scope, merges overlapping targets, groups matches, renders each file, lays out the result and fits it to the inline budget in single-purpose functions instead of one 280-line closure; no user-visible change.
  • executeBash plans the command, joins the CPU budget, leases the persistent shell, arms the abort and deadline, and settles the result in single-purpose steps instead of one 270-line function; a builtin command runs in 0.08 ms and a 20,000-line seq in 0.9 ms per call, unchanged.
  • The task card appends every agent, review, finding and nested-call row into one list instead of building and spreading a list per section; its rows are unchanged across a 6,000-card corpus, and drawing that corpus takes 1115 ms instead of 1305 ms.
  • A streamed delta sets the working message once, from the last tool call that states an intent, instead of once per call, and asks no earlier call's tool to derive an intent, so 4,000 deltas over six tool calls set it 340 times instead of 24,000 and a message update takes 4.5 µs instead of 5.3 µs (median of seven).
  • The terminal event controller starts a message, ends a message, settles a tool result and ends a retry in single-purpose steps, and a user message's local-echo signature is defined once in transcript-composer.ts; the transcript is unchanged.
  • /usage groups reports by provider, lists saved resets, collects limit rows and draws each row in single-purpose functions instead of one 180-line function; its output is unchanged across a 20,000-case corpus.
  • /session builds its provider, message, token, cost, language-server and MCP sections in single-purpose functions; its output is unchanged across a 20,000-case corpus.
  • /share runs the custom share script and the default share in separate steps, /memory runs each subcommand in its own method, /move checks and creates its target in one step, and /compact merges its instructions and mode in one function; no user-visible change.
  • /mcp runs its Smithery search, deploy and login steps from mcp-smithery-commands.ts, its OAuth login and reauthorization from mcp-oauth-login.ts, its list, resource, prompt and notification reports from mcp-server-reports.ts and its messages from mcp-command-output.ts, and MCPOAuthFlow.storedCredential builds the credential row a login stores; its output is unchanged across a 20,000-case corpus.
  • The composer's Esc handling, submission, /queue dispatch, follow-up and image-path paste run in single-purpose methods, its key listeners install once per editor, and the tiny-title download row is defined in tiny-title-download-row.ts; no user-visible change.
  • A session switch or reload captures its rollback, enters the target transcript, loads it and rolls back a failure in single-purpose steps instead of one 269-line method, and reads the branch once to restore the thinking selector and service tier; behavior is unchanged across 11,160 generated switch and injected-fault cases.
  • The end-of-turn context check recovers an overflow, retries an overflow stamped with the pre-promotion model, recovers a length stop and runs threshold maintenance in separate methods instead of one 231-line method; no user-visible change.
Fixed
  • A read of a missing artifact:// id, a missing agent:// output or a missing PDF image member lists at most 20 of what exists followed by a count of the rest, instead of every id in every session, which printed thousands of artifact ids into one tool result in a long session.
  • A transcript rebuild, such as after a compaction, releases the tool cards and assistant segments it removed from the screen and the arguments of calls that settled, instead of holding every card a long agent run ever drew, so a 900-second soak of tool turns and compactions ends at 987 MB RSS instead of 1,512 MB with 481,732 heap objects instead of 945,900.
  • A collapsed edit card whose change lines alone pass 40 draws 40 diff rows and counts the rest as hidden, instead of drawing every line of the run that crossed the budget (a 600-line replacement drew 603 rows) or overflowing the call stack on a run of a million lines, and cutting the diff and counting its lines no longer copy each run or re-split the kept text, so cutting a 5,600-line, 400-hunk diff takes 355 µs instead of 544 µs and counting it 56 µs instead of 190 µs (median of nine rounds of 300).
  • A session switch, /resume, reload, /new or /drop that fails part-way, such as on a disk error while the outgoing transcript is flushed, keeps the session listening, so later turns reach the screen and the session file instead of running unseen and unsaved.
  • Kimi web search with a kimi.ai credential posts to api.kimi.ai instead of api.kimi.com, and the Kimi API Format options describe the API each one uses instead of naming a host.
  • Reading a PowerPoint deck shows each slide's notes under that slide instead of under the slide whose number matches the notes part, so notes added to a later slide first no longer appear under an earlier slide, and it reads a slide named by an absolute or percent-encoded target, and the pictures and notes of a slide part not named ppt/slides/slideN.xml, instead of dropping them.
  • Writing into a ZIP or tar archive that holds a member named __proto__, or extracting or reading every member of such a ZIP, keeps that member under its own name instead of dropping it or listing its bytes as members 0, 1, ..., and converting an EPUB or XLSX whose reference names an absent member called toString or another Object.prototype property skips the reference instead of failing.
  • Reading a PDF, or attaching one with @, returns when a page's content stream ends in <, instead of never returning.
  • Escape or abort_retry that lands while an unreplayable tool batch's continuation is being announced cancels the continuation, instead of the session sleeping out the wait, re-requesting the turn and reporting a recovery that was cancelled.
  • In a linked worktree named after its branch, the status line's path segment and the launch card keep the worktree directory (monorepo/topic) on a row with a pr segment and no git segment, or with segmentOptions.git.showBranch: false, instead of showing monorepo with neither the worktree nor its branch on the row.
  • The launch card draws the dirty marker (*) and the effort the last launch's status line settled on when that row had no context gauge, instead of drawing a dirty branch clean or an older effort until the session mounted; a row that follows another repository no longer records that repository's tree status under the project.
  • A TTSR interrupt retries the turn it aborted when the aborted turn settles with a message whose timestamp differs from the partial it streamed, instead of dropping the injection and releasing the resume gate with no continuation.
  • An expanded task card draws an agent's Output heading once when the agent returned {}, [] or an empty JSON value, and when a running agent's live output is one, instead of twice.
  • An agent with several approval cards open at once stays marked as waiting until its last card closes, instead of showing as working once its first card is answered, so the agent dashboard draws it as blocked and the runtime budget excludes the operator's reading time across the whole span, counted once.
  • A non-recursive discovery scan reads only the files directly inside its directory, so a helper module under tools/<name>/ no longer registers as a custom tool named for its file, and a file below commands/, rules/, prompts/ or instructions/ (rules/archive/old.md) no longer loads as a command, rule, prompt or instruction.
  • VEYYON_TIMING=x on an interactive launch stops the launch card's terminal before it prints, so the startup timing tree reaches stderr instead of only the log file, and the startup bench's ready:load and ready:boot arms report samples again.
  • A tool card in a rebuilt transcript serializes its call's arguments through one accessor shared by every block instead of a getter allocated per block, so a drawn tool turn keeps one closure and two scopes instead of two and four (bash: 0.9 closures and 1.85 scopes per turn instead of 1.9 and 3.85).
  • The terminal components barrel exports HOOK_EDITOR_TEXT_PAD_COLS, computeModalDims, sizingForArea and the other modal sizing names again, which it dropped when they moved to chrome/modal-geometry.ts.
  • The compiled binary loads a legacy extension that imports @mariozechner/pi-coding-agent, @earendil-works/pi-tui, @veyyon/ai or another shimmed package root, or a pi- subpath such as @mariozechner/pi-ai/oauth, instead of failing it with no bundled module registered for @veyyon/pi-coding-agent or Cannot find package '@mariozechner/pi-ai'.
  • The slash popup a launch's first / opens states /context and /compact without a context figure while the session is at rest, instead of measuring it and building every active tool's schema inside the keystroke, so the first / echoes in 5.5 ms instead of 27.2 ms and its popup settles in 38.1 ms instead of 59.5 ms (median of 11).
  • A read card rebuilt on resume drops the file text it was built from once its card text exists, and a prune that replaces the read's result drops it too, so a resumed 600-turn session whose overflow prune superseded 398 reads holds 49.6 MiB of heap instead of 54.7 MiB and 244 MiB RSS after collection instead of 248-249 MiB (two runs each, 40-second settle).
  • Dismissing the welcome card on a resumed session reports the card's rows as dropped from native scrollback instead of unmounting them unreported, so the first turn no longer replays the transcript: on a resumed 600-turn session its peak resident memory is 492 MiB instead of 527 MiB, its CPU time 0.48 s instead of 0.68 s, and its largest erasing frame 57 rows instead of 31,635 (median of five).
  • Resuming a session or rebuilding the transcript puts its earlier prompts in the up/down history ring without writing them to the prompt history database again, so resuming a 600-turn session adds 0 rows to history.db instead of 600 (median of seven).
  • A spawned agent no longer records its context gauge, model name and thinking level into the launch facts, so the next launch's card and status row state the top-level session's reading instead of the last spawned agent's, and a spawned agent no longer builds every tool's wire schema at creation to measure that gauge: the first one created in a fresh process takes 119 ms instead of 181 ms (median of five).
  • A models.yml at a path other than the default one, rejected by a check after the schema (a missing apiKey, a retired key), names the file that was read in its error instead of the default models.yml path.
  • A language server error reaches the lsp tool's result once as LSP error: <message> instead of LSP error: LSP error: <message>, and rename_file drops a server that answers workspace/willRenameFiles with the JSON-RPC MethodNotFound code whatever its message says, instead of listing it as a server note.
  • A settings file with defaultThinkingLevel: off starts sessions with thinking off instead of at high.
  • A retry.fallbackChains entry that is not a string is skipped with a config warning and the next entry is tried, instead of leaving the failed turn's prompt waiting forever on a retry that threw.
  • A failure inside retry recovery, such as a locked auth database, a credential helper that exits with an error, or a full disk while the model switch is recorded, ends the retry with an auto_retry_end failure naming it, instead of leaving the prompt waiting forever.
  • A usage-limit or rate-limit error whose message states a zero or elapsed retry window parks the credential for the computed backoff instead of for no time, and the session retry loop, the auth gateway and the advisor read a retry window from an error message through one parser.
  • A same-file reload that changes a bash turn's kill signal or a user turn's synthetic, steering, attribution or demoted-reasoning marks now closes the live provider sessions, instead of continuing a provider chain whose history no longer matches the transcript.
  • A models.yml compat block type-checks every key of the compat contracts, so a wrong value such as thinkingKeep: last fails validation naming the key instead of going to the provider verbatim; thinkingKeep, reasoningDisableMode, toolSchemaFlavor, the Anthropic flags and trustExplicitThinkingOnly are documented overrides.
  • Disposing a session waits up to 1 second for its aborted agent loop to finish before it releases async jobs, eval kernels, browser tabs and the transcript, so a tool that ignores its abort signal no longer races the teardown of what it is using.
  • A record-valued settings row (model roles, default effort, provider limits, agents, fallback chains, per-provider retry, tool approval) opens when a runtime override or --config file sets one of its entries; the overridden entries show their source and are read-only, and edits to the other entries write only the profile's values.
  • The Default Model row states which layer supplies the active model at the start of its description, so the one-line footer no longer truncates it away.
  • A tool card's header and section labels are clipped at the card's width instead of running past its edge, so an ssh host or a write path with no break in it no longer folds onto the next terminal line, and no card row exceeds a terminal narrower than the rail and its indent.
  • An invalid enum value in config.yml falls back to the setting's default for every enum setting, not only edit.afterEdit.
  • The pending previews of ssh, browser, read, debug, bash, github, lsp and 30 other tools update a string argument as it streams instead of when the JSON object closes, and the live preview matches the rebuilt transcript.
  • The session /new starts while a response is still streaming, and every other top-level session in the process, runs background bash, the job tool and daemon exit watches on a job manager of its own, and a job reports to the conversation that started it or spawned the agent that started it instead of being refused.
  • The Claude Code capture helper moved from src/cli/claude-trace-cli.ts to scripts/claude-trace-capture.ts beside bun run claude:trace, its only caller; it was never a veyyon subcommand and no longer ships in the package source.
  • statusLine.segmentOptions.model.roomy: false joins the model and its thinking level with the dot separator in the composer footline instead of being overridden to the Model @level form.
  • The plan review overlay repaints when a plan is swapped in or an external-editor annotation is committed, instead of waiting for the next keystroke.
  • An agent spawned by another agent, and an agent revived from its transcript, reports its start, progress and finish on the top-level session's event bus, so the agent dashboard, the RPC agent stream and collab guests show agents at depth 2 and below instead of nothing.
  • --profile <name> --resume <id> for a session another profile wrote forks it into <name> at its recorded working directory and leaves the source unchanged, instead of appending to the other profile's transcript under <name>'s settings and credentials.
  • /resume <id> for a session another profile wrote relaunches Veyyon in that profile instead of opening the transcript under the running profile.
  • veyyon --resume <id> naming a subagent transcript nested inside a session's directory activates the profile that wrote it instead of forking it into the profile the launch started in.
  • veyyon --fork <id> for a session another profile wrote activates that profile and writes the fork under it, instead of copying the session into the profile the launch started in; --profile <name> --fork <id> still forks it into <name>.
  • A named profile with no hindsight.bankId set retains into and recalls from the veyyon-<profile> Hindsight bank instead of the veyyon bank every profile shared; set hindsight.bankId: veyyon in that profile to keep reading memories it retained before.
  • With sessions stored under $XDG_DATA_HOME/veyyon, veyyon --resume <id> activates the profile that wrote the session, and the session picker's all-projects view and an ACP client's session list show the active profile's sessions from every project, instead of none of them.
  • The session picker's all-projects view and an ACP client's session list no longer show each spawned agent's transcript as a session of its own.
  • /profile <name>, /resume of another profile's session, and veyyon --profile <name> run from a tool's shell start the other profile on its own .env files instead of the credentials the running profile loaded from its own.
  • An Exa API key read out of a profile's mcp.json no longer reaches a process started under another profile, which resolves EXA_API_KEY from its own configuration instead.
  • On Windows, launch in a project where another profile's daemon broker is running starts this profile's own broker instead of connecting to the other profile's pipe and failing with Daemon broker authentication failed, and a session under VEYYON_CODING_AGENT_DIR no longer shares the default profile's broker and its environment.
  • Switching a running session to a transcript another profile wrote, through an extension or RPC switch_session, fails with an error naming the owning profile instead of continuing that transcript under the running profile.
  • An unknown-flag error suggests a short flag in the spelling the parser accepts, such as -r for -rr, instead of --r, which the parser rejects, and offers -h and -v like every other flag.
  • A URL read whose image bytes do not decode returns the text page the server sent, or a note that the bytes were not a valid image, instead of failing the read.
  • A URL read of a page whose alternate <link> href is not a valid URL skips that link and reads the page instead of failing.
  • A compaction that writes the same summary as an earlier one, such as a second server-side compaction with its empty summary, stamps its dead-end warning on the new entry and passes the new entry to session_compact handlers instead of the earlier one.
  • With edit.streamingAbort on, a streaming edit patch whose last removed line has no trailing newline stops the turn when that line is absent from the file, instead of leaving it for the edit tool to reject after the stream ends.
  • Disposing a session while a Python or eval run is in flight no longer keeps the process alive for up to 3 seconds after the run finishes.
  • The CLI imports the terminal output guard when a worker thread starts rather than at startup, keeping it off the static boot graph; no user-visible change.
  • A tool card whose call carries an argument of the wrong type, such as input: 404 for search, draws the value as text or omits it instead of failing with Renderer failed: e.toWellFormed is not a function.
  • The read card for a structurally summarized file numbers each row with the line the model saw, a merged brace pair with its opening line, instead of counting up from line 1 past every elided body, and draws the … elision row and the summary budget notice without a line number.
  • The streaming-reveal throughput bench builds its target as a transcript view instead of a raw assistant message, so bun packages/coding-agent/bench/streaming-throughput.bench.ts runs again; no user-visible change.
  • An agent's live preview shows the shorthand the stream was still holding when a streamed message ends without a final content snapshot, instead of dropping it.
  • veyyon --resume <id> for a session from another project reopens that session in place and moves the launch into its recorded working directory, instead of prompting to fork it into the launch directory or failing without a terminal; an explicit --cwd moves the session's working directory there instead.
  • veyyon --resume <id> runs under the profile whose sessions directory holds the session, instead of the profile the launch started in; an explicit --profile takes precedence.
  • The advisor's tools read the primary tool session live, so an advisor ask delivers the host notification, an advisor task label rides the session's side transport, and an advisor spawn after a working-directory change inherits the new context files, workspace tree, skills and rules instead of the launch ones.
  • Clearing the transcript for a rebuild (resume switch, /shake, compaction, a display setting) releases the discarded blocks and their rendered rows before the new blocks are built instead of holding both trees until the next frame, cutting the retained heap at a rebuild of a 13,470-entry transcript from 264.7 MiB to 157.0 MiB.
  • Turning Settings → Providers → Import Other Tools' Config on or off applies to the next discovery load in the running session and leaves disabledProviders unchanged, instead of adding or removing a provider named importForeignConfig there and taking effect only in the next session.
  • Turning Settings → Appearance → Show Inline Images on or off redraws the transcript under the new value, instead of leaving the images on cards already drawn.
  • Closing /settings, flipping an appearance toggle there or hovering a status line preset keeps the configured statusLine.segmentOptions on the status line, instead of resetting them to the preset's until the next unrelated resync.
  • @veyyon/coding-agent/export/html exports buildSessionData and collectSubSessions again, which moving them to export/session-data had dropped from that entry point.
  • A resumed session at rest drops the committed transcript blocks above the retained screen from the terminal frame, including blocks that report a version and blocks a frame carries without drawing, keeps one block more instead of dropping none when the commit lands on a block boundary, and releases the rows each dropped block memoized, so a resumed 600-turn session at rest holds 87.9 MiB of heap and 746,900 objects instead of 105.5 MiB and 962,500 (median of three).
  • A session replace or display reset that lands between the paint that committed transcript rows and the next frame replays the whole transcript, instead of dropping the committed rows from the replayed frame and leaving 35 of 600 rows in the terminal's scrollback or 21 of 699 on the alternate-screen scroll tape.
  • The raw SSE capture that /debug reads releases each frame record as it evicts it instead of holding it until the window compacts, so the capture stays within its 512,000-character budget and a 60-turn session holds 883 content frames (130.0 KiB) instead of 1,407 (207.2 KiB).
  • An action on an element named by a tab.observe() id or a tab.ariaSnapshot() ref lands once on the element that replaced it after the page redrew, when that element is the only one with the node's role and accessible name, instead of failing with "Node is detached from document"; a click, hover or tap reads the element's point and dispatches in one round trip, so it also lands on a page that redraws every animation frame, and an element the role and name cannot single out fails naming the id or ref as stale.
  • tab.screenshot() sends a second capture on a CDP session of its own when the first has not answered within 1 second, up to three, so a capture sent while the page commits a navigation no longer holds until the protocol timeout or holds later screenshots and the tab's close behind it.
  • A tab.goto() aborted with net::ERR_ABORTED by a navigation the page started, such as a click handler's redirect, is sent once more and resolves on its own URL; a goto aborted twice fails naming the navigation that aborted it last.
  • A bash command whose caller cancels while the call joins the session CPU budget returns as cancelled without running, instead of running to completion and reporting success.
  • A /queue or => line submitted with Enter whose first message fails to send comes back to the editor as the => queue it was, with its pending images and their links, instead of leaving the editor empty.
  • veyyon commit edits a changelog's Unreleased section line by line, so adding or deleting an entry keeps the section's prose, categories outside Keep a Changelog, wrapped and nested entries and the file's final newline instead of dropping them, a ### toString heading no longer stops the changelog from being read, an entry differing from a proposed one only in case, spacing or a trailing period counts as the same entry for duplicates and deletions, and a changelog the proposal leaves unchanged is no longer rewritten, staged or reported as updated.
Removed
  • The unread sessionAccent field is gone from the status line's settings object; statusLine.sessionAccent still colors the editor border.
  • The unused built-in designer model role is gone from Settings → Model → Roles; a modelRoles.designer value you already set still resolves through @designer as a custom role.
  • The retired statusLine.separator and statusLine.transparent settings are gone; neither changed anything on screen, and a config file that still sets them loads unchanged.
  • 36 exported functions and constants that nothing called, and the unused Smithery Connect API client mcp/smithery-connect.ts, are gone; no user-visible change.
  • The model.toolCallLoopGuard.readSubsumptionThreshold setting and the tool-call loop guard's redundant-read steer are gone, replaced by read refusing reads still in context; a stored value is dropped from the config on load.
Fixed
  • Running two veyyon versions at once on Windows no longer prints could not remove the stale addon cache ... EPERM over the interactive UI and pushes the composer down; a real removal failure shows as a natives warning notice.
  • A console print from a library, a native addon warning or a worker thread no longer writes into the interactive UI and shifts the composer on Linux or Windows; the text goes to the veyyon log file.
Changed
  • Slash-command reports (/tools, /hotkeys, /context, /jobs, /todo, /lsp, /plugins, /effort) format with clean human summaries, consistent headers and indentation, and without raw XML tags or run-on bullet sequences.
  • The extension dashboard aligns list cursor bands to avoid text overlap, keeps the provider tab strip on a single scrollable line, and adopts the shared search input style.
  • The account manager displays quieter empty provider states without trailing dashes and phrases initial account addition cleanly.
  • The profile picker presents a dedicated Profiles title, drops the redundant other option, and cleans up the create-profile label glyph.
  • The resume session selector omits file size metadata when displaying empty sessions without messages.
  • The agent dashboard renders active tabs with standard theme highlight styling without hardcoded bracket characters.
  • Settings panel presents booleans as On/Off, humanizes enum and status labels, displays the selected setting description in the footer, simplifies default model display, and aligns the value column.
  • Bare-command pickers (/mcp, /usage, /account, /debug and the rest) widen to show every usage hint and description whole, cut a usage that cannot fit after a whole word, print one key legend in the footer with esc close instead of esc/ctrl+c close, name the search there while the list is searchable, and draw a dim scrollbar with a silver thumb.
  • The /debug card is titled /debug, matching the other bare-command cards.
  • Provider request shaping (secret redaction, Anthropic metadata, tool-order check) moved from session/agent-session to session/agent-session-provider-request; no user-visible change.
  • Home-path shortening in tool cards compiles its pattern once per home directory instead of on every call; no user-visible change.
  • A spinner or rail tick in the transcript re-renders only the blocks from the animating one down, so an idle resumed session with a long transcript no longer spends a core re-walking every block.
Fixed
  • Tool-result preview lines replace tabs with spaces, so a tab-indented line no longer opens a gap in the rendered preview.
  • Shutting down an LSP client releases callers still waiting for its project to load, and the LSP idle checker no longer keeps the process alive.
  • Timeout timers in MCP HTTP startup, the eval kernel exit wait, the lspmux liveness probe, stdin reading, ACP cancel cleanup, browser user-agent overrides and the interactive closing frame are cleared once the awaited operation settles; no other behavior change.
  • The ask dialog rejects a question with no options and no free-text answer instead of opening a dialog that cannot be answered.
  • A collab guest answering an ask question is offered Other only when the question allows a free-text answer, and a guest reply of Other to a closed question records no custom answer.
  • The extension dashboard's overflowing tab strip reserves room for the paging arrows and stays on one row at every width.
  • A bare-command picker on a narrow terminal narrows a long usage column so every subcommand keeps its description.
  • Switching to a model on another provider after a server-side compaction no longer resends the whole session history: before the next prompt, the session asks the model that minted the compaction to summarize it and continues from that summary, reported as an auto-compaction with reason provider_switch.
  • A prompt or idle compaction on a session that switched providers after a server-side compaction ports that compaction first, instead of summarizing the re-expanded history on the new provider in hundreds of staged requests.
  • A staged compaction summary that fails part way resumes on the next attempt from the segments that never completed instead of restarting from the first segment.
  • A background task card restored from a resumed session stops its rail animation once it scrolls above the live region, instead of repainting the transcript every 100 ms for the rest of the process.
  • A displaceable tool preview sealed during an animation frame releases the live region at that frame instead of holding it open until the next full render.
Fixed
  • A cursor-agent turn whose connection stops answering fails as a dead connection within 40 seconds of the last server byte, instead of up to 70 seconds.

1.5.2

2026-09-22 pending release
Added
  • /usage reset and its selector list and redeem usage-limit reset credits for Anthropic Claude accounts beside OpenAI Codex accounts, and accept a provider prefix such as /usage reset anthropic active.
Fixed
  • A turn whose provider stream died after every in-stream tool call of a batch that cannot be replayed (a Cursor exec-channel batch) continues from the results in context instead of stopping.

1.5.1

2026-09-22 pending release
Changed
  • The bash tool prompt states that a backgrounded job is never polled: no sleep, pgrep, ps, tail -f, top, and no second call that watches it.
Breaking
  • Agent settings use agent.*, tier.agent, advisor.agents, and argot.agents; legacy keys migrate on load, while historical runtime export names, custom-theme color tokens, session records, and RPC spellings remain unchanged.
  • Account formatting helpers are exported from @veyyon/coding-agent/session/account-format instead of @veyyon/coding-agent/slash-commands/helpers/format.
  • Screen takeover moved off ExtensionUIContext and HookUIContext onto an optional ui.terminal capability: custom() and setEditorComponent() are now ctx.ui.terminal?.custom(...) and ctx.ui.terminal?.setEditorComponent(...), and a component widget goes through ctx.ui.terminal?.setWidgetComponent(key, factory) while setWidget(key, lines) keeps the text form every host draws. A host that is not a terminal omits ui.terminal rather than declaring members with empty bodies.
  • Removed ui.setHeader() and ui.setFooter(), which every host implemented as an empty function, interactive mode included.
  • Session spine, plugin loader and contribution registry modules moved from src/session/ and src/extensibility/ to @veyyon/kernel: affected subpath imports now use @veyyon/kernel/session/*, @veyyon/kernel/loader/* and @veyyon/kernel/registry/*; edit-specific event normalization remains at @veyyon/coding-agent/extensibility/tool-event-input.
  • The 152 modules under src/tools/ moved into one directory per domain — core/ for the plumbing every domain reads, then fs/, search/, shell/, web/ and agent/ — so a subpath import previously resolved through @veyyon/coding-agent/tools/<name> is now @veyyon/coding-agent/tools/<domain>/<name>.
  • sanitizeStatusText is @veyyon/utils/sanitize-status-text, so a subpath import previously resolved through @veyyon/coding-agent/modes/sanitize-status-text names the utils module: the function is text-only and every host reduces a value to one line the same way.
  • The 81 site scrapers, the page loader and the Parallel extraction client moved to @veyyon/web: a subpath import previously resolved through @veyyon/coding-agent/web/scrapers/* or @veyyon/coding-agent/web/parallel is now @veyyon/web/scrapers/* and @veyyon/web/parallel, and the search providers' withHardTimeout is @veyyon/web/hard-timeout.
  • escapeMarkdownTableCell and the Turndown helpers are @veyyon/utils/markdown-table and @veyyon/utils/turndown, and htmlToBasicMarkdown is @veyyon/utils/html-markdown, so a subpath import previously resolved through @veyyon/coding-agent/utils/markdown-table, @veyyon/coding-agent/utils/turndown or the scrapers barrel names the utils module.
  • CliUsageError is @veyyon/utils/cli-usage-error; @veyyon/coding-agent/cli/usage-error is removed, so a flag the root parser rejects and one the command framework rejects are the same class.
  • @veyyon/coding-agent/session/auth-storage is removed; the credential types and AuthStorage it re-exported are imported from @veyyon/ai/auth-storage, the module that defines them.
  • The setting declaration vocabulary — SettingDef and its definition kinds, SettingType, SETTING_TYPES, isSettingType, SettingTab, AnyUiMetadata and SubmenuOption — moved from @veyyon/coding-agent/config/settings-schema to @veyyon/settings; the schema, its readers, the tab metadata and the tab groups stay where they were.
  • URL target parsing is exported from tools/web/read-url-target instead of tools/web/fetch.
  • Setting-change signals are exported from config/settings-signals instead of config/settings.
  • fgOrPlain is exported from theme/theme-binding instead of theme/theme.
  • Diagnostic cards use diagnosticsSection from tools/core/diagnostics instead of formatDiagnostics from tools/core/render-utils; diagnostic parsing and sanitisation helpers are imported directly from tools/core/diagnostics.
  • Read selector helpers parseSel and isRawSelector are exported from tools/core/path-utils instead of tools/fs/read.
  • readSourceFsPath is exported from tools/fs/read-view instead of tools/fs/read and remains available from the package root.
  • metadataLine is exported from tools/core/render-utils instead of tools/web/search/view.
  • Removed the unused inheritedAgentDir keybinding option and ohMyPiXAIUserAgent alias; xAI requests continue to use veyyonXAIUserAgent.
  • @veyyon/coding-agent/extensibility/hooks no longer exports HookRunner, HookToolWrapper, discoverAndLoadHooks, loadHooks and execCommand: the legacy hook runtime is removed, a plugin's hooks entry loads through the extension runner, and the subpath keeps the hook types.
  • UNICODE_SYMBOLS is @veyyon/view's; theme/symbols re-exports it and keeps the terminal-only Nerd Font and ASCII presets, the spinner frames and the bar ramps.
Added
  • Exported projectToolDisplay from presentation/web-tool-display.ts, projecting canonical tool execution displays for collab live sessions and HTML export.
  • tools/view-registry.ts exports toolViewDefinitions, the host-agnostic ToolViewDefinition for every tool card the terminal drew, and tools/renderers.ts derives the terminal adapters from it; the set of cards and their chrome are unchanged.
  • A tool card value-imports no tool: the search card limits are tools/search/search-card-limits.ts, the web search provider label is tools/web/search/types.ts, and the launch card owns callMeta and readyPendingSummary; each name is exported from that one module only. Print mode and the HTML export load a card without the tool behind it, and no output changes.
  • presentation/transcript-builder.ts is split into content-text.ts, custom-display.ts and session-projection-engine.ts, each under the presentation module size ceiling; no output changes.
  • src/presentation/ builds the @veyyon/wire/presentation view-models from session state, and PresentationEventBridge turns session events into transcript updates, so a renderer draws a session without importing one.
  • src/modes/terminal/driver.ts implements PresentationContext on @veyyon/tui: it renders every transcript block kind, the status line, the composer and the dialogs from view-models alone, and reports operator input back as UIEvents.
  • InteractiveMode.presentation applies transcript, status, composer and overlay updates to the existing terminal surface without constructing a second terminal or composer.
  • /process-manager opens the Agent Control Center across every conversation this process is running rather than only the one on screen, and a switches the roster, the comms stream and the transcript guard between the two scopes together.
  • read accepts a semicolon-delimited list of internal resources (skill://demo/one.md;skill://demo/two.md), the same list form grep and glob take, and returns one section per entry.
  • @veyyon/coding-agent/session/facade exposes AgentSessionFacade, a thirteen-member session API a front end drives — start, stop, submit, interrupt, retry, per-call tool approval, six event streams and the model, provider and context-usage readings — over a live AgentSession; the facade owns the permission prompt while it is started, and refuses a session another host already routes through.
  • setImageDisplayProbe lets a front end state whether it draws pictures, which is what the session reports to the model about an image a tool returned; a client that installs nothing draws nothing, so a host embedding the engine no longer inherits a terminal's answer.
  • ToolDefinition.view lets an extension describe its tool's call and result cards as a ToolView instead of building a terminal component, and re-exports the @veyyon/view vocabulary (ToolView, StatusRowView, TextBlockView, ViewSpan, ViewTone, ViewStatus) so a plugin can construct one; the terminal draws it, and a tool that also declares renderCall/renderResult keeps those.
  • src/modes/terminal/draw/draw-tool-view.ts draws a ToolView as terminal bytes through exhaustive tone-to-colour and status-to-icon records, so a tone added to the contract fails the type check until the terminal states how it looks.
  • A ToolView span carries file for a filesystem path a host opens however it can, separate from the link a URL rides on, and the tone vocabulary adds diffAdded and diffRemoved for the two sides of a change; the terminal wraps a file run in an OSC 8 link to its file:// URI.
  • Tool domains declare lazy factories in manifest.ts, while tools/view-registry.ts defines their views and policies and tools/renderers.ts derives terminal adapters with shared identities for provider aliases.
  • CustomTool.view lets a tool authored as a custom tool describe its call and result cards as a ToolView, the member ToolDefinition.view already offered an extension, and both adapters forward it to the host.
  • The ask tool emits a host-agnostic HostNotification through ToolSession.notify instead of calling the terminal, and the running host installs its delivery through setToolNotifier; a host that cannot reach an operator outside its own window installs nothing and the capability reads as absent.
  • Source-path comments in modes/terminal/controllers/input-controller.ts, tools/render-utils.ts, utils/block-context.ts and utils/shell-snapshot.ts name the Rust modules they cite at their new paths under natives/. No user-visible behavior changes.
  • A commit whose changes all sit under a grouping directory named natives proposes the directory below it as the scope, the way crates, packages and tests already do, instead of proposing natives.
  • The set_cwd renderer drops name, renderArguments and renderPending: no host reads any of the three, and the tool-renderer contract declares none of them. No user-visible behavior changes.
  • showHookInput's inputOptions.mask is a boolean rather than the character to draw, so the host that paints the field owns the glyph and /secret add no longer names the terminal engine to ask for a hidden field; the masked prompt draws the same • it drew before.
  • The todo tool states where a closed task's completion strike has reached with todoStrikeSplit and writes no SGR escape itself: the terminal draws the struck run with the theme's strikethrough attribute, so a session with terminal attributes off receives the task text without the \x1b[9m bytes it cannot render, and the transcript card sweeps the strike exactly as it did before.
  • runCommitAgentSession reports its run through a CommitAgentReporter instead of writing to stdout itself, and commit/agentic/agent-render.ts is the only module that draws it, so the commit domain names neither the terminal engine nor chalk; veyyon commit prints the same thinking line, markdown, tool tree and totals it printed before.
  • The /collab QR block draws its own leading blank row instead of being presented behind a spacer, so the slash-command registry no longer imports the terminal engine; the transcript shows the same blank row above the code and above the too-narrow hint.
  • The compaction policy vocabulary (check outcomes, the bar a pass is measured against, the truncation edge budget, the prune cache window and idle flush, and the recovery band) lives in @veyyon/kernel/session/agent-session-compaction-policy rather than interleaved with shutdown timeouts and credential backoffs in agent-session.ts, with no behavior change.
  • resolveSlashCommand in slash-commands/helpers/parse resolves a parsed invocation against a command set, joining the first argument token back onto a namespaced plugin command name.
  • SingleResult.isolationFallback records the requested and actual isolation backends and the reason when an explicit mode was not honoured; isolationModeName in task/worktree states a backend's setting spelling.
  • TruncationMeta.totalLinesUnknown states that a read's scan stopped after the shown window, so the notice omits a total it does not have.
  • PLUGIN_MANIFEST_ENTRY_KEYS and PluginManifestEntryKey in extensibility/plugins/loader state the four manifest entry keys a plugin contributes through.
  • cli/stdout-drain exports awaitStdoutDrain and exitAfterStdoutDrain, the one way a non-interactive mode exits after its last frame is written.
Changed
  • Typing a line that does not name a builtin slash command no longer loads the builtin implementations: slash-commands/dispatch.ts answers that from the declarations and loads the handlers only once a name matches, taking the launch graph from 1,642 modules to 1,545 and the interactive input controller from 1,296 to 1,059. No command, argument or completion behavior changes.
  • The 1,806-line builtin handler object moved out of slash-commands/builtin-registry.ts into one module per command category (builtin-setup.ts, builtin-modes.ts, builtin-session.ts, builtin-context.ts, builtin-share.ts, builtin-workspace.ts, builtin-model.ts, builtin-info.ts), with the autocomplete builders in builtin-completions.ts and the category map in builtin-categories.ts; the registry assembles the eight maps and still exports every name it carried.
  • The 860 lines of raw settings migrations moved out of config/settings.ts into config/settings-migrations.ts, which also drops the theme-classifier, builtin-tool-name and compaction-strategy imports from the module most of the product reaches for a setting. No setting, default, order or migration behavior changes.
  • Model target selection moved out of session/agent-session.ts into session/agent-session-model-targets.ts: role resolution, a configured target's model, the compaction candidate list and its configured thinking efforts read only the settings, a model and the available list, so they are callable without a session. No selection, ordering or fallback behavior changes.
  • A running tool card animates one mark: the "… (streaming)" spinner row is drawn only under a header that carries no running spinner of its own, and an agent an eval cell spawned shows the task card's static accent mark instead of a second spinner.
  • The read and write cards parse their arguments and details through @veyyon/utils/fs-tool-args, so a terminal launch no longer evaluates @veyyon/tool-render; the cards draw the same rows.
  • The legacy memories.enabled key is no longer a declared or host-defaulted setting: a config that still holds it migrates to memory.backend on load, the key is dropped on the next rewrite, and the local memory pipeline is enabled by memory.backend: local only. The presentation module's error messages, read-target parsing and cursor clamping use the @veyyon/utils helpers; no behavior change.
  • Reworded the Include Model in Prompt, Max Retry Delay, Hindsight Bank ID, Subagents and Subagent Delegation setting descriptions shown in /settings.
  • The HTML export attaches sub-sessions, redacts and writes the file through one finishExport for the session-manager and file entry points, a stage-1 memory job is completed under its ownership token through one completeOwnedStage1Job for the with-output and no-output paths and the global phase-2 claim reads its row through one readGlobalJobRow, the JavaScript eval cell builds its success, cancelled and failed results through one finish, the GitHub commit watch reports each poll and its grace and confirm notes through one reportWatching, and a capability scope splits (, [ and { groups through one keyed depth table so a closer of another kind never ends the open group; no behavior change.
  • The token-rate helpers are presentation/token-rate.ts, moved from modes/components/status-line/token-rate.ts without a forwarding module, and the status view-model is presentation/status-producer.ts with no presentation/status-builder.ts beside it; the search card limits are exported from tools/search/search-card-limits.ts only, and callMeta and readyPendingSummary from tools/shell/launch-view.ts only.
  • The propose-commit and split-commit tools state the summary and detail limits in their verdict through one verdictWithLimits, Enter and Ctrl+Enter run a builtin slash command and recall the line as typed through one #consumeBuiltinSlashCommand, and a read records its hashline snapshot through one recordFullHashlineContext and renders an over-long first line through one oversizedFirstLineText on the ranged and whole-file paths; no behavior change.
  • Tool-argument repair parses a stringified payload and a parse-sentinel payload through one repairJsonObjectArguments, the launch path resolves the prewalk strong, prewalk cheap and plan-yolo models through one resolveLaunchModel, --resume relocates a session whose recorded directory is gone through one block for local and cross-project matches, and the vault key scans staging entries through one bounded stagedKeyEntries for orphan recovery and crash-stage cleanup; no behavior change.
  • The models-config schema rejects an empty id, name, baseUrl, apiKey, contextPromotionTarget or compactionModel through one narrow helper, the URL fetcher assembles every render result through one buildRenderResult, the patch mode resolves a space-separated outer inner context through one resolveInnerAfterOuter, and the clipboard reads images and text from Windows through one runPowerShell bridge; no behavior change.
  • The plugin manager runs every bun install, bun update, bun uninstall and dry-run through one #runBun, the marketplace manager resolves where a plugin is installed and which scope a verb applies to through one lookup and one #scopeFor, the microphone recorder and its streaming twin walk the detected recorders through one fallback loop, the ZIP64 locator and end-of-central-directory record are parsed by one reader for the in-memory and ranged paths, and head and tail truncation build their single-line results through one helper each; no behavior change.
  • The profile bootstrap reads --profile and --alias through one flag reader, the vault's Linux and macOS atomic exchange share one rollback builder, the eval tool builds its cancelled, failed and completed results through one finishCell, and the lexical bracket scanner opens quote modes from one table; no behavior change.
  • ToolExecutionComponent rebuilds its block through one #syncBlock whenever the spinner frame, the expansion or the freeze changes, including when the spinner starts and stops, so a card that reads context.frame in its view (the vibe composer caret) blinks from its first paint and holds still once stopped.
  • The terminal tool card keeps four behaviors the producer split had dropped: a settled streaming diff preview is drawn without waiting for the next spinner tick, a live-widget call that never ran (ask) falls back to its label, a background task result finalizes its block while the job runs, and the write card coerces non-string content to text; EventController.attachTo seeds the projection engine from the engine's own message source.
  • DapSessionManager.launch and attach share one start sequence, and each breakpoint kind (source, function, instruction, data) replaces its list through one helper for both set and remove; no behavior change.
  • CustomEditor dispatches its twenty app-level shortcuts through one ordered table with the same precedence, deferral during early startup, bash-background fall-through and retry-yields-to-extension rules; no behavior change.
  • Every ModalShell host (tree, session, settings, model hub and model picker selectors, the agent dashboard and transcript viewer, the plan review, login, ask and MCP wizard dialogs, the extension dashboard) routes its close glyph, outside click, breadcrumb and footer chips through one routeModalChrome prelude instead of a hand-typed hitTestModalChrome sequence; no behavior change.
  • Goal and vibe mode context reach the model through one delivery step that keeps the hidden display, the agent attribution and the requested channel; plan-mode context is unchanged.
  • The /btw and /omfg panels settle every outcome (complete, saved, rejected, aborted, error) through one step per panel that clears a previous error and ignores a dismissed panel; the painted rows are unchanged.
  • The reftable ref readers answer null for a missing ref and rethrow an abort through one read-only git query, and the github ops raise the same "unavailable" ToolError for a missing repository root, primary root, branch or head through one check; no behavior change.
  • The legacy extension resolver's six promise caches (package root, manifest, bare dependency, native addon, addon-require scan, realpath) share one memoizer and one importer-scoped key; each result is cached once per key, as before.
  • veyyon setup speech picks the speech-to-text and text-to-speech models through one step that persists only a value the setting declares and flushes it before the next pick; no behavior change.
  • The SearXNG endpoint, token and Basic-auth readers share one settings-then-environment lookup, with an empty endpoint or token still read as unset, and the tool result builder records match, result and head limits through one setter; no behavior change.
  • ACP session/resume opens a stored session through the same step as session/load (same cwd check, same MCP configuration, same not-found error) without replaying history, and the symbolPreset and colorBlindMode settings apply to the theme through one signal hook that logs a fallback or a failed setter under the setting's name; no behavior change.
  • Single-range and multi-range read clip long lines to tools.outputMaxColumns through one display-only clipper that records each clipped line, including a block-context line outside the window, out of the seen-lines record and reports the column cap once; no behavior change.
  • /btw, /tan and /omfg read their argument and clear the composer through one handler; the composer's locked and awaitingApproval flags enter and leave their modes through one step; an agent's extension sendMessage and sendUserMessage are tracked and error-logged through one step; the b, c and down-arrow empty-composer keys install through one listener; and the token_in, token_out, cache_read and cache_write status segments render through one counter; no behavior change.
  • Async job deliveries and in-flight deliveries filter by owner and suppression through one step; the projection engine's running and background-task marks toggle set membership through one step; and skill loading resolves authored and managed skill real paths through one helper, so one SKILL.md reached through two paths loads once without a collision warning; no behavior change.
  • RPC agent and session events pass one typed-set check; every streaming edit mode recovers its matcher paths and entries through one non-empty and one single-path step; and skill, rule, context-file, prompt-template, slash-command and custom-command discovery resolve their project and agent roots through one step; no behavior change.
  • Streamed text and thinking segments reveal through one generic step; both Gemini search request paths build their per-attempt body through one transform step; and the cmux tab's waitForSelector is waitFor; no behavior change.
  • The theme.dark and theme.light setting hooks republish their slot through one factory; the RPC host_tool_result and host_tool_update frames pass one guard parameterised on the frame type and payload key; and the Hindsight retainMode, recallBudget and scoping env and settings values are accepted through one listed-value picker; no behavior change.
  • Built-in model discovery selects its standard and special providers through one predicate (disabled set, configured discovery rows, provider filter) and resolves each provider's discovery endpoint through one lookup (runtime override, models.yml, catalog); the ! and % shortcuts present, stream into, settle and release their transcript block through one step with their own failure labels; no behavior change.
  • The agent executor reads a message's or event's content and usage through one field reader, and a symbol-preset or colour-blind change re-renders the committed theme through one ephemeral re-apply step; no behavior change.
  • The legacy pi-coding-agent read, bash, grep, find and ls tool definitions draw their call card through one step (title, then the call's own summary), with the same bytes as before; no behavior change.
  • Grouped read displays and path expansion share delimiter scanning while preserving escaped separators, glob braces and selector ranges.
  • Learn and Memory Edit result views share text projection while preserving titles, empty results and error formatting.
  • Collab and HTML tool previews build display data without serializing discarded transcript input.
  • Tab expansion imports @veyyon/utils/tab-width; no user-facing effect.
  • The speech-to-text, tiny-model and TTS worker clients share one request lifecycle (subprocess/worker-request-client) and one progress-event shape; a worker error or client shutdown settles every pending request the same way in all three, with no user-facing effect.
  • Collab link formatting and parsing (formatCollabLink, parseCollabLink, generateRoomId) are @veyyon/wire's, re-exported from collab/protocol; a link the host mints and the browser guest parses read one grammar, with no user-facing effect.
  • Claude plugin hooks, tools, skills, slash commands and MCP servers resolve their plugin roots through one pluginRootsFor step, and no loader can mutate the cached root warnings; no user-facing effect.
  • Terminal and presentation event handling share tool-argument correlation and retry traces, with stable block identities across streaming snapshots and session resets.
  • Standalone and adopted terminal drivers share transcript updates and composer bindings with unchanged read-card presentation.
  • Grouped read projections preserve per-call status, paths, previews and line numbers across serialization.
  • Live and rebuilt tool cards share single-card rendering while preserving renderer precedence, streamed arguments and failure notices.
  • Status-line usage fields use the shared presentation contract without changing rendered output.
  • Bash and job cards load shared presentation helpers without importing their execution modules.
  • Plugin discovery shares installed-registry validation and root collection without changing scope precedence or warnings.
  • Runtime initialization no longer waits for first-frame replay cache publication.
  • Eval bridge options share field declarations while preserving exported interfaces and runtime behavior.
  • Settings selectors share list initialization, model-picker attachment and inline updates without changing input routing or persistence.
  • Extension API instances no longer allocate unused flag maps and provider-registration arrays; flags and registrations remain in the shared session runtime.
  • MCP commands share argument parsing without changing terminal and CLI option policies or interpreting argument text as a second command.
  • DAP and LSP configuration discovery share source ordering, and Gemini and OpenCode share user-context loading, without changing precedence or warnings.
  • Editor keybinding matching shares default key sets while preserving remap isolation and fallback chords.
  • Autoresearch experiment tools share active-branch session resolution without changing errors or session selection.
  • Autoresearch session updates share optional-column assembly without changing stored values, omitted-field handling, or database errors.
  • Extension dashboard loading shares disable and shadow precedence across capability kinds without changing state or metadata.
  • settings-selector.ts writes its escape comparison as \x1b instead of a raw control byte. No user-visible behavior changes.
  • Output minimizer tool filters collapse repetitive line filtering, table formatting and subcommand routing into declarative specs with unchanged output behavior.
  • GitHub search operations in the web domain and vibe mode tools use shared parameterized dispatch tables; behavior is unchanged.
  • Terminal selector cards and command controllers share modal select delegation, mouse routing, subcommand dispatch, and markdown panel presentation without changing TUI behavior.
  • Settings selectors share model-chain summaries, heading rows, picker targeting and candidate deletion without changing display or persistence behavior.
  • Parity ledger schema validation, module specifier resolution, and workspace traversal logic are consolidated into shared script helpers without changing validation verdicts.
  • Custom and legacy hook messages use the agent-core type definitions without changing message conversion or public type signatures.
  • Filesystem reads and writes share range calculations, candidate probing and result assembly while preserving SQLite row-operation exports.
  • Terminal code cells and tool views share row splitting, truncation summaries and text sanitization without changing rendered output.
  • Image inspection, Mnemopi initialization and search-provider loading use AI subpath imports while preserving usage-provider and credential-ranking registration.
  • Launch and live status rows share configuration reads, and launch redraws avoid enumerating the settings schema.
  • Settings group reads reuse declaration-path membership without caching setting values.
  • Filesystem and goal previews load without their tool execution modules.
  • Task cards share row construction, nested traversal, finding normalization and yield labels without changing rendered output.
  • Web-search providers share credential resolution and HTTP error handling, and discovery providers share equivalent scanners without changing provider behavior.
  • Tool cards share hidden-count formatting and symbol presets share identical glyph definitions without changing rendered output.
  • Tool cards share error rows, metadata and empty-result construction without changing rendered output.
  • Python, Ruby and Julia kernels share runtime probing and subprocess initialization while preserving language-specific environment and execution behavior.
  • First-frame replay recordings are written asynchronously so filesystem operations do not block editor input.
  • Launch status rendering reuses repository discovery when reading branch and worktree metadata.
  • Launch layout measures the fixed-height status footline without rendering it.
  • Every setting in every domain states what it does and what each value selects in one to three sentences, with no design history or internal vocabulary; the idle timeout row is labelled "Park Idle Agents After", the Park and Prune groups are one "Idle Agents" group, the artifact rows are "Artifact Threshold", "Artifact Head Size" and "Artifact Tail Size" with the unit on their options, and the MCP debounce row is "MCP Notification Delay".
  • AgentRegistry.setStatus consults AGENT_TRANSITIONS and throws AgentTransitionError for a status move the lifecycle does not perform, so a turn event that arrives after a kill or a park no longer revives the agent; a collab guest mirrors the host's roster through mirrorStatus.
  • An agent adopted at hand-over, revived from disk or listed from a previous run receives its idle and prune budgets through one builder, so a zero quiet budget disables pruning and a waiting budget is never shorter than the quiet one on every path; no behaviour changes on the paths that already agreed.
  • A retired agent.* setting is retired in one place: rejectedAgentModelSettings sweeps the keys the schema itself marks retiredBy instead of a second hardcoded list, and a retired key with no page hint is reported against the replacement the schema declares rather than being named as undefined; agentsEnabled is the one predicate for whether agents exist, replacing the delegationEnabled alias that returned it unchanged. No setting changes what it does.
  • Interactive chat and the transcript viewer use one replay implementation without changing displayed content or live-tool lifecycle.
  • Transcript rebuilds and live updates share tool-text projection without changing displayed content.
  • Settings group types derive from the settings schema while preserving their existing optional fields and value types.
  • JSON tree projections share bounded traversal, and code and Markdown cells share output assembly, with unchanged rendering.
  • Plugin discovery, import rewriting, MCP configuration toggles, Exa responses, secret-preserving text transforms, todo state cloning and projection, and session-file discovery use shared implementations without behavioral changes.
  • Tool views share progress rows, diff statistics, disclosure metadata, and task separators without changing rendered output.
  • Extension kind labels use a shared function without changing list or sidebar text.
  • Handbook HTML and search assets are generated during documentation and website builds instead of being tracked in Git.
  • Startup reads setting-change signals and terminal formatting without constructing the settings store or unrelated tool modules.
  • The terminal host constructs autoresearch screens and styled slash-command reports; headless command execution no longer imports those terminal components.
  • Subagent statistics and output sanitization are shared across the task tool's view and dashboard through host-independent presentation helpers in task/agent-stats.ts without terminal imports. No user-visible behavior changes.
  • Account inventory formatting no longer imports terminal progress-bar or theme helpers; displayed values are unchanged.
  • Terminal mode initialization overlaps session discovery instead of loading during sequential runtime warmup.
  • Interactive startup begins background model discovery after terminal initialization.
  • Model registry initialization defers unused provider records until lookup or enumeration while preserving runtime provider updates and model overrides.
  • Startup reads provider caches without constructing unrelated discovery options or their bundled reference maps.
  • Model discovery snapshots from earlier host-classification rules rebuild before model selection.
  • Resolved model snapshots no longer duplicate the bundled catalog; model selection and provider overrides are unchanged.
  • Resolved model snapshots use shared byte-integrity checks and atomic replacement without durability flushes; obsolete snapshots rebuild before model selection.
  • Startup imports AI streaming helpers and usage registration without evaluating the AI package barrel; usage providers and credential ranking remain available.
  • Evaluator implementations load on the first enabled request instead of session or tool construction, and loaded runtimes register their own process-exit cleanup.
  • URL reader initialization is deferred until an enabled URL read or search; local tools and transcript previews no longer initialize it.
  • Vibe tools load on /vibe activation rather than during startup, and failed loading preserves the session toolset.
  • The settings schema registry is @veyyon/kernel/settings/schema and config/optional-number is @veyyon/kernel/settings/optional-number; config/settings-schema composes the domain tables through declareSettings and still exports the queries (getDefault, getType, getUi, hasUi, getPathsForTab, retiredBy, isSettingPath, getEnumValues, isUnsetNumberPath, describeSettingTypeMismatch), the SettingPath and SettingValue types, SETTINGS_SCHEMA, the tab metadata and the typed groups, so importing a query loads the tables it reads. No setting, default or config file changes.
  • Settings (config/settings) extends SettingsStore from @veyyon/kernel/settings/store and passes the product's hook set (the global setting bindings, the SETTING_HOOKS table, the config migrations, the settings.json and agent.db legacy read, the changelog-marker seed and the per-directory resolution of enabledModels); config/settings re-exports the store's vocabulary and SettingSignal under the same names, so every import resolves. No setting, default, config file or write order changes.
  • Settings.getGroup delegates prefix path lookup to groupSettingPaths from the kernel store; model role dictionary normalization is consolidated across layer accessors.
  • The tool session, the terminal mode and the SDK read HostNotifier from @veyyon/host instead of @veyyon/utils/host-notification; behavior is unchanged.
  • session/messages.ts registers its message kinds on @veyyon/session's CustomAgentMessages rather than @veyyon/agent-core's; behavior is unchanged.
  • SessionManager, buildSessionContext, the session file loader and AgentStorage are @veyyon/kernel/session/*; session/messages.ts re-exports the custom-message payload helpers and the rehydration sanitiser from @veyyon/kernel/session/custom-message-payload under the same names. appendMessage accepts every AgentMessage but a branch or compaction summary, the set it accepted before. Behavior is unchanged.
  • BashExecutionMessage, PythonExecutionMessage, bashExecutionToText and pythonExecutionToText are @veyyon/coding-agent/tools/shell/execution-messages, declared on the shell domain's manifest as message kinds; session/messages.ts no longer exports them, and convertToLlm converts the two roles through the kind table the tool registry fills. formatGroupedDiagnosticMessages is tools/core/grouped-file-output, which lsp/utils re-exports. Behavior is unchanged.
  • The plugin loader, the plugin manager, the plugin settings dialog and the extensibility/plugins barrel read the manifest vocabulary from @veyyon/plugin; the barrel exports the same names. No user-visible behavior changes.
  • The website source is apps/site and the Python clients are clients/python, the directories that were website and python; the source-checkout veyyon launcher, the binary build scripts and the brand-parity checks read them there, and nothing they produce changes.
  • veyyon update on a source checkout regenerates the HTML export tool views from clients/web, the directory that was packages/collab-web, and the compiled binary reads the embedded stats dashboard from apps/stats, the directory that was packages/stats; the commands and their output are unchanged.
  • Typed tuple and Set copies use spreads rather than .concat() or .slice(), which those types do not define. No user-visible behavior changes.
  • web_search is constructed from tools/web/search rather than the retired web/search module. No user-visible behavior changes.
  • normalizeRoots takes one working-directory string. No user-visible behavior changes.
  • createCachedComponent lives in modes/terminal/draw/utils with the rest of the terminal drawing helpers, and the source map no longer lists src/web/ or src/swarm/ as top-level directories. No user-visible behavior changes.
  • Tests that lived under retired folder names (test/tui, test/capability, test/web, test/vibe, test/repair, and the other pre-cutover aliases) now sit next to the source they cover. No user-visible behavior changes.
  • ToolView cards that walk a JSON value read their bounds and scalar formatting from json-tree-view.ts rather than loading the terminal walker that draws tree-rail glyphs. No user-visible behavior changes.
  • The terminal's ToolView drawer names every kind the contract declares, so a kind added to the union fails the type check instead of drawing as a one-line text row. No user-visible behavior changes.
  • Session persistence, plugin loading and generic contribution registration use @veyyon/kernel; the turn loop, prompt rendering and tool-specific extensibility remain in the coding-agent package.
  • Terminal drawing of a ToolView lives in src/modes/terminal/draw/, and web search lives in src/tools/web/search/. Subpath imports previously resolved through @veyyon/coding-agent/tui and @veyyon/coding-agent/web/search now name those directories. No user-visible behavior changes.
  • Every tool module is grouped by domain under src/tools/<domain>/, each domain importing only core/ and itself, so the dispatch table no longer sits above 139 flat modules. No user-visible behavior changes.
  • Source-path comments and the gallery search fixture name the terminal renderer at its new path, hosts/terminal/engine, and a new architecture gate records every module outside src/modes/terminal/ that still imports it. No user-visible behavior changes.
  • A source-checkout update runs the native-addon ensure step in natives/bridge/bindings, the bindings package's path after it moved out of packages/.
  • Sixteen tool modules keep their terminal drawing in a <tool>-render.ts sibling — ask, ast-edit, bash, debug, fetch, file-search, job, launch, read, resolve, search-tool-bm25, ssh, structure-search, text-search, todo and write — completing the convention eight tools already followed, and tools/json-tree.ts is tools/json-tree-render.ts. Twenty-four of the twenty-six modules under tools/ that name @veyyon/tui are render siblings, print mode no longer loads the renderer through the todo tool, and subpath imports of the moved renderers follow the new layout. No user-visible behavior changes.
  • All five autoresearch experiment tools — init_experiment, update_notes, certify_arms, log_experiment and run_experiment — describe their call and result cards as a ToolView instead of building terminal Text, so none of them imports @veyyon/tui; the terminal draws the same bytes it drew before, including run_experiment's collapsed five-line output preview and its expanded full output.
  • ToolDefinition.view renderers receive a ToolViewContext stating whether the card is expanded, so a tool can shorten its collapsed output without asking a host what it is.
  • The goal tool describes its call row and result panel as a ToolView instead of constructing terminal components; panel and error formatting are unchanged.
  • The report_finding tool describes its call row and its result row as a ToolView instead of building terminal Text, so it no longer imports @veyyon/tui; the terminal draws the same priority mark, tone and truncation bounds it drew before.
  • The set_cwd tool describes its call row and its result frame as a ToolView instead of building terminal components in a set-cwd-render.ts sibling, which is deleted; the terminal frames the same header, rule counts and no-op wording, and the call row loses the blank row above and below it that Text's default padding added, drawing in the column every other tool's call row draws in.
  • The retain, recall and reflect tools describe their cards as a ToolView instead of building terminal components in a memory-render.ts sibling, which is deleted; the terminal draws the same rows, memory bullets, preview caps and expand hints, and a memory cut to a terminal narrower than the eight-column floor the sibling clamped to now ends in an ellipsis inside its own colour instead of a hard cut mid-escape.
  • The read_url tool describes its call row and its result card as a ToolView instead of building terminal components in a fetch-render.ts sibling, which is deleted; the terminal draws the same panel, metadata rows, preview caps, truncation notice and OSC 8 links, and the line stating how many preview lines were held back now draws in the dim its expand hint already used instead of a lighter muted.
  • The resolve tool describes its call row and its result plate as a ToolView instead of building terminal components in a resolve-render.ts sibling, which is deleted; the terminal fills the same plate in the same colour for an accepted, failed and discarded resolution, and the renderer's inline flag goes with it, since the only reader was the gallery CLI.
  • The debug tool describes its call row and its result panel as a ToolView instead of building terminal components in a debug-render.ts sibling, which is deleted; the panel draws the same session lines, output preview and truncation note, the header states the action after the tool name instead of concatenating both untoned, and the line stating how many preview lines were held back draws in the dim its expand hint already used instead of a lighter muted.
  • The ssh tool renders through a host-agnostic ToolView instead of an ssh-render.ts sibling, and a command or output longer than the viewport is windowed by the rows it occupies after wrapping instead of by its line count.
  • The todo tool describes its call row, its board and its finished line as a ToolView instead of building terminal components in a todo-render.ts sibling, which is deleted; the card draws the same rows, phase names, task marks, completion sweep and one-line summary it drew before, a task's mark and its words carry the tone as two runs instead of one, and a task whose status this build has no case for is drawn as open work rather than reaching the terminal with no colour to resolve.
  • The inspect_image tool describes its call row, its answer panel and its failure card as a ToolView instead of building terminal components in an inspect-image-renderer.ts sibling, which is deleted; the card draws the same question line, four-line collapsed and sixteen-line expanded answer windows, held-back note and sanitised failure text, the settled panel states the outcome on its rail instead of taking the muted legacy edge, and the model and media type ride on the header row rather than a second row beneath it.
  • The search_tool_bm25 tool describes its call row and its match list as a ToolView instead of building terminal components in a search-tool-bm25-render.ts sibling, which is deleted; the card states the same status line, five collapsed matches, label and description truncation bounds and fallback warning, the matches are a list the terminal marks and indents rather than rows carrying their own bullet and dot separators, and the held-back count closes the list with the expand gesture left to the host.
  • ast_edit uses ToolView cards with host-managed expand hints; change content and error formatting are unchanged.
  • The irc tool renders through a host-agnostic ToolView instead of an irc-render.ts sibling, a message states its direction in words (IRC to AuthLoader) rather than an arrow, and createIrcMessageCard and ircToolRenderer are no longer exported from the package root.
  • The write tool renders through a host-agnostic ToolView instead of a write-render.ts sibling, the line-number gutter is three columns wide through a 999-line file instead of widening at each power of ten, and a partial result no longer leaves two blank rows between its progress line and the file.
  • The file_search tool renders through a host-agnostic ToolView instead of a file-search-render.ts sibling, and a path too long for the frame is cut to the frame's width instead of wrapping onto a second row.
  • The text_search tool renders through a host-agnostic ToolView instead of a text-search-render.ts sibling, and the rows of a card with no matches indent two columns under their header instead of starting in column zero.
  • The structure_search tool renders through a host-agnostic ToolView instead of a structure-search-render.ts sibling, and a match group is held whole or dropped rather than cut at the row the budget ran out on.
  • The launch tool renders through a host-agnostic ToolView instead of a launch-render.ts sibling, and each captured terminal row is cut to one row with an ellipsis instead of wrapping onto a second.
  • The search tool describes its dispatching card as a ToolView instead of building terminal components in a search-renderer.ts sibling, which is deleted; a call reaches the same file, text or structure card it reached before, whether its type comes from the call or from the result's details, and the row for a call whose type is still arriving, the row naming a type that is not one of the three and the row for a failure that named no type draw in the column every other tool's row draws in instead of one column past it.
  • The github tool renders through a host-agnostic ToolView instead of a gh-renderer.ts sibling, each failed job's held-back log count closes that job's own log rows instead of sitting under the last job in the group, and one held-back log line reads as 1 more log line.
  • The browser tool renders through a host-agnostic ToolView instead of a render.ts sibling, a spawned browser's binary path is shortened against the home directory, and a rebuilt transcript holding a result without its call states no section for the script it does not have.
  • The read tool renders through a host-agnostic ToolView instead of a read-render.ts sibling, the file is the row's description so the row reads Read: src/example.ts, and the line-number gutter is a constant three columns instead of widening at the 10, 100 and 1000-line crossings.
  • The ask tool renders through a host-agnostic ToolView instead of an ask-render.ts sibling, and an option label keeps the emphasis its author wrote through an inline-Markdown run instead of the tool colouring it.
  • The web_search tool renders through a host-agnostic ToolView instead of a render.ts sibling, and veyyon q's answer cap counts the answer's own lines rather than the rows a terminal wrapped them into.
  • The five vibe tools render through a host-agnostic ToolView instead of a vibe-render.ts sibling, and the composer's caret blinks only on a surface that repaints and never on a message already delivered.
  • The job tool renders through a host-agnostic ToolView instead of a job-render.ts sibling, and a collapsed card closes with one held-back note instead of a count on the closing branch of each tree it cut.
  • The eval tool renders through a host-agnostic ToolView instead of an eval-render.ts sibling, a call is one card whose cells are labelled groups instead of one railed box per cell, and output that rewrote its row with a carriage return shows the row's last text instead of every draft of it.
  • The lsp tool renders through a host-agnostic ToolView instead of a render.ts sibling, a diagnostic is one row of location, severity and message, and references are one row per location instead of a per-file tree two levels deep.
  • The bash tool renders through a host-agnostic ToolView instead of a bash-render.ts sibling, and a sentence shaped like one of the tool's truncation, exit, wall-time, background or artifact notices that the program itself printed now survives instead of being deleted.
  • The edit tool renders through a host-agnostic ToolView instead of an edit/renderer.ts sibling, an edit spanning several files is one card whose sections are the files instead of one card per file, and the change counts are stated at the end of the row instead of a ⟦+3/-1⟧ suffix in the tool's name.
  • The task tool renders through a host-agnostic ToolView instead of a task/render.ts sibling, a call whose result has arrived stops repeating the brief and the agent list, and the live output under an agent is bounded to six rows instead of measured against the terminal's height.
  • formatContextUsage is stated once in @veyyon/utils, which the status-line gauge, the subagent progress row and an eval cell's subagent tree all read, so a tool no longer imports a terminal module to format a token count. No user-visible behavior changes.
  • Tool cards and late diagnostic messages share diagnostic parsing, severity ranking and structured rendering, with home-directory paths shortened in late diagnostic output.
  • formatSessionSnapshot is stated once in debug/session-snapshot.ts for both halves of the debug tool. No user-visible behavior changes.
  • A subprocess tool handler declares renderFinal alone: renderInline had one producer and two readers that both skipped it by name, so nothing it returned ever reached a screen. No user-visible behavior changes.
  • A custom tool's renderCall and renderResult return HostView instead of a @veyyon/tui Component, so the published tool-plugin contract no longer makes every tool plugin a terminal plugin; a renderer that returns a Component still satisfies it unchanged. No user-visible behavior changes.
  • An extension's MessageRenderer and AssistantThinkingRenderer and a hook's HookMessageRenderer return HostView instead of a @veyyon/tui Component. No user-visible behavior changes.
  • The autoresearch setup console and init_experiment clamp breadth and attempts through clamp/clampLow from @veyyon/utils/math instead of local copies of the same arithmetic. No user-visible behavior changes.
  • Time-Traveling Stream Rules, the todo board and thinking-effort resolution are session collaborators under src/session/runtime/, each owning its own state behind a host interface it declares (8, 13 and 12 names), instead of 22 fields spread across AgentSession, which drops from 19761 to 18356 lines. No user-visible behavior changes.
  • #processAgentEvent records the turn's last assistant message once, in its synchronous prologue, instead of twice in the same synchronous stretch. No user-visible behavior changes.
  • The hidden nudge vocabulary is session/nudges.ts alone: the customType names the session injects, the continuation cap, the plan-mode reminder budget, the plan-decision and prewalk trigger tables and the mid-run todo nudge gates, with HIDDEN_MESSAGE_TYPES naming the set. agent-session-message-shapes.ts reads message shapes and declares no type name, and runtime/todo-runtime.ts keeps no copy of the nudge thresholds. No user-visible behavior changes.
  • Source-path comments in prompts/all-registries.ts, prompts/eval-overrides.ts and tools/reroot-hint.ts name the benchmark modules they cite at their new paths under tests/evals/; behavior is unchanged.
  • Imports of the string, escape, keyboard, mouse and motion primitives name @veyyon/utils instead of @veyyon/tui, which no longer re-exports them. No user-visible behavior changes.
  • No file under session/ names the terminal renderer: the session id comes from @veyyon/utils/ttyid and image visibility from the front end's probe, so @veyyon/tui is no longer loaded on the conversation engine's account. No user-visible behavior changes.
  • Source directories are consolidated: memory (memories, memory-backend, mnemopi, hindsight), discovery (tool-discovery, capability), speech (tts, stt) and debugging (dap) each live under one directory, and seventeen single-module directories fold into the concern they belong to. Subpath imports of @veyyon/coding-agent follow the new layout. No user-visible behavior changes.
  • The terminal component tree is grouped by concern: the 94 modules that sat flat in src/modes/terminal/components/ now live under transcript/, composer/, selectors/, dialogs/, chrome/, dashboard/ and account/. Subpath imports of @veyyon/coding-agent/modes/terminal/components/* follow the new layout. No user-visible behavior changes.
  • The interactive terminal lives under src/modes/terminal/, and the modes that draw no terminal — print-mode.ts, rpc/, acp/ and the magic-keyword parsing in keywords/ — sit beside it rather than inside it. The palette moves to src/theme/ and extension state aggregation to src/extensibility/extension-state/, both of which a headless run reads. Subpath imports of @veyyon/coding-agent/modes/* and @veyyon/coding-agent/theme/* follow the new layout. No user-visible behavior changes.
  • Goal mode, the working line and push-to-talk are their own modules under src/modes/terminal/controllers/ instead of living inline in the interactive mode, which drops from 5605 to 4495 lines. No user-visible behavior changes.
  • The session runtime's module-level declarations move out of src/session/agent-session.ts into six siblings — agent-session-types.ts, agent-session-permissions.ts, agent-session-queue.ts, agent-session-retry-fallback.ts, agent-session-compaction-policy.ts and agent-session-message-shapes.ts — so importing the session event union or config record no longer pulls in the 20909-line runtime, which drops to 19704 lines. Subpath imports of @veyyon/coding-agent/session/agent-session follow the new layout; the package root barrel still exports every moved name. No user-visible behavior changes.
  • The session factory's free declarations move out of src/sdk.ts into six siblings under src/session/ — factory-options.ts, factory-extensions.ts, factory-prompt.ts, factory-tools.ts, factory-mcp.ts and factory-notices.ts — so naming CreateAgentSessionOptions or calling a discovery helper no longer imports the composition root, which drops from 4861 to 3832 lines. Subpath imports of @veyyon/coding-agent/sdk follow the new layout; the package root barrel still exports every moved name. No user-visible behavior changes.
  • Multi-target ast_grep searches now execute concurrently while preserving globally ordered paging, totals, parse errors, cancellation, and target-order failures.
  • The vibe screens, the image-inspection call and an LSP hover code block draw no border of their own inside a tool block, so a block keeps one left edge; a tree connector remains only where a row belongs to the row above it, in the eval value tree, the grep line gutter, the job tree and the LSP reference tree.
  • The legacy Pi bundled-module generator states which package roots a compatibility shim serves, so the compiled-mode override sweep derives that set instead of carrying its own copy; behavior is unchanged.
  • The /agents dashboard uses a centered, bordered card with bracketed tabs and explicit agent status words.
  • The status row reads its truncation limits from tools/core/render-limits, a leaf that imports nothing, rather than tools/core/render-utils, which drops the tool renderers, path helpers and image resizing from the launch card's import graph; first-frame time is unchanged, because those modules only declare functions.
  • An MCP tool describes its call and result cards as a ToolView instead of building terminal components in mcp/render.ts, which is deleted; the terminal states the same arguments, structure walk, raw rows, held-back count and spill warning, indented two columns under the row that heads them and without the branch glyph the call row opened with.
  • SETTING_KIND_HANDLERS is typed per setting kind, so each handler receives the definition variant its key selects instead of the whole union widened to any. No user-visible behavior changes.
  • Subagent drive paths share abort-aware promise handling with unchanged cancellation behavior.
  • Hook and user-message selectors share search editing without per-keystroke callback or query-array allocations.
Fixed
  • Fixed the daemon broker client and broker startup exiting 0 silently mid-read: Bun.file().text()/.json() does not ref the event loop, so a process that reached the token, lease, presence, or metadata read with nothing else pending could drain and exit before the read settled; the reads now use fs.readFile.
  • A pasted OAuth callback typed after a slash command that takes no arguments is consumed instead of being sent to the model as a prompt.
  • Automatic maintenance cuts an oversized body until a summarization request fits a summarizer, instead of parking the session with "Compaction freed too little context to make progress" when every candidate was skipped for holding fewer tokens than the summary needed.
  • Print, JSON and RPC mode flush Bun's stdout sink before exiting, so a piped consumer receives the whole last frame instead of losing up to 1 MiB of queued output.
  • A spawned agent's card shows its resolved-model badge again on the live block, the registry path an extension wraps and the rebuilt transcript, per agent.showResolvedModelBadge.
  • With VEYYON_FORCE_IMAGE_PROTOCOL=sixel and VEYYON_ALLOW_SIXEL_PASSTHROUGH=1, a bash card draws an inline Sixel image row as the program wrote it instead of blanking it.
  • The extension graph walk reads each module file from disk once; the CommonJS check on a candidate reuses the walk's read instead of opening the file again.
  • veyyon --help summarises worktree with the profile-scoped path ~/.veyyon/profiles/<name>/wt, matching the command's own description.
  • Legacy hindsight.dynamicBankId and hindsight.agentName written as flat keys migrate to hindsight.scoping and hindsight.bankId and are dropped on rewrite instead of surviving as dead entries.
  • Antigravity turns from a consumer Google account (aicode-consumers) no longer fail with 429 RESOURCE_EXHAUSTED on every request: the backend rejects any system instruction carrying the upstream oh-my-pi conventions header, so the preamble now opens Keywords follow RFC 2119: instead of RFC 2119:.
  • A Gemini or Antigravity 429 whose body is Google's generic Resource has been exhausted (e.g. check quota) / RESOURCE_EXHAUSTED is a per-minute throttle and retries on the same account after 45–75 seconds instead of being read as a 30-minute daily quota that exceeded retry.maxDelayMs and ended the turn; the "exhausted your capacity … quota will reset" body still reads as the daily wall.
  • Removing the retired providers.parallelFetch key no longer leaves an empty providers: section in the rewritten config.
  • A nested legacy task.isolation.enabled no longer overrides an explicit task.isolation.mode during migration.
  • Corrected comments that named a distribution channel or runtime API the project does not use; no behavior change.
  • A subagent that inherits the session's model shows that model and effort in its Subagents row, the task widget and the /agents roster, and keeps the badge when a follow-up turn wakes it.
  • An inherited subagent model's badge prints the effort the session settled on, so a parent running auto shows the resolved level rather than auto.
  • A subagent's row label is generated with the session's live model when no tiny, commit or smol role is configured, as a session title is, instead of the persisted default role.
  • A compaction candidate whose single-request summary timed out and was completed in stages starts the next compaction on that model staged, instead of waiting out the same timeout first.
  • Command-mode detection recognizes shell and Python prefixes after Unicode leading whitespace in loaded drafts.
  • An MCP server that repeats a pagination cursor or never ends its pages no longer hangs resources/list, resources/templates/list or prompts/list; every list method ends with a warning and the pages collected so far, as tools/list already did.
  • Tool result cards preserve raw-string and multipart text, including MCP output and grouped read previews, while runtime result cards select eval or launch from their call arguments.
  • Collab reconnect snapshots preserve completed tool result text and display metadata across current and historical tool-call record formats.
  • HTML export preserves provider fallback transitions and redacts their model text in primary and nested transcripts.
  • Empty job results render their success or error message for every registered job alias.
  • The website model catalog refreshes from its relocated repository path before using the deployed copy.
  • Release validation checks native sentinels under every discovered workspace root, and shortened release notes link to the complete tagged changelog.
  • Terminal input delivery continues to later subscribers when an earlier subscriber throws.
  • Tool cards retain wire-provided results during expansion and sealing, and restore expanded output when subsequent updates contain raw results.
  • Clearing or replacing presentation rows stops detached animations, preserves completed-call history, and keeps subsequent reads visible.
  • Stopping a terminal presentation driver releases card animation clocks even when its engine is not running.
  • Extension discovery resolves installed plugins from the session's selected profile.
  • Python composer borders retain their mode color when an agent view is focused.
  • Completed and rebuilt tool cards retain their call arguments and rendered call details.
  • Disposing a tool card stops its animation clocks and detaches its presentation subscription.
  • Legacy tool result renderers receive the completed call arguments for partial and final results.
  • Background provider usage refreshes repaint the status line without requiring further input.
  • Tool cards retain both call and result renderer failure notices with their fallback output.
  • The library entry point retains read-selector functions, write-preview helpers, filesystem-source extraction and the goal view after renderer separation.
  • An auto-compaction pass that finds nothing to summarize warns about a compaction dead end only while the context is still over the recovery band, so a pass that runs after maintenance already freed the context no longer advises starting a fresh session.
  • The MCP add wizard retains manually entered credentials for stdio, HTTP and SSE servers and opens OAuth settings after authentication errors.
  • Retry and Edit OAuth settings accept pointer input immediately after OAuth failure or cancellation in the MCP add wizard.
  • Kernel codec and shutdown defaults remain active when optional configuration fields are undefined.
  • Startup frame capture stops retaining terminal output after typing, recording settlement, or frame release.
  • Disabled launch status rows remain blank when the terminal is narrower than the composer inset.
  • veyyon licenses reads the Open Sans and Source Code Pro notices from the tracked docs/handbook/fonts/ copies instead of the ignored mdbook build output, so the bundle regenerates and its test passes on a checkout without a handbook build.
  • A compaction that had just freed context no longer reports "Compaction freed too little context to make progress" when its entry is written in the same millisecond as the kept assistant turn; whether a turn predates the latest compaction is read from its position on the branch, not its timestamp.
  • The home anchor sizes its fills, the welcome hero mounts, and the per-frame sizing pass runs on the screen the interactive mode renders on rather than the one its constructor built, and startup.quiet is read from the session's settings like the other startup reads.
  • An agent whose session is on screen is no longer parked under it: opening an agent pins it for as long as the main view points at it, a park deadline that elapses meanwhile is deferred, and the idle TTL counts again from the return to the main session.
  • The task card lifts the missing-yield warning out of an agent's output under both its current spelling, SYSTEM WARNING: Agent exited without calling yield tool, and the Subagent spelling a session file recorded before it; the task:subagent:progress and task:subagent:lifecycle bus channels a collab guest matches on keep their spelling.
  • Custom tools retain their declared call and result views when converted into extension tool definitions.
  • Terminal tool cards shorten home-directory paths and replace tabs in metadata, notices, code, diffs, and generic argument previews before width fitting.
  • Failed task results without agent details retain error text styling.
  • Launch model context estimates exclude project context and clamp the remaining percentage to zero.
  • Launched processes report completion even when they exit before the completion watch registers.
  • Autoresearch best-result and confidence calculations exclude unmeasured placeholder zeros while retaining measured zero values.
  • Configured shortcuts apply from the first editable frame, and model-selector shortcuts entered during startup preserve the draft through initialization.
  • The launch composer repaints typed input before runtime initialization continues and preserves the mounted editor's change handler.
  • Ordinary CLI launches no longer evaluate ACP authentication, and startup applies the configured theme once.
  • Submissions entered during startup execute after initialization without waiting for a model turn, preserving later drafts and attachments through command handling and editor replacement.
  • The launch composer shows the same linked-worktree location and draft-token estimate as the mounted session, avoiding a late footline repaint.
  • bun run gen:tool-views in the coding-agent package uses the relocated clients/web generator.
  • A web_search refusal, missing-credential, or provider-failure result is marked isError, so the model sees the failure as a failure.
  • A tool result whose image-visibility notice is unchanged is memoized across turns instead of allocating a new wrapper each time.
  • edit and apply_patch share one renderer object again, so a host that groups by identity (streamed-arg keys, the first-result replay) treats them as the same card. No user-visible change.
  • A debug adapter command resolves against the debug working directory only when it is spelled as a relative path (./tool, ../tool), restoring the prefixes a mechanical path rewrite had widened to any leading dot.
  • An overflow whose summary cannot fit any model no longer waits on a restore that never runs: the dead-end rescue truncates the oversized message to an artifact and the turn is retried, which the overflow-refusal suite now pins.
  • A live tool card drawn from a ToolView spins again: a view is handed the frame it draws, so the spinner tick now rebuilds the card instead of only repainting it, and the streaming glyph on the browser, debug, eval, launch, ssh and vibe cards no longer sits frozen on the frame it was born with.
  • The debug, resolve, retain, recall and reflect renderer entries declare the inline policy they declared before their cards became views. The flag reaches no reader in the terminal today, so nothing on screen changes.
  • A tool card's header keeps the subject it states: the middle-cut the edit card needs was applied to every framed card, so ast_edit, file_search, text_search, structure_search, search_tool_bm25, inspect_image, launch and ssh cut a query, a glob or a path to … in a narrow terminal to keep counts that already fitted. A card now asks for the cut, and the edit card is the one that asks.
  • A live card whose tool describes a ToolView obeys the merge policy its renderer entry states: debug, resolve, retain, recall, reflect and ssh drew the call row above the result card while the call was on screen, and drew one card when the transcript was rebuilt.
  • veyyon config set and veyyon config reset create the profile's config directory before taking the file lock, so a first write into a profile that has no directory yet persists the setting instead of exiting 1 with ENOENT.
  • goal-tool.ts no longer imports a ViewSpan type it stopped using when the goal card became a view. No user-visible change.
  • A structure in a tool card stops at the nesting level its bound names: the view walk numbered a root key 0 where the terminal walk numbered it 1, so every task, eval and argument card showed one level deeper than the collapsed and expanded bounds allow.
  • A JSON structure in a tool card states 1 more line where one line of a long string is held back, instead of 1 more lines; the view walk wrote the phrase by hand where the terminal walk it replaced counted through the shared formatter.
  • The launch shell no longer evaluates the prompt registries: modes/keywords/{orchestrate,ultrathink,workflow}-keyword.ts kept imports of prompts/subagent/rows, prompts/turn-control/rows and the @veyyon/utils barrel after the notices they fed moved to magic-keyword-notices.ts, which pulled 62 extra modules into the first frame's import graph.
  • The subagent model resolver's own precedence list no longer points at a fifth case it lost when subagent.model became the shared-switch layer. No user-visible change.
  • HTML export works again: the five assets export/html/index.ts loads as strings — the markdown renderer, the template CSS, HTML and JS, and the generated tool views — lost their with { type: "text" } import attributes while unreleased, so each one loaded as a module and every export threw Missing 'default' export in module.
  • Seven published entry points the reorganization dropped are served again at their new paths: ./modes/terminal/components/*, ./modes/terminal/components/extensions/*, ./modes/terminal/components/status-line/*, ./modes/terminal/controllers/*, ./modes/terminal/setup-wizard/*, ./modes/terminal/utils/* and ./export/markit/*. Every subpath 1.3.0 published now has a successor key.
  • A source build of the compiled binary reaches the native addon package at natives/bridge/bindings, its path since the bindings moved out of packages/, instead of failing on a chdir to packages/natives.
  • Tools are offered in BUILTIN_TOOL_NAMES declaration order again: composing the registry from the five domain manifests reshuffled seven names, and tool order is prompt order, so every cached prompt prefix was invalidated for a session whose tool set had not changed.
  • The source launcher (scripts/veyyon, scripts/veyyon.cmd) finds the native addon at natives/bridge/bindings/native and provisions it through that package's ensure-native.ts, instead of dying at boot on packages/natives/scripts/ensure-native.ts after the bindings moved out of packages/.
  • The compiled binary builds again: its legacy-extension module surface resolved each bundled package by its directory name under packages/, so the build died on packages/natives/package.json once the bindings moved to natives/bridge/bindings and the terminal engine to hosts/terminal/engine. Each bundled package is now found by the name its manifest declares, wherever the member sits.
  • The compiled binary builds through its legacy-extension shims again: the @veyyon/ai and TypeBox compat shims were named by hand-written paths under packages/coding-agent/src/extensibility/, so the build reported two unresolvable entrypoints once both shims moved into @veyyon/kernel. Each shim is now resolved through its owning package's published subpath, at build time and at extension load.
  • The launch card states the context percentage recorded at the end of the last launch instead of ?, and falls back to ? when the release, the model or the project changed.
  • A debug adapter that exits during startup now reports its own stderr — pip install debugpy for a missing debugpy — instead of EPIPE: broken pipe, send.
  • A submission arriving in the tick after the terminal asks for input is no longer dropped, and loop mode's auto-submit timer no longer arms a tick late: the goal-mode exit check ran as an await on every turn, and an async guard that returns early still yields before the input callback is installed.
  • A run that draws nothing no longer tells the model its picture is on screen. -p, rpc and acp emit text, and the sentence attached to an image a tool returned was written from the launching terminal's graphics protocol, so the same read in a Kitty window reported the image as displayed to a pipe; the front end now states whether it draws, and a client that says nothing is taken at its word.
  • A debug launch whose interpreter refuses now reports the interpreter's own exit code and stderr instead of EPIPE: broken pipe or DAP adapter disposed: writing the first request onto an adapter that has just died fails at the pipe, and the pipe's error names nothing the user can fix.
  • An extension published against the old @veyyon/tui barrel loads again. Moving the width, key, mouse, motion, LaTeX, fuzzy and paint primitives to @veyyon/utils removed them from that barrel, so a plugin importing visibleWidth or Key from @earendil-works/pi-tui failed at import time and its whole extension went inactive; the bare tui package root now resolves to a compatibility surface carrying the renderer plus every module the barrel dropped.
  • A turn that stops right after a tool round gets its stop-time passes again: the assistant message the settle reads was recorded behind the persistence await, so a text-only stop dispatched in the same tick as a tool result settled as if it still carried tool calls, and the todo reconciliation never fired past its first reminder.
  • A model selector that names a provider and no model (openai/, openai/:high) is rejected where it is written instead of parsing to a model id of the empty string, which every caller then carried until a lookup failed somewhere else.
  • A read, write, grep or image target that reaches outside the working directory through a symlink asks for approval even when it carries a selector suffix, so link.env:1-10, db.sqlite:users:42 and archive.zip:dir/file.ts:5-9 are no longer auto-approved where the bare path would have prompted.
  • A tool status line shortens the paths it was given, so grep, glob, ast_grep, ast_edit, debug and set_cwd show ~/project/src instead of printing the home directory into the transcript, and a long path list is truncated rather than pushing the row past the terminal width.
  • Multi-target ast_grep searches across overlapping paths deduplicate matches so totals, file counts, and paged results are not duplicated or truncated.
  • grep reports why an archive could not be opened or read when the failure is not an Error, instead of the word undefined.
  • The advisor configuration overlay, the autoresearch dashboard overlay and the autoswarm setup console share one set of box chrome, one stat strip and one key legend, and each frame ends where its content ends instead of being padded to the height of the terminal.
  • The advisor overlay footer states the chord and the verb instead of repeating the advisor or model name already in the frame title.
  • The autoresearch dashboard overlay opens on alt+x; ctrl+shift+x never reached veyyon under kitty, whose kitty_mod default consumes that chord.
  • The autoresearch result table and summary no longer repeat the primary metric as a secondary column.
  • A crashed autoresearch run reports - for the metric instead of 0ms, which sorted it as the fastest run in the table.
  • A turn that calls a tool and then stops with text is treated as ending in text: the todo reminder fires again as the board changes instead of falling silent after the first one, and the rewind, plan-mode, verification and code-review checks run at that stop.
  • The goal report from /goal show and the goal detail menu states the goal's status once: a paused goal read Status: paused (paused), and a finished one Status: complete (paused). Goal mode being off is now named only where the status does not already carry it, as active (mode off).
  • A goal objective reaches every surface that shows it as one plain line: the /goal show report, the /goal menu title, the warning a disabled Goal Mode prints over a stored goal, the /goal autocomplete row and the goal tool's own card each formatted the objective raw, so an escape sequence in one styled or moved the rest of the surface, a tab opened a hole in it, and a newline split it across two fields.
  • veyyon profile new runs in the compiled binary; 1.4.1 failed with awaitPromise is not defined from a mis-minified dynamic import, and --smoke-test and the installer CI now drive the profile seed path through the artifact.
  • --export <path> of a missing or non-file session exits non-zero with Session file not found or Not a session file instead of writing an empty transcript and reporting success.
  • --mode json exits EXIT_FAILURE on an errored turn and EXIT_INTERRUPTED on an aborted one, as text mode does; print and RPC mode wait for stdout to drain before exiting so a piped consumer receives the last frame.
  • RPC mode answers a malformed stdin line with the documented parse error frame and keeps reading; the previous chunk parser threw on the first bad line and ended the session.
  • veyyon install with no target exits 2 as a usage error, the same as a missing positional in every other subcommand.
  • --help lists every --mode value from the parser's own table, acp included; the export example and the worktree description state the profile paths (~/.veyyon/profiles/<name>/…).
  • --no-extensions disables discovery only: a path named with -e on the command line still loads, in a session and in veyyon models, as the flag's help text states.
  • A --model @role whose role is unset, unknown or cyclic is reported as that role failure; a model id no model matches is reported with the count of models with usable credentials and the nearest candidates, instead of Model "x" not found. Run "veyyon models".
  • A plugin's slash command runs from /<plugin>:<name> and from /<plugin> <name>; the parser split the namespaced spelling at the colon, so an exact lookup never matched.
  • The goal tool is a read-tier tool: it prompted for approval in ask mode and was denied outright in plan mode, having no declared tier.
  • An explicit agent.isolation.mode the host cannot provide runs on the next backend, records isolationFallback on the result, opens the merge summary with a notification naming the requested and actual backends, and is logged; auto reports no fallback.
  • A spawned agent whose merge back into the parent tree does not land (a patch that does not apply, a branch that does not merge, a nested repository patch that fails) is reported as merge-failed instead of a clean completion whose work is not in the tree.
  • The task card's line and size counts describe the block the reader sees: when the child's output was empty and stderr or a placeholder stands in for it, the artifact's 0B no longer labels that text.
  • Claude Code's ~/.claude/plugins/installed_plugins.json is read only when discovery.importForeignConfig is on, and the plugin-roots cache is keyed on that setting; marketplace installs, --plugin-dir roots and a trusted project registry are the operator's own and load at the default, and their skills are part of the profile's skill set.
  • The read card states depth and limit by name after the path; read { path: ".", limit: 3 } drew .:1-3, a line range the tool never read, and the HTML export's call line did the same.
  • A ranged read of an immutable source (archive.zip:src/a.ts:2-3, artifact://x:2-3) numbers its lines whatever readLineNumbers is set to, so the requested lines are attributable.
  • A read of a file over the snapshot size cap states Showing lines A-B (file not scanned to end) and no of N: the scan stops once the window is collected, so N was a lower bound presented as the file's length.
  • The streaming bash preview reads the env object by walking the partial JSON, so "env" inside a command string is not an assignment list and a cwd or timeout after the object is not an entry.
  • No tool card text reaches the terminal with a control sequence in it: drawToolView strips escapes from every text field of every view kind, so a model-supplied reason, a file's contents or a generic argument carrying \x1b[2J or an OSC 8 link is drawn as text; a captured run keeps the styles the terminal replays and loses the rest.
  • A tool card whose renderer throws falls back to the generic arguments-and-output card, never to the exception's text; the vibe card reads a recorded session's details field by field, so a malformed record draws the generic card rather than failing the transcript.
  • The job card lists a queued job as queued, with its position, rather than as running.
  • A daemon the broker ran on a PTY that died to a signal reports that signal (SIGTERM, SIGKILL) instead of the shell's exit code 1; the addon recovers the signal from the wait status.
  • The launch broker's idle reaper stops last-client-exit daemons when the last client disconnects and a persistent sibling keeps the broker alive, and re-arms when that persistent daemon ends so the broker exits too.
  • A plugin whose package.json or .claude-plugin/plugin.json cannot be read or parsed costs that plugin and is reported through the operator channel and veyyon plugin doctor; it no longer aborts the session's plugin load.
  • A plugin manifest's hooks and commands entries load: hooks through the extension runner and commands as plugin-sourced custom commands; only tools and extensions were read before.
  • Two extensions registering the same tool or command name, and a custom tool or command that fails to load, are reported on the operator channel; the later registration wins and the earlier one was silently dropped.
  • An MCP tool that withholds arguments withholds values only; a key-only error was delivered as undefined.
  • An MCP server whose reconnects trip the breaker is reported on the operator channel with the server name and the suspension; the suspension was logged only.
  • A CommonJS extension (module.exports = …, or a transpiled module with exports.__esModule) runs instead of being reported as missing its default export; the wrapper mirrors Bun's __esModule interop.
  • A marketplace catalog entry the parser drops is reported with the plugin name, the failing field and the reason: veyyon plugin marketplace add/update print it to stderr and a session states it on the notice channel; the entry was skipped in silence and the cached catalog persisted without it.
  • The account manager names the account a failed OAuth refresh signed out ("the login for [email protected] was signed out"), so a surviving account rendered beside it is no longer read as the dead one; a credential that states no account keeps the unattributed wording.
  • A provider whose other login still serves every request states that instead of printing press a to sign in again, which claimed the provider was unusable on the same card that listed the account using it; a provider with nothing left serving still asks for the login.
Removed
  • WRITE_GUTTER_MIN_WIDTH is no longer exported: the line-number gutter of a code card is the host's, stated once in src/modes/terminal/draw/draw-tool-view.ts, and no tool sets it.
  • resolveModelFromSettings is removed from config/model-resolver; the role chain a session starts from resolves through resolveModelRoleValue, which reports why a role failed.
  • ReadRenderArgs no longer carries offset: the read schema states a line window on the path itself (src/app.ts:50-200), and limit is the directory entry cap.
Added
  • The /autoswarm dashboard lists a New session action (n) over an existing session, which closes it keeping every file and every logged run and starts a fresh one with the setup as it stands.
  • Swarm presets: swarm and wide are built in, and the console saves the current shape under a name to presets.json beside the autoresearch databases, offered in every repository.
  • The run screen's running row shows the last twelve lines the harness printed under the command, refreshed once a second, with escapes stripped and a carriage-return progress row shown in its final state.
  • The setup form's models note lists up to three authenticated models close to a spec that resolves to nothing (No model matches "opus4". Close: anthropic/claude-opus-4, …).
Changed
  • The updater, startup version check and rollback picker order releases by publication date and treat any different latest version as an update.
  • /autoswarm opens the swarm console and takes no arguments. On a branch with no session it is a centered launcher card: a form with the goal, a swarm/wide preset switch, breadth from 2 to 8 with ◂/▸ steppers, per-arm models, attempts, a certification toggle, an iteration cap, a Start button and a Save as row for presets. Over a session it is the run dashboard: the ledger beside the detail of the highlighted row, the actions the swarm's state allows on single keys (s start or resume, p pause, n new session, x stop, c clear session, r reset worktree), e for the setup form and Enter for the full-width detail view.
  • A field edited in the setup form is written as it is typed, to the session on the branch or parked for the start; the form opens with the caret on the Goal row, Enter on the Goal row starts or resumes the swarm, ↑↓/tab move between rows, ←→ or a digit set a count and space flips a toggle, ctrl+u clears a text row, a click lands the caret or steps, picks and flips the control under the pointer, and the footer names the keys of the highlighted row.
  • /autoresearch is the serial loop only, with the subcommands status, resume, goal <text>, off and clear [--keep-tree|--reset-tree]; /autoswarm no longer takes them.
  • The run screen opened by ctrl+x and /autoresearch status is the run ledger alone, with its type-to-filter and its esc close footer.
  • The dashboard hydrates the session on the branch from the store when the runtime has not loaded it, so its detail pane states the session's goal and runs instead of (not stated).
  • A click on a single-line field places its caret under the pointer in the hook prompt, the login dialog, the MCP add wizard, the history search, the text and config rows of the settings screens, and the setup form; the rollback panel's list takes the pointer through the settings host.
  • ctx.ui.custom(..., { overlay }) accepts OverlayOptions beside true, so an extension can place and size its own card over the transcript.
  • The /agents card ships as it did in 1.4.0; the flat frame and the roster detail pane added after that release were withdrawn before this one.
Fixed
  • The permission card's title bar reads Permission required instead of the markdown source ## Permission required; a select dialog's title bar draws the heading's text and leaves the markdown to the body.
  • A hook status set through ctx.ui.setStatus keeps the theme colours it was painted with, as its contract states, so the autoresearch status row shows its kept count in green, its flagged count in yellow and its best metric in the tool colour instead of one grey line; cursor moves, hyperlinks and graphics in a status are still stripped.
  • Interrupting Claude mid-thinking no longer fails every later turn with Refusal (reasoning_extraction) on an endpoint that enforces the classifier: the hidden continuity message that carries the unfinished reasoning states which turn it came from, and the request drops it on same-model replay to a signing Anthropic endpoint, and after one refusal on any other, instead of re-sending it on the retry and for the rest of the session.
  • A logged metric of zero or below is a measurement: a session minimising a count tags the kept run that reached zero as best, and a signed metric's negative runs are ranked by direction in the Best row, the keep gate of log_experiment, the confidence figure, the Trend row and the prompt's recent-run rows; only a crash's logged placeholder reads as unmeasured, and it prints no metric in the prompt as it does on screen.
  • The setup form's models note states a spec past the last arm ("glm" has no arm at breadth 2.), which the persisted setup dropped without a word when breadth came down.
Added
  • The models page on veyyon.dev lists every provider and model veyyon supports, read live from the bundled catalog and auto-refreshed from the repository; bun run site:build regenerates website/models-data.json from packages/catalog/src/models.json.
  • A bare interactive launch replays the previous launch's card from a cache before the CLI's import graph is evaluated, then adopts those rows and corrects only what changed. bun scripts/bench-startup.ts --runs 5 --bin packages/coding-agent/dist/vey reports the card's first byte at a 34-35ms median composed and a 15ms median replayed. The recording is discarded unless the terminal size, the environment this process received and the binary's path, size and modification time all still match, and it ages out after 24 hours; a launch whose composed card disagrees with the replayed rows drops the recording so the next launch records a fresh one.
  • bun scripts/bench-startup.ts gains a replay arm and keeps the first-frame recording inside its scratch directory. The recording resolves its path from os.homedir(), which Bun fixes at process start, so the bench's seeded HOME did not reach it and a run read and overwrote the operator's own cache.
  • VEYYON_REPLAY_DEBUG names a file the launch appends its replay decision to. A rejected recording is otherwise indistinguishable from a slow launch, and the logger does not exist yet at that point.
  • /rephrase asks for the reply on screen again in plainer prose, and refuses unless the conversation is resting on a finished reply.
  • /autoswarm opens a setup console for the goal, breadth, attempts and certification, then runs autoresearch with breadth: each iteration builds several candidate arms, rejects the ones that are empty, out of scope, unreadable or duplicates, has the survivors cross-review each other, and keeps at most one; /autoresearch is unchanged and still serial.
  • The autoswarm setup console assigns a model per arm, comma separated in arm order, so one iteration compares models as well as ideas; start_arm switches the session to that arm's model before its first edit, the status row names the arm and the model building it, and the session returns to its own model when the arm is logged or the mode is left.
  • An autoswarm run records the model in force when run_experiment measured it and the run screen shows it under the arm, so a round that stayed on one model is distinguishable from a model comparison; measuring an arm that start_arm never announced, or one that is not the arm in flight, states which model actually built it.
  • The autoswarm setup console accepts a pasted model list that carries a newline, which was rejected whole and inserted nothing.
  • Autoresearch and autoswarm have handbook pages.
  • Machine-wide resource limits cap CPU, memory, disk writes and process count across every veyyon process at once, beside the existing per-session limits, in /settings under Resources; both scopes default to no limit.
  • The two resource-limit scopes share one definition of each cgroup control-file format, with no user-visible change: the duplicate the machine scope carried while unreleased could write a freeze quota for a very small CPU budget.
  • bun run test:cgroup-proof drives both resource-limit scopes against a real kernel outside the test sandbox and reports each cap as held or not, refusing with a named reason on a host that cannot delegate cgroups rather than passing having proved nothing.
  • The run screen's session pane shows the number of runs since the segment's best one as Since 8 runs later, none better, and a Trend row of one block per run, oldest on the left, scaled between the segment's lowest and highest measurements; an unmeasured run draws as ·, and a segment wider than the pane draws its most recent runs behind a leading ….
  • The autoresearch status row shows the same gap as 2 since best, and drops that segment before the best metric when the terminal is too narrow for both.
  • The autoswarm setup console states under its title what Enter does on this branch: resumes the named session with its run count, or starts one, with whether autoresearch.sh exists or the first turn writes it; over a session the legend reads enter resume.
  • /autoresearch resume and /autoswarm resume continue an interrupted or paused session without a console or a transcript line, are offered in argument completion, and print what would start one when the branch has no session.
  • An interrupted autoresearch loop puts paused · send a message to resume on the status row and (paused) in the run screen title until a turn resumes it, and the interrupt notice names /autoresearch resume and /autoresearch off in full rather than a bare off.
  • The run screen title is fitted to its border before the border truncates it, so a long session name gives way and (paused) or (mode off) stays readable instead of ending in (paus….
  • /autoresearch <text> on a live session where <text> is the stored goal is a resume with nothing to add: it neither prints a context notice nor sends the goal to the model a second time.
  • A file search accepts path as the directory its input globs are searched under, the way path scopes a text search: { type: "files", input: "*.ts", path: "src" } is src/**/*.ts, each input entry keeps its own depth, and a scope that is a glob or an input that is absolute is rejected naming the spelling that works.
  • Browser tool calls batched in one turn run in the order written; run and close on the same tab used to start together and the run found its tab closed.
  • ExtensionAPI.setModel(model, { ephemeral: true }) switches the session's model for the rest of the turn without recording it as the session's own model, so a resumed session opens on the model the operator chose.
Changed
  • The hidden messages a session injects and the gates that fire them live in session/nudges.ts rather than as file-private literals in agent-session.ts, and the mid-run todo and prewalk gates key their tool tables through the TOOL registry so renaming a tool now fails to compile instead of silently disabling the nudge.
  • The subagent model resolver reads the shared-model switch once and builds the layers of whichever scope that selects, instead of gating each layer against a second reading of it, and the unread SUBAGENT_MODEL_SCOPE_VERSION constant is gone from the rejected-setting log record. No change to what any subagent runs.
  • The compaction policy vocabulary (check outcomes, the bar a pass is measured against, the truncation edge budget, the prune cache window and idle flush, and the recovery band) lives in session/compaction-policy.ts rather than interleaved with shutdown timeouts and credential backoffs in agent-session.ts, with no behavior change.
  • The thinking display's hidden-code fence marker is named hiddenCodeMarker, no longer colliding with the unrelated elisionMarker in @veyyon/utils/byte-truncate; the marker text is unchanged.
  • The read tool's URL routing, internal-URL routing, notebook reading, document conversion and structural summarizing each live in their own named method instead of inline branches of one 758-line execute, with no change to what any read returns.
  • The doc comments on the agent registry's conversation scope and address check, and on the subagent model and thinking-level resolvers, state the precedence and the boundary rule without the surrounding narrative; no behavior changes.
  • The queued-message predicates and the retry.fallbackChains selector vocabulary live in session/queued-message.ts and session/retry-fallback.ts instead of as file-private helpers in agent-session.ts; RestoredQueuedMessage is imported from the first of those.
  • The ACP permission gate's argument reading — which tools need consent, the prompt each call earns and the file locations it points at — lives in session/permission-intent.ts instead of as file-private helpers in agent-session.ts, which also drops that file's private copy of getStringProperty in favour of the @veyyon/utils owner. No change to what is gated or prompted.
  • The launch card paints about 8ms sooner: the bundled themes are embedded as text and parsed on the ask instead of building all 98 before the first frame, and the card path no longer evaluates node:assert/strict, node:crypto, node:inspector, node:child_process or node:zlib for calls it does not make.
  • The status row reads its truncation limits from tools/render-limits, a leaf that imports nothing, rather than tools/render-utils, which drops the tool renderers, path helpers and image resizing from the launch card's import graph; first-frame time is unchanged, because those modules only declare functions.
  • The home screen hero drops the recent-session row; /welcome still lists recent sessions.
  • The launch card paints the whole status row from config instead of a hand-written path and branch, so the profile, model, approval rung, branch and context gauge are on screen with the first frame; measured on a pty against the built binary, the row lands at 47-48ms rather than 1067-1083ms and the frame is editable at 49-50ms.
  • The status row's segment gathering and row fitting live in one module, modes/components/status-line/quiet-row.ts, which the live footline and the launch card both render through, so the two rows cannot carry different segments or order them differently.
  • A status-line segment reads a flat SessionFacts value block rather than the AgentSession itself, which is what lets the same segment table render before a session exists. No change to the mounted row.
  • bun scripts/bench-startup.ts renames its statusline arm to statusrow and times the status row being on screen; the old arm timed a segment only a session supplied and now reports nothing, because the card supplies the row.
  • The autoswarm setup console, the autoresearch experiment tool and the run screen's stacked split clamp their breadth, attempt and row counts through the shared clamp rather than local copies. No behavior change.
  • bun scripts/bench-startup.ts times its statusrow and replay:statusrow arms against the approval rung on the row rather than the context gauge, which is the row's last segment and is shed on the bench's eighty-column pty: both arms reported no samples at all. They now read a 39ms median composed and 15ms replayed against the built binary.
  • The autoswarm setup console and the autoresearch experiment tool clamp their breadth and attempt counts through the shared clamp rather than local copies. No behavior change.
  • VEYYON_TIMING reports the window between process start and the launch card instead of hiding it: the tree now starts at the CLI entry and carries spans for the command load, the launch-card import, the prologue, settings, the theme and the paint, leaving only Bun's own start and the entry's static imports under (before instrumentation).
  • The launch card arrives in about half the time. The binary is now code-split, so the standalone loader links the CLI entry and the launch card instead of the bytecode of every subcommand, tool and agent-runtime module before the first statement runs, and whitespace and syntax minification are on. Measured warm on a pty, the card's first byte goes from 138-151ms to 57-72ms, the first keystroke echoes at 111ms instead of 188-207ms, and the binary is 231.7MB instead of 296.9MB. Function names are still kept, so a stack trace is unchanged.
  • The six modules on the launch path that reached for the @veyyon/utils barrel now import the subpath that owns what they use, so painting the card no longer evaluates the YAML parser, Handlebars and the prompt-variable layer that the barrel re-exports. The launch card's import graph drops from 311 modules to 268, its import span from 32.5ms to 20.2ms, and the card's first byte to 50-58ms.
  • The launch shell no longer evaluates a workspace barrel. The launch card, the editor and the status row reached @veyyon/tui, @veyyon/utils and @veyyon/agent-core for a handful of symbols, and each barrel is export * from over its whole package; every such edge now names the leaf that owns the symbol. Three modules were split along the same line: internal-urls/resolve-sync.ts takes the URL resolver out of the OSC 8 writer, modes/image-reference-markers.ts takes marker rendering out of the blob store, and modes/magic-keyword-notices.ts takes the three hidden notices out of keyword detection. Module evaluation for the three shell entries drops from 147ms to 63ms, the editor from 36ms to 3.6ms and the status row from 78ms to 6.5ms.
  • The status row no longer evaluates the agent runtime, the compaction engine, the provider error registry or the bundled model catalog before the composer takes a keystroke. session/context-usage.ts reached the @veyyon/agent-core barrel for countTokens, its compaction barrel for estimateTokens and @veyyon/ai/utils/schema for two wire helpers, and config/inline-tool-descriptors-mode.ts reached @veyyon/catalog/identity for one family token; each now names the leaf that declares the symbol. The three shell entries evaluate 333 modules rather than 583.
  • The status row reads its premium-request formatter from @veyyon/utils/format instead of @veyyon/stats/format, so painting the card no longer evaluates the stats package. No visible change to the row.
  • The status row's non-message token accounting lives in session/non-message-tokens.ts, so painting the launch card no longer evaluates the compaction layer or the tokenizer through session/context-usage.ts, and config/inline-tool-descriptors-mode.ts reads modelFamilyToken from @veyyon/catalog/identity/family instead of the identity barrel. No change to the counts the row reports.
  • The host capability probe and the environment it measures against moved out of the session budget module into session/cgroup-host.ts, and the capabilities a probe reports no longer carry the field it used to pick a cgroup parent. No behavior change.
  • The first-frame recording is written by startup/first-frame-recorder.ts through the shared atomicWriteFileSync instead of a hand-rolled temp-and-rename, so the cache file is created owner-readable only; the replay reader on the boot path still reaches node builtins only.
  • A cold launch states the context gauge as soon as the session knows its usage instead of when the live status row mounts: the at-rest context record fires on session construction and the launch card repaints on it. A warm launch repaints nothing.
  • The launch card paints the working directory on the status row instead of leaving it blank until the session mounts. Measured on a pty, the row named the directory at 59-62ms with the card rather than at 1067-1083ms; the model, mode and context gauge still arrive with the session, to the right of it.
  • The status row's location is rendered by one owner, modes/components/status-line/location.ts, which the live row and the launch card both call, so the two cannot drift. shortenPath and sanitizeStatusText moved to their own modules, re-exported from tools/render-utils.ts and modes/shared.ts; defaultDisplayRoots and resolveDisplayRoots are now imported from the location module rather than from segments.ts.
  • The launch card paints the git branch on the status row beside the working directory. Measured on a pty, the branch appeared at 59-66ms with the card rather than at 1067-1083ms.
  • The status row's branch is rendered by one owner, modes/components/status-line/branch.ts, which the live row and the launch card both call, so the two cannot drift.
  • Which segments a status-line preset shows is resolved by one function, resolvePresetSegments, so the launch card honors the same preset and git.enabled rules the mounted row does. No behavior change to the mounted row.
  • The branch a repository is on can be read from .git alone through utils/git-head.ts, without running git; utils/git.ts composes that reader with the git symbolic-ref fallback a reftable repository needs, rather than keeping a second copy of the file parsing.
  • StatusLineComponent.watchBranch is watchGitState: one repaint request for every git read the row is painted from, rather than a name that described only the HEAD watcher. No user-visible change.
  • The model and effort a subagent runs are chosen on one of two exclusive scopes. Subagents → Same Model for All Subagents is off by default, and each agent's page under Roster decides; on, Shared Model and Shared Effort decide for every agent while the per-agent rows are hidden and keep what they hold for when the switch goes off. An agent naming neither runs the profile's default model role at medium effort, and subagent.modelByDepth decides nothing in either scope.
  • A subagent definition that names a tool nobody recognizes is reported with the file and the unknown names instead of loading an agent with no tools and a prompt pruned of everything.
  • User-authored subagents are discovered from ~/.veyyon/subagents/, shared across profiles, and enabled per profile.
  • The compaction loader names the engine on every pass, not only a remote one: "Compacting context... (local compaction)", "(openai remote compaction)", "(azure remote compaction)" or "(codex remote compaction)". A codex or azure server-side pass used to be announced as openai, and a local pass was not announced at all.
  • The model the advisor runs is asked in one place: Model → Advisor → Advisor Model, directly under Enable Advisor, hidden while the advisor is off. advisor is gone from the Roles table, which was a second surface for the same slot under a different name; the slot itself is unchanged, so @advisor and any existing modelRoles.advisor keep working.
  • The Advisor rows are contiguous in the Model tab: the Prewalk rows were declared between them.
  • The resolved-model snapshot a warm launch restores is verified against the bytes it was written from instead of being parsed and serialized back to check itself. The file is now a header line then the stage payload, and reading it costs 14.6ms instead of 27.6ms measured on a 9.2MB snapshot; a snapshot written by an earlier version misses and is rebuilt.
  • The launch card paints the real composer instead of a drawing of one. Typing at the card edits the editor the session goes on to use, so nothing has to be replayed into it at handover, and a submit typed before the session exists no longer discards the draft. Measured warm on a pty, a keystroke at the card echoes 3.2ms after the card's first byte.
  • The settings store holds no database handle. AgentStorage.forAgentDir is the one owner of the run's agent.db and opens it on first use, config/legacy-agent-db-settings.ts owns the first-run read of the pre-config.yml settings table, and the launch card no longer evaluates bun:sqlite or the SQLite credential store to read a setting. Measured warm on a pty, the card's first byte goes from 50.5ms to 42.4ms and a keystroke is echoed at 45.6ms instead of 53.6ms; a database that will not open now costs usage statistics with a logged reason rather than failing the launch.
  • The three hidden magic-keyword notices are in session/magic-keyword-notices.ts rather than under modes/, which was the one edge from the session into the UI directory with no drawing behind it. No behavior change.
  • A running autoresearch or autoswarm loop occupies one status row instead of a widget above the composer, and ctrl+x opens a run screen listing the session, the playbook, the run in flight and every logged run, with the highlighted entry in full beside it.
  • A bare /autoresearch opens the run screen instead of leaving the mode; off still leaves it.
  • /autoresearch clear and /autoswarm clear ask before resetting the worktree, naming the baseline commit and how many files hold uncommitted changes, and reject an unrecognized flag instead of resetting.
  • /autoresearch and /autoswarm list their off and clear subcommands before a letter is typed, and clear offers --keep-tree and --reset-tree.
  • A run records the arm that produced it and the arm that certified it, both shown on the run screen.
  • Each row of the run screen's list states the run number, the arm that produced it, its metric, its change against the baseline of its own segment, and whether it is the baseline, the leader, kept, dropped, crashed, failed or flagged.
  • The run screen's session pane opens with the best measurement of the segment, its change against the measurement the segment started from, and the run and arm that produced it.
  • The autoresearch status row prints the best metric with its change against the segment baseline, and sheds the pair together.
  • The autoswarm setup console legend lists only the keys that act on the focused field and the range its arrow keys move within.
  • The autoswarm setup console states the harness runs one iteration costs, as 3 arms × 3 attempts: up to 9 harness runs per iteration.
  • The autoswarm setup console legend reads enter needs a goal while the goal is empty, and Enter starts nothing.
  • The run screen's session pane states the count of runs logged since the best one.
  • The autoresearch status row states the count of runs logged since the best one, and sheds it before the best it qualifies.
  • The run screen's session pane draws the segment's measurements as a row of blocks, one per run, with a gap for a run the harness never measured.
  • The run screen's list states the run in flight and the run awaiting a log by number, with the harness result on the unlogged row.
  • The subagent model resolver states the layer numbering its per-agent chain actually resolves. No behavior change.
  • The status row no longer carries the secrets segment. The secrets id is gone from every preset and from statusLine.segments, and a configuration naming it is rejected; /secret list states what a session has masked.
  • The roster states that an operator may write an agent, and names docs/features/subagents-authoring as the instructions.
  • The subagent authoring page states which frontmatter key spellings are read: thinkingLevel and thinking-level reach the same field, an underscore does not, and the bundled definitions use the dashed form.
  • The rewind and checkpoint entry readers and the side-channel reply bound live in session/rewind-checkpoint.ts and session/ephemeral-reply.ts instead of as file-private helpers in agent-session.ts; the doc comment describing the Anthropic request metadata payload is attached to the function it describes rather than to the tool-order check below it. No behavior change.
  • session/content-text.ts is gone; the session modules that flattened content blocks call the @veyyon/utils owner, which now carries the options that copy held. Two implementations of the same flattening each documented themselves as the only one.
  • The MCP command controller races its OAuth login and its connection wait through withTimeout and raceWithTimeout in @veyyon/utils rather than a file-private copy, and the protocol probe's truecolor bar converts hue through hsvToRgb in @veyyon/utils/color rather than a second implementation of the same conversion. Both emit what they emitted before, byte for byte.
Fixed
  • /login for a provider that takes a pasted API key (OpenAI Platform, Groq, xAI and every other key provider) shows the dashboard where a key is obtained and prompts for the paste, instead of opening a platform login page in the browser as if it were an OAuth sign-in; the setup wizard likewise neither opens nor copies that URL.
  • The /login picker prints api key or browser login beside every provider, and the two OpenAI rows read OpenAI Platform and ChatGPT (Codex subscription), so a key paste and a subscription sign-in are told apart before one is chosen.
  • vey auth-broker login <provider> prints Get an API key at: for a key provider instead of Open this URL in your browser:.
  • The RPC open_url login request carries credential ("api-key" or "oauth") so a host opens only the URL a flow waits on; RpcClient.login's onOpenUrl receives it as a fourth argument.
  • Cancelling or failing a sign-in in the setup wizard clears the provider search that started it, so the next keystroke begins a new search rather than appending to the old one and matching nothing.
  • Tool renderers sanitize raw tabs and shorten embedded home directory paths across error, output, and fallback views.
  • A job poll whose job failed lists each job with its own error again, instead of collapsing to the result text.
  • Print mode exits with code 130 rather than 1 when a turn is cancelled or aborted.
  • The web search command exits with code 2 on missing or invalid query arguments.
  • The TTSR command exits with code 2 on missing required subcommand parameters or unrecognized actions.
  • The shell command exits with code 2 when invoked without an interactive terminal.
  • CLI file argument processing reads attachments via portable node:fs promises rather than runtime-specific file methods.
  • The subagent HUD retains the model badge for running tasks without a description when space is available, rather than dropping it against an unused description floor.
  • Deleting a session in the session selector removes it from the folder and all-projects lists so toggling scope does not restore the deleted entry.
  • ModelSelectorPanel displays 'No matching models' when an active query yields zero results instead of the unprompted 'No models available' login hint.
  • The model browser replaces a Bun ANSI-stripping call with the portable node:util helper on disabled model rows.
  • The model picker avoids re-fetching and re-sorting the catalog on every query keystroke.
  • The thinking effort selector preselects the default row when initialized without a level override.
  • veyyon auth-gateway serve honours accounts.loadBalancing: with it off, one account per provider serves every request the gateway forwards, where before the gateway rotated on each of the several credential reads a single request makes.
  • An autoswarm arm's model switch is ephemeral: quitting mid-arm and resuming the session no longer opens on the arm's model with nothing to restore the session's own.
  • An overlay box narrower than six columns no longer draws a title wider than its own border.
  • A warning repeated verbatim with nothing between the two, as the compaction dead-end notice was at the top of a resumed turn, renders once.
  • Settings selector displays a dimmed '(unset)' placeholder for optional text settings with empty values.
  • The subagent roster list bounds item rows and wraps custom agent hints so the list and navigation controls fit within the modal viewport.
  • The settings selector passes the target pane width to the status line preview.
  • The autoswarm setup console handles Unicode emojis, CJK characters and combining marks on backspace and windowing without splitting surrogate pairs, and supports Shift-Tab field navigation.
  • Autoswarm triage rejects candidate arms whose edits fall inside off-limits directories, where an exact string comparison only matched exact path literals.
  • Autoswarm review assigns the director as the sole reviewer when certification is disabled on the session, instead of creating peer review rings for three or more surviving arms.
  • Autoresearch storage rejects completing or logging an abandoned run, and rejects duplicate log calls on an already logged run.
  • /autoresearch status and /autoswarm status open the run screen, where the word was swallowed as the goal and overwrote what a live session was optimizing.
  • A live session's goal changes only through goal <text> or the setup console, so any other text after the command reaches the model as context for the resume and leaves the stored goal as it was.
  • /autoresearch and /autoswarm offer status and goal in their argument completions, where only off and clear were listed and every other word reached the goal write.
  • Checking out a branch other than the one an autoresearch or autoswarm session records now shows paused · session on <branch> on the status row and keeps the session's runs readable through ctrl+x, where the session was looked up by its branch alone and so read as baseline pending with no runs whenever it was resumed off that branch.
  • An autoresearch or autoswarm loop re-reads the branch at the start of every turn, so checking the session's branch back out lifts the pause and restores the experiment tools, where a checkout mid-conversation went unnoticed and the loop kept its tools and system prompt on a branch it was not on.
  • Turning an autoresearch or autoswarm loop off while it is paused clears the pause, so the status row stops naming a branch to return to and reports the mode as off.
  • Leaving the autoswarm setup console with Escape reports that nothing was started, where it returned to the prompt without a word.
  • Pressing Escape during an autoresearch or autoswarm turn pauses the loop until the next message, where a measurement waiting to be logged made the loop resume itself on the very next turn and the interrupt did nothing.
  • A hidden continuation that was waiting for the agent to go idle no longer starts a turn after Escape ends the one it was waiting on, where an autoresearch stall nudge queued a turn earlier restarted the stopped loop three milliseconds after the interrupt.
  • An autoresearch or autoswarm loop no longer diagnoses a stall when the turn that ended was a session continuation and the loop's own resume has just been accepted behind it, where the false stall queued a second hidden turn and counted against the budget that turns the loop off.
  • /autoswarm <text> over a live session opens the console on the session's stored goal, where it prefilled the typed text and Enter overwrote the goal with a message meant as context for the resume.
  • A slash command that opens a console or a prompt while the agent is idle no longer sits under the Working… loader and its elapsed clock; a prompt raised mid-turn keeps the turn's loader.
  • The autoswarm setup console's breadth, attempts and certification answers outrank the breadth, attempts and certify arguments the model passes to init_experiment, where a guessed breadth: 1 turned a configured swarm into a serial loop; the tool's reply states the configuration in force and that the console set it.
  • A metric unit that is a word is spaced off the number (1,596,000 comparisons) while a symbol or abbreviation stays attached (192.78ms), and a metric_unit that repeats the metric's own name is dropped, where every surface printed 1,596,000comparisons.
  • The autoresearch status row pluralizes its run and arm counts (1 run, 2 arms), where it read 1 runs.
  • log_experiment rejects status: "keep" for a run that did not improve on the segment's best unless a justification is given, where a loop kept eight flat runs in a row as eight commits that changed nothing the harness could see.
  • skill://<name> and skill://<name>/SKILL.md read a skill whose SKILL.md is a symlink into a directory outside the skill's own, where the read was refused as an escape of the skill root; every other file under the skill still has to resolve inside it.
  • /account status, /account refresh, /lsp and /trust print their report as a titled transcript block at the rail, where the lines landed as a bare paragraph that read like an assistant message.
  • Logging an arm whose model restore is refused, because the session model lost its key mid-run, reports that the session is still on the arm's model instead of recording the restore as done, and the next arm's restore target stays the session's own model rather than the arm it was stranded on.
  • An autoresearch or autoswarm turn that ends with no experiment tool run, no measurement waiting and no resume armed is steered back to the loop on the next turn; after three such turns in a row the loop turns itself off and states how to reach the session's runs, where the mode stayed on and the status row read as a live experiment until the user typed something.
  • /autoswarm goal <text> opens the setup console with <text> in the goal field, where the field read goal <text> and that literal was written as the goal on start.
  • /autoresearch clear --keep-tree, and a clear off an autoresearch/* branch, leave every worktree file in place, where both deleted autoresearch.sh and the other loop files from the worktree; a confirmed reset restores the committed harness and removes only the legacy files the prompt forbids, and a clear on a project that never ran a loop no longer creates the run store.
  • The interactive console (bash with a PTY), the autoresearch dashboard and every other ui.custom(..., { overlay: true }) surface sit above the composer zone instead of covering the whole screen: the prompt, status line and footline stay painted while the overlay is up, and the console box is sized to the rows above them.
  • A session on a model whose thinking blocks are bound to their conversation prefix, such as Claude 5.1 and later, leaves an already-sent tool result in place instead of superseding it mid-history, which invalidated every thinking block recorded after it and drew a 400 on the next turn.
  • The installer refuses to replace a binary whose only ownership record is a pre-identity v1 receipt, instead of moving it aside. That receipt vouches for the path alone, so a user who deleted the installed binary and put their own file at the name left exactly one behind, and it was being read as permission to displace their file.
  • veyyon agents unpack writes to ~/.veyyon/subagents, the directory subagent discovery reads, instead of the profile's agent/agents dir, where an unpacked definition was reported as written and then never loaded.
  • veyyon config set and veyyon config reset no longer exit 1 with an ENOENT on a profile that has never been launched: the save now creates the profile's agent directory before it takes the config file's lock, which refused a parent directory that did not exist yet.
  • A /btw or /omfg reply long enough to be truncated no longer ends in a replacement character when the cut lands inside an emoji: the trim counts UTF-8 bytes but removes UTF-16 code units, so it could stop on half a surrogate pair.
  • A malformed content block no longer opens a blank line in a session listing or in rendered history. A text block whose text is absent or is not a string contributed an empty part, so the join placed a separator beside it, while a thinking or tool block carrying just as little text was skipped outright.
  • The composer hairline and the transcript rules no longer change shade about half a second after the launch card appears: the card mixes them out of the background this terminal reported on the previous launch instead of a static token, and tui.paintGround on auto decides the paint from that same recorded background rather than repainting the whole window when the terminal answers. The background is recorded per terminal in cache/launch-facts.json, whose shape version is now 4.
  • The launch card's context gauge no longer jumps when the session mounts in a project it has never measured: the reading filed under the model is now that reading with the measuring project's context files subtracted, so a card seeded in a heavy repository no longer states 77% left where the session settles at 88%. A project's own reading is unchanged and still wins where it exists.
  • The session mount no longer forces a full-viewport repaint over the launch card, so the screen no longer flashes and darkens at handover; the mount now writes only the rows whose content changed.
  • veyyon agents unpack writes bundled definitions to ~/.veyyon/subagents, the directory agent discovery reads, instead of the active profile's agent/agents, where nothing loaded them; the --user and --project flags are gone, and --dir still writes to a path you name.
  • The launch card states the model name, git state and context percentage recorded at the end of the last launch instead of placeholders, so the status row and hero do not change when the session mounts; each fact falls back to its placeholder when the release, the model or the project changed.
  • The launch card's status row resolves a linked git worktree the same way the live row does, so the path the card paints, its ◫ marker and the truncation it forces survive the session mount without being rewritten.
  • The launch composer's footline reads the draft's token estimate from the same editor the session mounts, so the right zone is on screen with the first keystroke instead of appearing when the session does.
  • certify_arms is attached only to a session whose breadth is above 1, so a serial autoresearch loop is no longer offered a tool that triages arms it does not have.
  • An autoswarm winner has to beat the segment's baseline, not merely the worst of its sibling arms: an iteration where every arm regressed reported a winner, which was then logged as an improvement and re-applied.
  • /autoresearch clear --keeptree and any other misspelling of --keep-tree no longer fall through to resetting the worktree, which is the opposite of what the flag selects.
  • The autoswarm setup console keeps its hint column still while a value is adjusted, and wraps its explanatory line instead of cutting it on a narrow terminal.
  • The autoresearch status row reads the breadth once, so the first turn of a swarm no longer prints autoswarm with no arm count: the name came from the configured breadth and the arm count from the stored session, which do not agree until init_experiment runs.
  • The run screen keeps esc close on its footer at every terminal width, shedding the navigation hints first, instead of truncating the exit hint off the right edge of a narrow terminal.
  • The autoresearch status row drops whole segments, least informative first, when the terminal is narrower than the row, and rebuilds on a resize, so the ctrl+x runs chord that opens the run screen is on screen at every width.
  • A crashed run reports what its harness measured before it died, and states that it has no metric when the harness measured nothing, instead of formatting the zero the log call was required to supply as a measurement, which listed a segfault as the fastest result of a session where lower is better.
  • The run screen lists each metric once: the primary metric a log call repeats among its secondary metrics is no longer stored or rendered as a secondary of itself, which printed a second unitless copy of every measurement and a -100.0% row for a crashed run.
  • Every surface reporting the best run of a segment names the same run. The run list, the session pane, the status row and the prompt built from the session each ran their own scan, and two of them counted a run logged with the placeholder zero the log call requires of a run that measured nothing; where lower is better that zero is the best value there is, so the list tagged a run best that the pane beside it did not.
  • The run screen prints the artifacts directory with the home directory collapsed to ~, so a screenshot of a loop no longer states the account name and profile layout it ran under.
  • The run screen fits a terminal shorter than fourteen rows: it wrote a fixed fourteen rows whatever the height, so a ten-row terminal lost the four rows holding the footer and the border.
  • The run screen fits a terminal narrower than 29 columns: the sidebar is sized from the available width rather than its own floor, and every row is cut to the width it was handed, so the border no longer wraps into rows the card did not count.
  • The run screen stacks its list above its detail on a terminal too narrow to carry both beside each other, instead of leaving the detail two columns wide and printing every field of the selected run as a single letter and an ellipsis.
  • The run screen names an autoswarm the setup console configured before the first init_experiment call, instead of titling it "Autoresearch" and reporting "serial, no arms and no review" for a swarm whose breadth was already set.
  • /autoresearch clear resets to the baseline of the session on the branch it is standing on rather than the newest open session, so a second worktree on another branch no longer selects the commit this one is reset to, nor has its session closed instead.
  • The run screen pages its detail pane with pgup and pgdn, and closes on escape only: q closed the surface, so filtering the run list for a label containing a q lost the screen mid-keystroke.
  • The status row of a run in flight advances its elapsed time once a second instead of freezing at the value it had when the run started, and its clock stops as soon as no run is in flight and the run screen is closed.
  • A search filter typed into the run screen survives the loop logging a run and the clock advancing, instead of being cleared once a second while a run was in flight, and the highlighted run stays highlighted when rows are added above it.
  • /autoresearch clear rejects the reset when it cannot read git status, instead of prompting about zero uncommitted files and then discarding whatever was there.
  • The GitHub link in the veyyon.dev navigation carries an accessible name, so a screen reader announces the destination instead of an unnamed link holding a hidden icon.
  • veyyon config set and veyyon config reset create the profile directory they write into, so the first command run on a machine that has never launched a session persists the setting instead of exiting 1 with an ENOENT on that directory.
  • Raising breadth, attempts, parallelism or certification on a live session is written to the session instead of being dropped, so the loop runs the shape the setup console last showed.
  • The launch card keeps one set of facts per project instead of one for the whole machine, so working in two projects no longer erases both and leaves the context gauge reading ? on every start. The file is versioned, bounded to the 24 most recently written projects, and a copy written by the previous shape is discarded rather than read.
  • The launch card records what a project knows and what a model knows separately, so working in two projects no longer erases both and leaves the context gauge reading ? on every start, and a project opened for the first time states the model's display name and effort instead of a raw id. The file is versioned, each map is bounded to its 24 most recently written entries, and a copy written by a previous shape is discarded rather than read.
  • The launch card states the context gauge in a project it has never measured, using the at-rest reading the configured model last took anywhere, so a project opened for the first time draws a real bar instead of ? left and an empty one; the project's own reading still wins where it has one, and the gauge falls back to ? only until that model has idled once.
  • The launch card states the effort the last launch ran at, so the status row no longer grows a @high tail when the session mounts and shifts every segment beside it; the rung is dropped when the model changes and erased when the row stops printing one.
  • The launch card prints the configured model id's last path segment rather than the whole qualified id, so a namespaced id no longer costs the status row its context gauge on the first launch of a project.
  • The launch hero states the configured model instead of no model yet · /login when no display name has been recorded yet.
  • The context percentage the next launch states is recorded only while the session is running the configured default model, so a session started with --model, switched with /model before anything was sent, or fallen back to another model no longer leaves the card a gauge measured against a window its model does not have.
  • The context gauge renders every percentage at one width, so a reading that arrives or changes no longer shifts the status row beside it.
  • The launch card lays its status row out against the same width the live row uses, so a narrow terminal no longer shows the card keeping a segment the running session immediately drops.
  • The screen no longer shakes while an answer streams into a viewport it has not filled: each streamed chunk repaints its own block alone, and the anchor fill above it is now resized in that same frame rather than reused at the previous frame's height, which composed one row past the viewport and moved the window per row of the answer.
  • The composer no longer lifts off the bottom row for a frame when content collapses (a tool card closing, the working indicator retiring): the anchor is sized from the children about to render rather than from the frame that already composed, so the placement no longer needs a second paint to correct it.
  • Context budgeting is unchanged from 1.3.0: the unreleased reserve for the model's output allocation is withdrawn, because subtracting it from the usable window moved every model's compaction threshold, roughly doubling how often compaction fired and invalidating the prompt cache on each pass.
  • A turn too large for compaction to summarize is truncated in the middle, keeping the head and the tail, instead of pausing automatic maintenance; the removed text is written to a recovery artifact the notice names. A session whose newest turn was a single oversized message could previously make no progress, and rewinding the tree did not clear it.
  • A payload the outbound secret scan refuses for its size is treated as a context overflow, so the session compacts and retries instead of stopping at "the provider request exceeds the confidentiality scan byte limit" on every attempt. The scan runs before the request is sent, so nothing else had reported the turn as too large.
  • An image a kitty-protocol terminal cannot be handed reports the format as the reason instead of claiming images are switched off, and a picture whose conversion is still running no longer prints a placeholder that a moment later becomes the picture.
  • A key pressed before the launch card appears is drawn into the card's composer about a millisecond later instead of 156ms later, so the composer no longer sits on screen ignoring what is typed into it while the main module loads.
  • The status row's dirty marker appears when git status answers instead of waiting for whatever redraws next, which in a resting session is the next keystroke; the row had been showing a clean branch over a tree nothing had looked at.
  • The status row keeps the dirty marker the launch card painted instead of dropping it for the width of its own git status, so a handover on a dirty tree no longer shows the branch change colour twice; a scan that finds the tree really did move still repaints it once.
  • A turn that ends on text after a tool call is recognized as finished even while a session subscriber is still running, so the todo reminder, the rewind pass and the session-stop hooks no longer skip a turn whose final message arrived behind a slow subscriber.
  • A memory limit pins the capped subtree's swap to zero, so the cap bounds the whole anonymous footprint; while unreleased a 256 MB machine cap let a single process reach 5,520 MB by swapping.
  • The machine limit requires a parent that delegates two cgroup levels, so a host that delegates one — a container whose cgroup root holds processes — reports per-session limits held and the machine tier unheld, instead of reporting a machine cap the kernel never applies.
  • The CPU-limit probe and the limiter resolve one environment, so the probe can no longer report support for a cgroup path the limiter does not write to.
  • A probe directory left behind by a killed veyyon no longer makes the only usable cgroup parent look unusable, which dropped the machine to no budget at all until it was deleted by hand.
  • /cpu-limit status reports whether the kernel is holding the machine-wide limit, naming the resource it refused, instead of printing the configured cores on a host that holds none of them.
  • /cpu-limit states that a session CPU cap above the machine cap is bounded by it, rather than printing the two numbers with nothing relating them.
  • A background bash job joins its session's CPU and memory budget instead of running outside every cap.
  • A model whose compaction route answers 404 reports the downgrade to local compaction once, instead of once for the 404 and again in different wording on the next compaction.
  • A session that never enabled goal mode no longer reports "Goal mode stopped driving" after three consecutive provider-killed turns; the failed-turn counter and its stand-down warning now require a running goal.
  • A goal now starts with its full failed-turn tolerance instead of inheriting the failures an earlier goal in the same session left behind, which stood it down on its first error rather than its third.
  • A turn-ending provider error too long to render inline now reports how many lines were dropped instead of ending without a trace.
  • The installer asks what is already installed before downloading, so a machine already on the released version finishes in seconds instead of fetching the whole binary to discard it.
  • The installer repairs an install whose binary was replaced since it was written — a local build copied over it, or a write interrupted mid-swap — by moving that file aside and installing, instead of refusing and leaving the machine on the old version; a file at a path the installer has never installed to is still refused untouched.
  • A completed goal now reports the tokens the turn that completed it spent after the goal tool ran, including a subagent that returned in the same batch, instead of stopping its count at the tool call.
  • /cpu-limit no longer sets a budget: it reports both scopes and lifts this session's CPU cap, and points at /settings under Resources for configuration.
  • An ACP client following a tool-call location now opens the file, not a name ending in the read tool's line range.
  • The settings screen states that left returns to the category list, and no longer expands a row that has no description, which consumed the next left with nothing on screen to show for it.
Removed
  • subagent.sharedModel, subagent.model, subagent.thinkingLevel and subagent.modelByDepth decided the model and effort for every subagent at once and are rejected; a config still holding one is reported once, naming the agent page that replaces it.
  • The --subagent-model launch flag, which set the model for every subagent in the session.
  • The --project flag on veyyon agents unpack, which wrote definitions to ./.veyyon/agents; there is no project scope for subagent discovery, so those files were never loaded.
  • The tagline under the wordmark on the session welcome hero, which the launch card and the mounted hero each printed.
Breaking
  • The model-facing workspace search surface is now one mandatory search tool taking ordered required type ("files" | "text" | "structure") and input, replacing the separate glob, grep, and ast_grep tool IDs. Retired per-engine and search.enabled values are discarded while text-context values and persisted tool inventories migrate.
Added
  • search takes paths for a text search, returning the matching files with per-file counts instead of match lines; the shell route it replaces (rg -l) is intercepted, and searching buildSystemPrompt under packages/coding-agent/src costs 3,492 tokens as match lines against 215 as a file list.
  • Esc pressed twice within half a second over a composer holding text discards the draft; undo brings it back, and a single Esc still leaves the draft alone.
  • /advisor reports advisor status, opens the WATCHDOG.yml roster editor and applies a save to the running session, starts or stops the advisor for the session, and copies the advisor's own transcript; the subsystem shipped complete but no command, key or menu row reached it.
  • session.newKeepsBackground decides what /new does to a turn still streaming: off (the default) stops it and closes its provider stream before the new session starts, on keeps the old conversation running and says which one.
  • The status line carries a background chip counting conversations this process is still running that no screen is showing, present in every preset and silent at zero.
  • The terminal renderer composer zone gains a formal defect oracle and automated invariant sweep suite covering prompt counts, output bleed, row mixing, footer alignment, mouse click routing, caret positioning, overflow, pad transparency, hairline integrity, and virtual scroll stability.
  • prewalk.cheapModel and prewalk.strongModel configure the cheap model prewalk switches into at the first edit and the strong model it starts on.
  • /prewalk accepts an optional model argument to arm a per-session target model override.
  • edit.afterEdit selects what one turn that changed files owes before it finishes: verify (the default) runs one check when none followed the last edit, review reads back every code file changed since the last user message and judges correctness, maintainability and cross-file contracts, off neither; the legacy edit.critiqueCodeMutations boolean migrates to review or verify on load.
  • Configurable launch.cleanupWaitMs setting (default 15 minutes) purges exited launch daemon records from memory and disk after a retention TTL.
  • Exporting a session to HTML streams the snapshot into the output file instead of assembling the whole document in memory, taking an 80MiB transcript from 1007MiB of peak resident memory to 532MiB with byte-identical output.
  • A session snapshot that contains a reference cycle fails the HTML export with an error instead of writing until the disk fills.
  • bench/session-memory.bench.ts reports heap after a forced GC, current RSS and high-water RSS at each of three phases (module baseline, SessionManager.open, buildSessionContext) over a synthetic transcript sized by SESSION_MB.
  • Added model.toolCallLoopGuard.readSubsumptionThreshold (default 3) to steer models that re-read unchanged code lines back-to-back before consuming full context.
  • VEYYON_DEBUG_STARTUP=1 writes one line per phase of a prompt submission (compaction check, plan arm, context build, memory context), so a slow submit names the phase that spent the time.
  • read takes depth and limit arguments for directory listings, and a read of the session working directory root with neither now returns a concise top-level listing with per-subdirectory entry counts instead of the recursive tree.
  • A tool result that carries an image now states whether the picture reached the screen, so a model reading a file describes what it shows instead of reporting that it displayed it.
  • A picture the block gives up on after the fact, because the session's image budget demoted it or a Kitty session could not convert it, is stated to the model as undrawn instead of being reported as displayed.
  • statusLine.segmentOptions.path.displayRoots names the workspace roots the working directory is shown relative to, with ~ accepted for the home directory, replacing the two hard-coded conventions (~/Projects and /work); the first matching entry wins and a non-absolute entry is dropped and named in the log once.
  • read accepts a semicolon-delimited list of internal resources (skill://demo/one.md;skill://demo/two.md), the same list form search takes, and returns one section per entry.
  • Eval kernels gain kv, a bounded JSON store under the session's artifacts directory that survives kernel resets and is shared between JavaScript and Python without cross-session filename collisions or lost concurrent updates, and defs(), which lists the names user code has defined in the kernel.
  • Every supervised process termination records which component ended it and why, with distinct attribution for each path (operator stop, signal, restart, broker shutdown, idle reaper, OS signal, broker recovery, launch failure, external signal, and natural exit); launch list output shows the lifetime owning condition and retained completion records with exit codes, reasons, and output tails, queryable after the name is reused and across broker restarts.
  • A click on the working directory, git branch or pull-request text in the composer status line widens the location to the row and retracts the model chip to pay for it, animated over the shared expand curve, and a second click reverses it; display.transitions: off lands on the click frame.
  • /omfg forges rules that carry the extended TTSR frontmatter (astCondition, interruptMode, pathScope, repeatMode, repeatGap, repeatCompactions, warmupMatches), confirms ast-grep conditions against the conversation's tool history through the same gate chain a live stream applies, and fails loudly on a malformed optional field instead of dropping it.
  • /omfg saves forged rules to the active profile's rules directory only; the project target is gone, because project .veyyon/rules was never discovered across sessions.
  • Settings → Stream Interrupts (TTSR) groups the profile's own rules under a leading User created section instead of From native, ahead of foreign-tool and built-in sections.
  • An opt-in eval.pyWorkspace experiment teaches the agent to keep large tool results and repeated repository operations inside the persistent Python kernel, reducing intermediate transcript output without changing kernel execution.
Changed
  • A search result carries its limit and truncation notice again. The unified tool nested each type's result one level deeper than the output layer reads, so a capped search files returned five paths and said nothing about the cap, no search of any type ever named a raised limit or a page selector, and an already-spilled result was spilled and truncated a second time.
  • A capped structure search pages with skip and names the match count instead of advising limit, which the tool accepts only for a file search: following the old advice cost a rejected call, an error result and a retry.
  • A structure-search metavariable binding is elided past 60 bytes, because a binding is a source range inside the match the result has already printed line by line. A multi-node capture arrives joined onto one line, so $$$BODY was a second copy of the whole body, and a capture spanning lines entered the result with no line number for a caller to anchor on. Over five patterns of this repository the bindings cost 8,414 tokens on top of 9,052 tokens of match text, and the same five searches now cost 12,769 tokens against 19,826. A value inside the bound is kept, since it names which fragment bound to which variable for about ten tokens.
  • A text search states its per-file match cap and its internal ceiling. A search of one file returned the first 200 matches and said nothing, so a file with 4,000 matches read as a file with 200; a multi-file search capped each file at 20 with the same silence; and a search that stopped at the 2,000-match native ceiling reported a file count that was a floor as though it were a total. skip pages files and reaches nothing past either cap, so each notice names narrowing the pattern.
  • A search call rejected for a cross-type field names the fields that type accepts. In a recorded trial a model sent limit to a text search, read Search type "text" does not accept: limit, and spent a second request rediscovering the field by removing it; the rejection already costs a full round trip, so it now states the set that would have worked.
  • A read whose line bound came from the default rather than from the caller is held to the tools.artifactSpillThreshold budget every other tool result is held to, instead of a compiled 512-bytes-a-line allowance: reading packages/coding-agent/CHANGELOG.md cost 19,768 tokens and now costs 10,586, and :50 cost 23,643 and now costs 10,357. A read that names a line count still returns those lines.
  • Reading a notebook, a converted document, an archive entry, a URL body or an internal resource is bounded by tools.artifactSpillThreshold instead of a compiled 50KB constant the setting could not reach, so lowering the setting now lowers those results as it already lowered bash, eval and ssh output.
  • A @path mention in a prompt is bounded by tools.artifactSpillThreshold instead of a compiled 50KB constant, and a capped mention names the budget in effect rather than that constant; the file body, an over-long first line and a mentioned directory's listing all take the same budget.
  • A multi-range read whose first line alone exceeds the output budget names that line and its size, instead of a backwards [Lines 1-0 …] window whose continuation selector pointed back at the line that just failed.
  • A directory listing from read takes the tools.artifactSpillThreshold budget: the unsliced listing followed a compiled 50KB constant the setting could not reach, and a listing sliced by :N-M carried no byte bound at all. A capped slice names how many listing lines remain and the selector that continues them.
  • An archive directory listing from read takes the tools.artifactSpillThreshold budget instead of a compiled 50KB constant the setting could not reach.
  • A structural summary from read stops at read.defaultLimit lines or the tools.artifactSpillThreshold budget, whichever binds first, clips lines to tools.outputMaxColumns, and names the bound that stopped it and the line that continues it. A summary is a projection over the whole file and carried no bound at all, so a selector-free read of a declaration-dense file returned the whole projection: packages/catalog/src/discovery/cursor-gen/agent_pb.ts cost 326.5KB, about 82,000 tokens in one result, and now costs 3,138 tokens. Across 4,039 summaries in this repository the median is 57 lines; the 54 files over 300 lines cost 311,211 tokens between them and now cost 174,450.
  • A PDF image-member list and an agent://<id>/<field> extraction from read take the tools.artifactSpillThreshold budget. Both returned the whole collection whichever size the document or the artifact happened to be: a scanned PDF listed every extracted image, and an extraction takes no line selector, so nothing could page it. A capped list names how many members it dropped, and a capped extraction is cut by bytes and names its full size and the URL that pages it.
  • A read of a directory that names no depth lists the top level instead of recursing two levels: every entry up to 100, each subdirectory annotated with its direct-child count, and a footer naming depth: 2 for the recursive listing. Only the session working directory root answered this way before, and the second level is a default nobody asked for: over seven directories of this repository the selector-free read cost 27,568 tokens and now costs 7,131, with packages/coding-agent/src at 8,163 against 1,273. A directory wider than 100 entries states how many it held back and names depth: 1 for the flat listing of all of them; a caller that names depth or limit is unaffected.
  • A search_tool_bm25 call activates the tools it matched well instead of every ranked match up to its limit of 8, which under tools.discoveryMode: all is the whole hidden set. An activated tool's schema is re-sent on every later request of the session, so one loose query cost more than the mode saved: "keep track of what is left to do" activated todo plus set_cwd, task and web_search for 2,239 tokens a request where todo alone costs 1,048, and across eight such queries the tail was 3,319 of 7,349 tokens. A tool now activates when it scores at least half the best match, so a near-tie still activates and the same queries cost 4,030; a weaker match is returned in also_matched and a query naming it activates it.
  • tools.artifactSpillThreshold bounds what a spilled tool result costs a request, not only when an artifact is written. The window kept inline was tools.artifactHeadBytes + tools.artifactTailBytes, so a 404KB result delivered 39.8KB at a threshold of 8KB and the same 39.8KB at 200KB; it now delivers 7.8KB and 39.8KB. The head and tail settings shape the window inside the budget and keep their ratio, and the elided bytes stay recoverable through the artifact:// id. This covers every tool result except read, which is bounded by the lines its caller asked for.
  • The debug tool loads only where at least one configured DAP adapter command resolves, so a host with no debugger installed no longer pays about 1,000 tokens of debug schema on every request for a tool whose every call would fail on the missing adapter.
  • search.contextAfter defaults to 1 line instead of 3. A tool result is sent again on every later request of the session, so each line of a search result is billed once per remaining request; over eight searches of this repository the wider window cost 16,836 tokens against 11,483.
  • The eval prompt-override registry, the system-prompt eval hooks, the argot cache and the reroot hint name @veyyon/evals paths instead of the retired @veyyon/metaharness, @veyyon/typescript-edit-benchmark and @veyyon/deepswe-bench packages. No behavior change.
  • The /providers account card filters its provider sidebar: ctrl+s enters search, typing narrows the list by fuzzy match on provider name and id, the arrows move within the matches, and esc leaves search before it closes the card (#922 by @Crqptx).
  • A parked subagent is pruned rather than closed: subagent.autoClose.enabled, .parkedMs and .waitingMs are now subagent.prune.enabled, .afterMs and .waitingAfterMs, existing config files migrate on load, and the settings tab states park and prune as two stages in their own groups.
  • A parked subagent keeps its roster row for an hour, and two hours when it stopped waiting on a peer, instead of five and thirty minutes.
  • A subagent restored from a previous run is aged from its own transcript rather than from the moment this session found it, so restored agents no longer sit at "just now" forever and are pruned on the same budget as the rest.
  • session.newKeepsBackground states that a change needs a restart, because switching it does not affect the running session (#928).
  • A turn that changed files takes at most one continuation before it finishes; the verification pass that always ran unconditionally is now the verify value of edit.afterEdit and no longer stacks a second forced continuation under the review pass.
  • The Julia, Python and Ruby eval kernels share one execution loop instead of three copies of it; no change to how a kernel behaves.
  • Reading a file or fetching a URL no longer loads the document converters, and a web search no longer loads the browser fingerprint generator, because the constants those paths wanted are separated from the libraries that sat behind them, taking about 40ms off session startup.
  • The launch card is painted and flushed before the agent runtime graph is loaded, taking an interactive launch from a blank terminal for 760ms to a typable composer at 111ms.
  • The agent runtime is loaded in stages that hand the event loop back between them, so a character typed while the launch card is up is drawn in 6ms instead of 198ms; the load itself takes the same time.
  • /agents is the subagent dashboard: the surface is named that in its title, its /agents description, its keybinding descriptions, /hotkeys and the handbook, in place of "Agent Control Center".
  • The subagent dashboard no longer opens across every conversation the process is running: /process-manager and the a scope toggle are removed, and the card is scoped to the conversation on screen.
  • A locally built binary minifies identifiers the way the released one does, cutting it from 310.7MB to 303.4MB and its launch card from 143ms to 131ms; function names are still kept, so stack traces are unchanged.
  • veyyon --help renders its command list from registry summaries verified against command statics and loads only the hidden default command for its flag table, reducing a measured warm Windows invocation from 1.2 seconds to 0.13 seconds.
  • The CPU model is read once per process instead of on every system prompt build, removing about 30ms from the window before the composer accepts input.
  • No user-visible change: the once-per-process CPU model cache gained a reset the test suite calls, so a suite that fakes the platform reads its own answer instead of the one an earlier suite in the same process cached.
  • Row band painting compiles its escape pattern once for the process rather than once per painted row.
  • The default launch command imports the session runtime and ACP terminal authentication only when it runs, so loading its flag table no longer imports the runtime graph.
  • The vibe screens, the image-inspection call and an LSP hover code block draw no border of their own inside a tool block, so a block keeps one left edge; a tree connector remains only where a row belongs to the row above it, in the eval value tree, the search line gutter, the job tree and the LSP reference tree.
  • Classified runner output (cargo, bun, Go, ctest, dotnet, clippy, golangci-lint, Gradle lint, pytest, and tsc/eslint-family) opens with a result-contract header, [clean] <command> or [errors] / [errors N] <command>, carrying the verdict above a body of retained diagnostics.
  • Files → LSP is one enterable row whose nested page independently controls language servers, the agent tool, diagnostics after write or edit, format after write, lazy startup, and diagnostics deduplication; --no-lsp still disables the full stack.
  • Startup paints the resting composer, with its real hairline, ghost prompt and exact row count, in the first frame from one static component shared with the mounted zone, instead of reserving eight blank rows until mode initialization finished and letting the prompt arrive seconds into a cold launch.
  • Multi-target structure searches now execute concurrently while preserving globally ordered paging, totals, parse errors, cancellation, and target-order failures.
  • A picture a terminal will not draw now leaves a row naming the file, the media type, the pixel size and the cause, in place of [Image: image/png], including when a Kitty session cannot convert it to PNG.
  • Session creation overlaps two serial file reads with neighboring startup work: rules discovery starts as soon as the session context loads rather than after the skills await, and the vault key read starts before the secrets, env and vault entries load.
  • ModelRegistry persists its static bundled, cached-standard and cached-discovery layers to a content-verified resolved-models.json that a warm launch restores instead of rebuilding, invalidated by catalog, cache-row and custom-model content changes or a cached row crossing its 24 h freshness window, but not by SQLite sidecar churn or a provider re-verifying models it already had.
  • A failed MCP tool call decides on a reconnect from the shared socket vocabulary plus this layer's own stale-session rules, so an unreachable or unresolvable host reconnects the server the way a refused connection already did, while a live server answering 500 or holding a request past its deadline stays a failed call.
  • The debug log records which classification rules decided a failed turn's retry, next to the classified kind, so a retry nobody expected is diagnosed from the log instead of by re-reading the provider's sentence.
  • The browser tab worker and supervisor state why each teardown step and each optional probe discards its failure; behavior is unchanged.
  • The browser tab worker and supervisor reach bestEffort and optionalResult through @veyyon/utils/discarded-fault rather than the package barrel; behavior is unchanged.
  • Daemon completion parsing and eval-store serialization errors use shared type guards; behavior is unchanged.
  • Broad multi-file text searches now keep only deterministic representative matches inline and save the complete formatted result behind an artifact:// reference. The preview budget follows the turn-aware output curve from an 8 KiB search ceiling (~2 KiB early at turn 0), emitting up to two representative matches per file while preserving counts and warnings; explicit single-file and line-range searches retain their full output, and only visible representative lines are recorded as seen for anchored edits.
Fixed
  • Tool discovery ranks a tool on its full description instead of its one-line summary alone, which left 96 to 99 percent of each tool's own text out of the retrieval corpus. Over the 17 built-ins, launch scored zero for "tail the output of a server I launched" and eval scored zero for "evaluate javascript and keep the variables", while short-description tools such as goal and set_cwd ranked in six of ten realistic queries; launch now ranks first in eight of those ten.
  • Tool discovery indexes a compound word both whole and in parts, so a lowercase query reaches the word it names: SQLite was indexed only as sq and lite, TypeScript only as type and script, and IPython only as i and python, so queries spelling any of them scored zero against the tool that documented it.
  • A search limit notice is true of the result it prints beside. A text search over 84 files reported "at least one file had more than 20 matches" for a page whose largest file held 12, reported "stopped at its internal ceiling of 2000 matches" for a result of 63, and marked its complete file total "84+": the per-file flag was computed over every matching file rather than the 20-file page, and one flag meant both an exhausted 2000-match fetch budget, which leaves files unopened, and a single file's clipped match list, which does not.
  • A structure search excludes matches in prose grammars (markdown, reStructuredText, AsciiDoc, LaTeX, plain text, logs, CSV, TSV) and states how many it withheld, instead of reporting documentation paragraphs as code matches: over this repository logger.warn($$$ARGS) returned three changelog paragraphs averaging 2,000 characters, none of which contains the string logger.warn.
  • The search tool states that case is on by default and that passing false matches case-insensitively, so a case-insensitive text search no longer depends on guessing the default.
  • A directory listing reports a native addon that could not load instead of answering "(empty directory)". A container whose glibc was older than the shipped addon required got an empty listing for a full checkout on the first read ., both in the tool result and in the workspace tree the system prompt carries.
  • A directory listing reports the errno for a directory the process cannot open instead of answering "(empty directory)", which a model reads as a directory worth no further look; only a directory that is not there still answers empty.
  • An explicit --model pointing at a dynamically-discovered model (a providers /v1/models` entry or models.dev overlay absent from the bundled catalog) no longer fails with "not found among N models" when the background discovery refresh has not completed before model resolution. The deferred pattern path does a synchronous cache-aware discovery pass when none of the patterns resolve against the static catalog, mirroring the fallback already present for default-role models.
  • Unified search now preserves purpose-specific field semantics through the Antigravity tool-schema adapter, replaces primitive search tools in explicit tool lists, keeps plan/subagent/bash guidance aligned with the active tool, and redirects intercepted shell searches to search.
  • A bash-interceptor rule that still names a retired search primitive (grep, glob, find, ast_grep) now redirects to search naming the type field the tool accepts, instead of the purpose field it rejects.
  • File and structure search results are head-truncated at their own byte budget with the full output saved to an artifact, instead of relying on the shared spill layer's middle-elision. File search uses a 4 KB head window (paths are dense and mtime-sorted, so the most recently modified files are on top), and structure search uses the same 8 KB budget as text search. Both recover the full output through an artifact:// footer.
  • veyyon --help describes the grep dev command as the standalone native text-search probe, which is what the command itself says, instead of naming the retired standalone grep tool.
  • Bundled edit and write guidance now uses TTSR's deferred reminder path instead of inheriting the global interrupt policy and aborting the active model response. Every bundled tool-scoped rule must now declare its interrupt policy explicitly.
  • A streaming answer lands in the empty space below the conversation instead of pushing every row already on screen up one row per token, so the screen no longer shakes while a model talks into a viewport that is not yet full.
  • A settings search box reduced to nothing but spaces leaves search and shows the settings list again, instead of holding an apparently empty box over zero matches until esc.
  • A permission prompt for a long command keeps its answer rows on screen: the card sheds lines from the command, saying how many it dropped, instead of clipping the option list off the bottom.
  • A session file that another window wrote its session_exit record into no longer reports that window as a live second writer, so a session whose duplicate window was closed by SIGHUP stops telling the operator to close a session that has already closed.
  • veyyon --resume <id> finds the session under any profile, so the id printed on exit resolves after relaunching under a different one instead of reporting the session as not found.
  • The collab host, guest client and relay socket load when /collab or /join runs instead of during every interactive startup, and a settings domain reads the relay default from @veyyon/wire rather than through the collab protocol module.
  • Argot's dictionary generator, corpus walker and project vocabulary load when a project dictionary is first read instead of during every startup, so a session with argot.enabled off no longer evaluates them.
  • The stats dashboard's aggregator, SQLite layer and embedded client load when /stats first runs instead of during every interactive startup, so a session that never opens the dashboard stops parsing them.
  • The Python, Ruby and Julia eval backends call their kernel and executor modules on every cell instead of holding a copy of each taken when the backend module loaded, so an availability check or executor replaced after startup is the one that runs.
  • A cursor-agent model receives the operator's global, profile and project instruction files again: the assembled prompt was blanked for that api in favour of a channel the server ignores, so every layer reached the model on no channel at all; one prompt is now built for every api and carried to Cursor on the active user turn.
  • A personality named after a property JavaScript objects inherit, such as toString or constructor, is reported as unknown and falls back to the default like any other unrecognized name; the built-in catalog was indexed without an own-property check, so those names resolved to a function, the system prompt build failed silently, and the default was substituted with no warning and any personality/default.md override ignored.
  • A personality spec can no longer spell a prompt tag such as <critical> and have it render as prompt structure; only <personality> was neutralized before, and a project's .veyyon/personalities file, which arrives with a cloned repository and outranks the user's own, is injected into every request.
  • plugin install <name>@<marketplace> --dry-run no longer performs the install: the marketplace branch never read the flag, so it fetched the plugin, wrote the cache and both registries, and reported a completed install; it now resolves the version from the catalog and writes nothing.
  • plugin link <path> no longer fails with EISDIR when the plugin is already installed as a real directory from npm or a marketplace, which is the state a local checkout replaces.
  • The Agent Control Center's read-only transcript viewer expands argot shorthand instead of showing the model's raw §handle text; it parses a subagent's or advisor's persisted transcript directly, and the persisted form keeps the handles.
  • Characters typed in the same terminal read as a paste are no longer discarded; the editor read the pasted payload and the bytes following it but dropped the ones preceding it, so the last thing typed before Cmd+V disappeared.
  • Backspace works at the launch card: the startup gate refused any chunk carrying a control byte, so a character typed by mistake before the composer mounted could not be taken back and the typo was what the session started with.
  • An unattended goal keeps driving after a turn whose post-turn maintenance outlasts the continuation delay, instead of sitting active and idle until someone types.
  • Text typed at the launch card appears there as it is typed, instead of staying invisible until session startup finishes and the composer mounts.
  • Text typed before the launch card paints reaches the composer instead of being destroyed, because the startup tty flush now runs only for the relaunch backlog it was written for.
  • The advisor roster editor reports a failed save instead of clearing the buffer and claiming the write reached disk.
  • The advisor roster editor states why the model list is unavailable when the model registry fails, instead of opening a picker holding nothing.
  • /advisor status names the next move on a text client too — the setting to change or the model role to assign — instead of reporting the state and stopping.
  • Interrupting a turn stops every configured advisor's review as well, instead of leaving one model call per advisor streaming and billing against a turn that was just stopped.
  • A prompt typed at the launch card while the session is still starting is carried into the composer instead of being discarded, and the terminal's own probe replies are still excluded so no escape sequence reaches the draft.
  • A bash working directory on a different Windows drive from the project renders as the absolute path it is, instead of being reported as a path inside the project.
  • Web search keeps trying the next provider when one returns only follow-up suggestions or intermediate search queries, instead of counting that metadata as a result and handing the model a list of questions; a SearXNG "did you mean" on a misspelled query no longer ends the search.
  • Public Web search reports which engines did not answer within the deadline instead of reporting an empty web, so a slow or bot-walled engine no longer surfaces as "returned no renderable search content".
  • The VEYYON_TINY_DEVICE and VEYYON_TINY_DTYPE reference states the providers.tinyModelDevice and providers.tinyModelDtype defaults as the default sentinel each setting actually holds, and names what that sentinel selects, instead of listing CPU and q4 as the stored values.
  • A collapsed ssh result measures its preview in rendered rows at the frame's inner width and shows the newest three lines with a count of what was hidden, instead of slicing the first five newlines and overflowing the frame whenever a remote line wrapped.
  • A terminal resize, theme switch or session switch no longer seals a backgrounded subagent's tool card mid-flight, so the progress it reports afterwards still reaches the card instead of being dropped for the rest of the turn.
  • The composer defect oracle counts only the prompt glyph the frame states, so a transcript row opening with $, > or ! is no longer reported as a second composer prompt.
  • An exported or shared session escapes quotes in every value it places in an HTML attribute, so a link target, link title or image mime type carrying a " renders as text instead of closing the attribute and adding an event handler that runs on the share origin.
  • An exported or shared session escapes the model names in its header, so a model name carrying markup renders as text.
  • The write tool accepts a file holding an indented numeric mapping key, so a docker-compose 80: http, a Kubernetes container port and a dict literal keyed by port number are written instead of being refused as pasted search output.
  • A read, write, search or image target that reaches outside the working directory through a symlink asks for approval even when it carries a selector suffix, so link.env:1-10, db.sqlite:users:42 and archive.zip:dir/file.ts:5-9 are no longer auto-approved where the bare path would have prompted.
  • A server-side compaction failure states the reason once instead of wrapping it in its own prefix, so a host without the compact route reports "Server-side compaction is not available for openai-codex/… (404 Not Found); falling back to local compaction." rather than nesting the message inside itself.
  • A tool status line shortens the paths it was given, so search, ast_edit, debug and set_cwd show ~/project/src instead of printing the home directory into the transcript, and a long path list is truncated rather than pushing the row past the terminal width.
  • The /omfg panel names the rule it saved under ~, truncates it to one row, and shortens the paths embedded in a failure message, instead of printing the home directory in its subheader, footer and error text.
  • The launch status header truncates the command it is starting instead of drawing a row wider than the terminal, and a failed launch collapses to a few lines with a count of the rest until it is expanded, instead of printing every line the process wrote.
  • A conversation /new leaves running in the background keeps its own row in the agent registry instead of being overwritten by the session that replaced it, so it stays listed and its finished turn no longer marks the foreground conversation idle.
  • The /resume picker floats its card in the middle of the screen instead of drawing it against the top edge over a half-blank terminal, most visible right after /new or a profile switch, when the folder holds few enough sessions to make the card short.
  • Clearing memory waits the full deletion retry window before reporting the database files removed, instead of half of it, so a Windows SQLite lock that outlives close() no longer leaves files behind under a success message.
  • Ollama discovery keeps a configured base URL's path, so an endpoint mounted at a subpath behind a reverse proxy is found instead of reporting no models.
  • The write tool and the hashline parser agree on what a numeric-keyed mapping looks like, so a body of true, false or null values is accepted by both instead of one each way.
  • Cancelling an LSP request while a project is still loading returns on every language server, instead of returning on most and throwing AbortError on rust-analyzer alone.
  • Reading a directory the process cannot open reports the permission error instead of rendering it as (empty directory).
  • A commit-analysis or changelog reply that a model writes as text is matched against the shape the caller needs, so a refusal, an error object or a reasoning object no longer crashes the parse or fills a changelog with one bullet per character.
  • Launch daemon teardown and browser process cleanup no longer throw on a host where the native addon cannot load; a daemon falls back to ending its PTY and a browser scan reports no candidates, instead of the failure ending the session (#917).
  • ctrl+g passes a GUI editor the flag that makes it block, so an edit made in VS Code, Cursor, Zed or Sublime is read back into the composer instead of the editor forking and the composer keeping the text it already held.
  • A /guided goal turn survives a model that wraps its JSON in prose, a code fence or a brace run that is not JSON, instead of ending the interview with a parse error.
  • A /guided goal turn shows a spinner while it runs, so the screen between an answer and the next question no longer looks inert.
  • A streaming answer no longer composes a frame one row taller than the viewport on every chunk, which moved the window down to fit and back up on the next frame and shook the screen for as long as the answer kept arriving.
  • Mounting a chat block no longer routes home-anchor slack for rows the content has already taken, which composed a frame taller than the viewport and moved the window on that frame and back on the next.
  • An irc send with await ends as soon as its recipient is terminated or leaves the roster, instead of blocking for the full timeout, or forever at timeoutMs: 0, on a reply that can no longer arrive; a recipient that is merely idle or parked is still woken by the delivery and given the full timeout to answer.
  • A truncated advisor preview, retry reason, and background-task label measure their budget in display columns rather than UTF-16 code units, so a wide or multi-code-unit character is no longer cut in half or counted as one column.
  • The tool approval card, the ast_edit diff preview, and an eval display() value cut long text at a code point rather than a UTF-16 code unit, so an emoji or rare CJK character sitting at the limit is no longer split into an invalid lone surrogate.
  • The Stream First Event Timeout and Stream Idle Timeout settings name the VEYYON_* environment variables that override them, instead of a PI_* prefix that no longer exists.
  • A Kagi search refused for exhausted credits reports that, instead of a generic request failure, on any status code other than 401, 402 and 403.
  • A rendered tool error shortens embedded home-directory paths, so an error card no longer prints the home directory and no longer spends its width budget on the prefix instead of the reason.
  • A job label truncated for display keeps a regional-indicator flag whole instead of emitting half of one, and an empty width budget yields an empty label rather than an ellipsis a column too wide.
  • Brave and Jina web search honor an API key held in the credential store, instead of reporting themselves unconfigured unless the key was also exported as an environment variable.
  • A JSON-RPC header field whose name merely ends in Content-Length, or a server log line that mentions it, no longer sets the frame length and parks the language-server connection on a byte count the stream never reaches.
  • A grep over an archive member removes its extracted scratch directory when extraction fails partway, instead of leaving it behind for the life of the host.
  • Waiting for a language server's project load removes its abort listener, instead of leaving one on the turn's signal for every feature request made during that turn.
  • A path list naming an entry the process is not permitted to stat resolves that entry as present, instead of failing the whole tool call with a permission error.
  • The working-directory boundary selects its targets by value, so a path argument carrying no usable value can no longer suppress the paths it also inspects.
  • An auto-compaction failure names its actual cause when the rejection is not an Error, instead of reporting the literal text compaction failed.
  • A liveness probe of the lspmux server cancels its timeout once the process answers, instead of leaving a timer pending for the rest of the window on every probe.
  • SQLite path detection restarts its scan at the beginning of the string, so a scan that ended abnormally cannot make the next one miss candidates.
  • The write tool rejects content carrying hashline patch markers, unified diff hunks, or read-output display prefixes with an error naming the detection and stating corrective action instead of silently stripping prefixes before writing.
  • Converted LLM message wrappers preserve reference identity across turns so the provider context canonicalizer re-renders only newly appended messages.
  • Memory pipeline SQLite storage (storage.sqlite) manages schema migrations via PRAGMA user_version and dynamically backfills missing columns on legacy databases.
  • The session tree selector formats custom tool arguments using width-aware truncation without double-stringifying string arguments or splitting surrogate pairs.
  • Renderer failure notices sanitize thrown errors, subjects, and fallback descriptions by shortening embedded home directory paths, replacing tabs, and truncating long payloads.
  • The extension inspector origin path truncates Windows backslash paths on directory boundaries and uses standard preview limits.
  • veyyon session declares the sessions alias so the plural command routes to session analysis.
  • Error-formatting call sites use errorMessage so thrown non-Error values and empty-message errors surface readable text, and a reported cause no longer repeats a redundant Error: prefix ahead of its message.
  • AgentSession logs compaction tail elision artifact persistence failures explicitly instead of swallowing them.
  • Quitting no longer hangs when a background session never settles.
  • /new typed while the agent is answering starts the new session without interrupting the answer: the running turn finishes in the background and is flushed to its own transcript, while the composer attaches to a fresh session immediately.
  • /resume onto a session that is still answering re-attaches the running session instead of replaying its transcript as finished text, so its answer keeps streaming into the view and the session being left takes its place in the background.
  • veyyon bench --model @role and veyyon dry-balance --model @role resolve a configured model role instead of failing to find a model named after the alias.
  • The model name segment on the composer status line is preserved against wide working directories and git branch names by ranking it above location shortening in footline degradation.
  • A clipped working directory on the composer status line now carries one ellipsis at the front instead of one at each end, so the directory the session is in stays visible and the visible text reads as a suffix of the real path.
  • The composer status line clips the working directory and the git branch from their own fronts together instead of dropping the branch, never clips a branch short enough to read whole, keeps the path's icon in front of the clip mark, and gives up the context gauge before letting either part fall under its floor.
  • A clip mark on the composer status line is painted in the colour of the text it kept instead of the colour in force before the segment, and a clipped directory or branch opens on a name boundary within four cells of the cut rather than on an orphaned separator.
  • The composer status line no longer paints an empty location zone beside a wide gap: the cells a shed right-group part frees are given back to the working directory and branch, including the cells freed by the shed that ended the fitting, and a token estimate or context gauge is given up before the zone falls under the width at which a name reads.
  • The composer status line keeps the running-subagent count after every other part has gone, instead of giving it up to widen the working directory; at the widths where the count is the whole row the location zone is empty rather than the count being absent.
  • The composer status line's state chips stay on the right edge of the row when no working directory or branch shares it, instead of rendering against the left margin.
  • The composer status line leaves the location zone empty rather than painting a directory fragment with its icon cut off, at the widths where the zone cannot hold an icon, a clip mark and a letter.
  • A click on the composer status line's working directory or branch now shows that name in full, spending the model chip and then the rest of the row's readouts for the room, and only leaves it clipped when the name is longer than the whole row; the click previously paid with the model chip alone, and lost even that room to a context gauge the collapsed row had shed.
  • The composer status line's expansion travels on a 320ms symmetric curve instead of a 180ms front-loaded one, and the room it frees now tracks the widening text frame by frame, so the row no longer steps backward at the start of a click before opening.
  • A click on the composer status line shows the half that was clicked in full rather than widening both, and a readout the click narrows past the width a name reads at is given up whole instead of resting as a fragment.
  • A clipped working directory on the composer status line opens on a directory boundary on Windows, where a path outside the home directory keeps its \ separator; the clip previously had no boundary to find in such a path and always opened mid-name.
  • The composer status line's path budget is counted in terminal cells rather than UTF-16 code units, so a working directory holding wide or astral characters is clamped to the width it paints and is never cut between the halves of one character.
  • The composer footline's click targets — the context gauge, the secrets chip, the goal readout and the path expansion — answer a click in a session whose transcript has not yet overflowed the viewport, instead of staying inert until it does.
  • A stripped working directory keeps the case it has on disk on Windows, instead of being lowercased by the case-insensitive comparison that decided it was under the root.
  • The status line's default-branch lookup no longer raises an unhandled rejection in a directory holding a .git on a host with no git on PATH; the lookup fails to the main fallback instead.
  • The composer status line no longer prints a control character or an escape sequence a name carries: a working directory, git branch, worktree label, multi-repo suffix or provider model name holding a tab, carriage return, bell, newline or escape is sanitized before it reaches the row, where it previously opened a hole in the width arithmetic, overwrote the row's own start, rang the terminal on every repaint, or handed the terminal a sequence of its own.
  • Outbound wire path canonicalization only relativizes paths matching the active session working directory instead of accumulating prior working directory roots, preventing distinct absolute paths in command output from collapsing to the same relative representation.
  • A working-directory change in a live session no longer re-renders earlier messages already sent to the provider, so only messages appended after a set_cwd render against the new directory.
  • Session CPU limits fail closed on unsupported or failed budget groups, lift rate control on removal, refuse a process-creating command before the process exists while leaving launch stop and launch list reachable, escalate over-budget termination from SIGTERM to SIGKILL, and track descendant processes on macOS.
  • Saturated session CPU limits now refuse spawns for MCP servers, extensions, hooks, and custom tools before the process is created.
  • Windows session CPU limits disable Job Object rate control on non-positive or non-finite core counts rather than throttling the process to the minimum rate.
  • A bash command carrying a leading cd, or a relative cwd, is now judged for approval against the directory it will actually run in.
  • Patch failure error rendering shortens absolute paths to avoid displaying home-directory paths and bounds large unmatched hunks with an omitted line count.
  • Filesystem cwd boundary checks expand comma- and whitespace-delimited path arguments matching execution, preventing multi-target reads or searches from bypassing working-directory approval prompts in non-yolo modes.
  • The read tool renderer sanitizes resolved directory paths with shortenPath to avoid displaying unshortened home-directory paths.
  • Stopping a daemon during restart backoff cancels the timer and attributes operator stop without recording a duplicate completion entry, and broker recovery terminates daemons left in restarting state without dead recovery branches.
  • Multi-target structure searches across overlapping paths deduplicate matches so totals, file counts, and paged results are not duplicated or truncated.
  • Acknowledging a completed background job before lifting its watch no longer delivers a duplicate completion notification when retention is zero.
  • A generic tool card with an undrawable image result no longer accumulates duplicate image placeholder rows on rebuild.
  • With Language Servers off, which is the default, the write and edit tools no longer start a language server to inject diagnostics, format the file, or notify the workspace that a file changed, including on the ACP client-bridge write path.
  • A malformed irc send reports its own validation error instead of being reported as an interrupted wait when a peer message arrives in the same batch.
  • A job list snapshot or cancel-only call keeps its own result when an interrupt lands beside it.
  • The composer sits on the viewport bottom on the frame it mounts instead of appearing mid-screen for a moment after the launch card is adopted.
  • The Agent Control Center reports the model an agent is running now instead of the one recorded when it registered.
  • /new and /resume restart the driving session's roster clock, so the Agent Control Center no longer ages the main agent from the conversation that ended.
  • The main agent's roster age advances with its turns instead of freezing at process start.
  • Web search no longer reports "Public Web returned no renderable search content" when one engine serves a bot wall: Startpage's proof-of-work interstitial served at HTTP 200 is now refused as a challenge, and an engine answering with zero results no longer ends the aggregate's wait for a slower engine that has results.
  • Adding an account with a key the provider rejects now leaves the error on screen, instead of remounting the account manager over it so the attempt looked like it silently did nothing; cancelling still returns to the account card.
  • veyyon plugin install --dry-run now resolves the target and fails when it cannot be installed, instead of exiting 0 with "Would install" for an unpublished npm name or a missing git repository, and reports the name and version the target resolves to rather than a 0.0.0-dryrun placeholder (#911 reported by @Crqptx).
  • veyyon plugin uninstall now removes a plugin installed from a local path, which was permanently unremovable because uninstall read only plugins/package.json dependencies while linking registers the plugin in the runtime config and node_modules; the linked directory itself is left untouched.
  • veyyon plugin doctor no longer reports "no plugins installed" in a profile whose plugins were all linked, and names how many linked plugins it found.
  • veyyon plugin config <plugin> now names the missing subcommand instead of reporting "Plugin name required" for a plugin name that was supplied.
  • A completed background job now fills its still-pending originating tool call instead of starting an unrelated recap turn after an interruption, including when zero retention is configured or foreground completion races background delivery.
  • Automatic compaction parks the run, or drains already-queued input once, when every summarizer candidate refuses, instead of reporting that nothing happened and looping.
  • A successful local compaction rescue retries without restoring the failed overflow or truncated assistant turn, and idle compaction stays silent.
  • Agent transcript headers and roster rows share terminal, approval-blocked, and peer-waiting status precedence, so interrupted agents settle red and untyped agents render without a dangling separator.
  • /agents keeps a parked subagent focused, preserves its reconstructed assistant messages, tool calls, and results, reattaches a revived session, and rejects stale revival, removal, scope, and rapid-focus races instead of switching to the wrong transcript.
  • IRC broadcasts no longer wake completed idle peers; direct messages still wake the addressed peer.
  • ImageMagick pixel caches used by proof capture and HD demo scripts stay inside an owned scoped directory that the parent removes after child failure without deleting concurrent or inherited unrelated directories.
  • An indented row inside a tool block keeps its indent when it wraps at a narrow width, instead of continuing at the block's left edge.
  • The finalization reminder counts the files a multi-file edit actually wrote: a call that reports overall failure after writing some of its files is now unverified evidence, and a file a per-file entry skipped is no longer named as affected.
  • A mutated path is XML-escaped before it reaches the hidden finalization reminder, so a file name spelling </system-reminder> cannot end the reminder envelope early, and a relative ast_edit path is resolved against the call's working directory before duplicate paths are collapsed.
  • A colour or title escape sequence a command writes in two pieces no longer leaves part of itself in tool output: the sink holds a sequence its chunk ended inside until the piece that finishes it arrives, and drops one the stream never completes.
  • A launch tool block no longer renders as a bare title with no rows: the header drops the placeholder ellipsis while op is still streaming, and every operation falls back to the result text when the structured detail it renders from is absent.
  • The compaction.remote setting description documents that server-side compaction applies to supported OpenAI, Azure OpenAI, and ChatGPT Codex Responses models.
  • branchSummary.reserveTokens reaches the branch summarizer, which previously used the built-in 16384 reserve whatever the setting said.
  • The ssh tool works again on a profile whose directory nests more than a few levels deep, because its connection multiplexing socket is named from a 16-character digest instead of a 64-character hash that exceeded the 108-byte Unix socket limit, and a path that still cannot fit drops multiplexing with one warning rather than failing the connection.
  • debug reaches the Python debugger on a host that installs python3 and no unsuffixed python, because an adapter may now declare alternate command spellings, while a command written in dap.json is still used exactly as written.
  • A subagent that calls yield with unusable data now fails the run instead of returning success with the system warning as its result, matching how a subagent that never yields at all is already reported.
  • A subagent result that cannot be serialized now fails the run and reports the serialization error, instead of returning success with an unparseable error envelope as its payload.
  • launch recovers from a broker connection that fails while the broker is still binding its socket, instead of caching the rejection for the life of the process and reporting that first error on every later call.
  • bash runs cd - && … again, instead of reading the leading cd as a directory literally named - and rejecting the call with a path the operator never typed.
  • A bash working-directory error shortens the path it reports instead of printing the absolute one, which put the home directory into the tool result and the transcript.
  • search reports why an archive could not be opened or read when the failure is not an Error, instead of the word undefined.
  • A detached daemon that exited while no broker was supervising it is recorded as its own exit, not as a non-detached daemon terminated by the replacement broker.
  • Background conversations abandoned at shutdown before their transcript finished flushing are named in the log, instead of leaving a short file as the only trace.
  • A streaming answer no longer slides the whole conversation up one row per streamed row: the anchor slack now sits below the content and above the composer, so a streamed row lands in the empty space and the composer keeps the viewport bottom.
  • The registry's static-stage snapshot (resolved-models.json) now actually hits on a warm launch. Two defects kept it missing every time: the fingerprint stamped models.db and its SQLite sidecars by mtime, and SQLite moves those files on every connection — including the writer's own — so the launch after every write rebuilt and rewrote the 12 MB file; and authoritativeFreshProviders was serialized as a Set, which JSON turns into {}, so the reader's array guard rejected every restore regardless. The fingerprint now reads a row-content stamp from the cache table, the field serializes as an array, and a relocated registry writes its snapshots beside its own database instead of beside the default profile's. Warm registry init drops from about 183 ms to about 49 ms; test/the-static-model-stage-snapshot-hits-unless-cache-content-changes.test.ts defends each case.
  • Unified search approval preflight now covers every multi-target syntax execution accepts; search also excludes byte-truncated context from editable seen lines, unions ranged and unrestricted text scopes, orders equal-mtime file results by path, suppresses pattern errors from unrelated structure languages, and distinguishes exhausted structure pages from a search with no matches.
  • Unified search now keeps warning-heavy text results within the inline byte budget, preserves semicolon path lists longer than one filename component, excludes matches hidden by generic truncation from editable seen lines, classifies every SSH path-list encoding at the execution approval tier, matches native Unicode tie ordering, and reports exhausted text pages with their totals.
  • Unified search no longer broadens or misroutes malformed calls: files-mode rejects a path owned by other modes while retaining the historical /-means-workspace alias, structure search rejects unsupported ssh:// scopes without an approval prompt for work it cannot execute, immutable internal or fetched sources never receive editable hashline anchors, and the Bash interceptor no longer redirects mutating or otherwise non-equivalent find commands to a read-only file search.
  • The plan-mode extension example now keeps canonical search available both while planning and after restoring normal tools. It previously advertised retired grep/find identities, requested a nonexistent find tool, and dropped search when plan mode ended.
  • Memory summarization now retains canonical search results from session rollouts. It previously allowlisted the retired grep tool name but discarded every result emitted by unified search before Stage 1 summarization.
Removed
  • The /providers account card no longer writes accounts.loadBalancing: its b key and footer chip are gone, Settings → Providers → Accounts is the one writer, and the card reports the stored value.
  • Dropped the Ecosia web search engine; it answered a search with a Cloudflare challenge rather than results, and the Public Web aggregate now fans out to Startpage, Google, DuckDuckGo and Mojeek.
  • A launch from your home directory no longer prints the three-line notice about relocating to a scratch directory; the relocation is unchanged, and /cwd and the status line state the session's directory.
  • The tools.unifiedRuntime experiment and the runtime tool it gated. The unified tool never shipped enabled, duplicated the eval and launch prompts at 2,368 tokens, and the experiment was abandoned in favour of keeping eval and launch as separate tools.
Breaking
  • Veyyon source checkouts, Docker images, and published packages now require Bun 1.4.0 or newer.
  • Code a repository carries no longer runs because you opened the directory. A project plugin registry (.veyyon/plugins/installed_plugins.json) and an extension or hook file inside the working tree are withheld until the operator records a decision, per file, by sha-256. Both loaded during startup, before the approval rung, the working-directory boundary or the secret-use boundary could apply: a plugin registry names install directories that supply extensions, hooks, custom tools, slash commands and MCP servers, and an MCP server names a command to spawn and can put a ${ENV_VAR} credential in an HTTP header, so cd into a clone was the whole exploit. A project that was working through either route needs veyyon trust once. Paths outside the project root — profile extensions, installed plugins, a path you configured — are unaffected, and nothing prompts: a session that cannot ask loads nothing and reports which file and which surface it refused.
  • A stdio MCP server no longer inherits the environment veyyon was started with. It used to be spawned with { ...Bun.env, ...config.env }, so an MCP package — third-party code installed once from a registry and updated without being read — could read every provider key, cloud credential and CI token exported in that shell straight out of environ, without making a single tool call and with nothing in the product bounding it. A server now receives a baseline of what a program needs in order to run (PATH, HOME, temp, locale, certificate and proxy settings, and the directories version managers use to resolve a command; on Windows also PATHEXT, SystemRoot, ComSpec and the ProgramFiles variants, matched without regard to case) plus whatever env sets. An ambient variable a server legitimately needs is named in the new envPassthrough, and inheritEnv: true restores full inheritance for one server while logging a warning on every spawn. A server that was silently relying on an ambient variable needs that variable named.
  • The hook event session.compacting is now session_compacting. Its old spelling collided with the session settings root, so a hook subscribing to it was read as configuration under session and dropped. A hook file naming the old event is rejected with the new name in the error rather than silently never firing.
  • prompts/all-registries.ts no longer exports assertEvalPromptOverridesClaimed. The refusal it performed is assertEvalPromptOverrideIdsExist in prompts/eval-overrides.ts, which reads the generated id space instead of the loaded registries.
  • The welcome hero no longer animates in, so InteractiveMode.playWelcomeIntro, the suppressWelcomeIntro init option, the InteractiveModeInitOptions type and the setup wizard's playWelcomeIntro option are gone, and gradientLogo and gradientEscape take no shine argument.
Added
  • The HD recorder resolves every binary a take will need before it records one frame: docker, bun for the scene check, and ffmpeg and python3 for the publish chain, with ImageMagick accepted under either of its two names and bun looked for at ~/.bun/bin/bun when a non-login ssh shell does not carry it. A publish tool first called after the recording is a take lost to a PATH difference; a rehearsal, which publishes nothing, needs only docker.
  • bun scripts/verify-scene.ts <scene> checks a capture scene without recording it: every string the scene waits for must be produced by the prompt it submits, the product's own source, the sandbox seed, or a line the scene types, and anything else is declared in the scene with a # needle-source: line. A guard nothing produces does not fail fast — it waits out its timeout and the publish step then leaves the previous take's frame under that name. The HD recorder runs it as a preflight, refuses an endpoint that is not on the recording host, refuses a model row the server does not serve, and writes <scene>-model.txt beside the frames naming the row, endpoint and host the take was driven by. Two guards in the hero scene were already stale: the todo board carries no count in its header, and "Status: complete" is only drawn in a goal details panel the scene never opens.
  • veyyon trust and /trust decide what a project may run: bare reports, approve records the files exactly as they are now, --deny/deny remembers a refusal so the next launch does not ask again, --forget/forget drops the decision, and a named path decides one file — which is how a refusal that names a file the scan does not list gets answered without leaving the session. Decisions live in <agent dir>/project-trust.json, keyed by the symlink-resolved project root, so a symlink to a project is not a second identity; a store from another version, or one whose records are malformed, is discarded rather than half-believed.
  • /secret clear everywhere empties all three vaults in one command. Clearing was per vault and the only way to be sure nothing was left was to run clear global, clear profile and clear project and read three reports, so the question people actually ask — "is any of this still stored" — had no command. The new form names every scope in one report, including the ones that held nothing, and revokes every placeholder it removed in a single notice to the model. everywhere, all, everything and every all parse, and only on clear: they are refused on add, scope, rm and discard, where "all of them" is not a destination.
  • proof/zoom.py holds a recording on one measured region and eases back out, so a row whose subject is a small block of text survives the downsample from the 2560-wide capture to the published 1920.
Changed
  • The Subagents block above the composer is one row per running agent again — a mark, the agent's id, its spawn description and the model it resolved to — with the house rail as its left edge and no tree connectors. It had been rebuilt as a table of lanes with an id column, a model column against the right margin and a live activity column resolving recovery over tool over description, and the table said less than the short list it replaced: three padded columns read as a grid to scan, the activity column drew whatever text a tool call happened to carry, and a bun -e command with a real newline in it put the tail of that command outside the block. Every cell folds newlines to spaces before it is bounded, because a bounded width states nothing about how many lines a string occupies, and the row never draws a task's prompt.
  • Light travels down the rail of the Subagents block, and only the rows whose agent is inside a tool are lit, so the sweep is a count of what is working rather than a decoration on the block.
  • Every tool result block hangs its output from the same rail and draws no tree connectors. Grep, ast-grep, glob, the file list, web search, the IRC renderer and the diagnostics list each drew ├─, │ and └─ to nest rows that are not a hierarchy, which put a second vertical edge inside a block that already had one; nesting is two spaces of indent instead, and an IRC message body no longer carries a quote glyph of its own either.
  • A block's header row sits on the rail rather than at column zero, so one left edge runs from a block's title to its last row of output. The title used to start two cells left of the rail beneath it, and a status icon at the top of the spinner ramp is a full cell where the rail is an eighth of one, so the two stacked read as a chip balanced on a wire.
  • The rail's travelling light advances by elapsed time instead of by a count of the repaints a block managed to get. A tool printing output holds the loop, several setInterval callbacks land late or coalesce, and the highlight stalled and then jumped — the hitch was worst exactly when the terminal was busiest. Every rail also reads one monotonic clock, so two blocks on screen carry the same head instead of the board's rail crawling at a divisor of its own.
  • The memory backend's start finishes behind the first frame instead of in front of it: a session hands it to AgentSession.deferStartupWork, and the first turn awaits it, so every tool call and subagent spawn still observes an installed per-session state.
  • A session no longer builds every prompt registry in order to validate an environment variable: the eval-override refusal reads the generated id space at prompts/ids.generated.ts, which takes prompt assembly from 718 reachable modules to 528 and accepts an id owned by a sibling package whatever the import order was.
  • The launch hero is a still card. The sun used to bloom open and the wordmark reveal behind a 33 ms timer for 2.2 seconds before the screen settled, and display.transitions no longer governs it; overlays and the tool rail still read that setting.
  • The launch card is painted before the session is built rather than after it: an interactive launch reaches the first frame in 3.7ms of paint, where the plugin-root preload, extension and skill discovery, the model registry, the MCP connections and the interactive mode's own mount all used to run against a blank terminal. The mode adopts that screen and that card, whose model line and recent session fill in when the session resolves them, and the theme ground goes on with the card.
  • The memory LLM has one owner. loadMnemopiConfig built a second, unsanitised remote client from the mnemopi.llm* settings that every session overwrote, and the paths that load a config without resolving it (dispose, diagnostics, the stats memories) kept it -- a client with no credential resolver and no provider-text obfuscation. A config carries the request (config.llm); the client is built only in resolveMnemopiProviderOptions. The memory_edit schema is derived from the store's own operation list instead of restating it.
  • The advisor's usage-limit remedy and the api-key resolver ask AIError.isUsageLimit(error) rather than re-deriving a quota verdict from a status and a message. Both had their own copy of the expression, and the resolver's copy read one predicate while rotateSessionCredential read another, so a bare 429 with no body could rotate on one side of the same failure and not the other.
  • The container scan behind the re-root hint reads one round of directories per depth instead of one directory at a time, so it costs the height of a tree rather than the sum of its directories. It runs during startup as part of the system prompt, where it measured 169ms of a 215ms prompt build; on this workspace with a warm cache the scan is 46.3ms before and 16.1ms after, and the whole predicate answers in 23.2ms including the git check-ignore call it batches. The project-marker stats are one round as well, and the repository marker's answer is reused by the container question rather than asked for twice.
  • A discovered config naming an environment variable that is not set now says so, naming the file, the entry field and the variable. ${VAR} expansion re-emitted the literal ${VAR} for an unset variable, so an unresolved reference was indistinguishable from config text: the eight providers that expand a config (native, Claude, Claude plugins, Cursor, Gemini, OpenCode, SSH, Windsurf) passed it on and reported nothing, and an MCP server with a mistyped or unexported variable failed to start with no line anywhere naming the cause. Expansion now reports what it could not resolve to a sink the caller supplies, and the parameter is required, so a consumer states what happens to an unresolved reference rather than inheriting silence: discovery turns it into a warning, and the MCP connect guard stays the enforcement point that refuses a structural field, reading the same grammar from the expansion instead of its own copy. No value is ever quoted in the warning.
  • Config values (models.yml apiKey and headers, MCP env and headers) accept ${NAME} / $NAME for an environment reference and literal:<text> for verbatim text. A bare value shaped like an environment variable name (upper case, digits, underscores) is still read as a reference, and now fails closed when the variable is unset or empty; every other bare value keeps its env-then-literal behaviour, so keys such as sk_live_... are unaffected. A key that is genuinely upper-case text rather than a variable name is written literal:MY_KEY.
  • The pi.registerProvider API example states the credential grammar it depends on. It showed apiKey: "GOOGLE_CLOUD_PROJECT", which now reads that environment variable and resolves to nothing when it is unset, so an extension author copying the example got a provider with no credential.
  • Republishing a whole session no longer holds a second copy of the transcript, and no longer reads the file back to learn nothing. A rewrite (compaction, elision, a title-change fallback, a recovered write fault) built the body by string concatenation and handed the filesystem one flattened string, then re-read the file first in case another window had appended to it: a 253MiB transcript of 118,358 entries spent 747ms, 1056MiB of peak resident memory, and 604ms of that in one unbroken stretch with the event loop stopped, 246ms and 322MiB of it on a read whose usual answer is that nothing changed. The body is now produced in chunks of about a megabyte by a factory the storage backend can ask for twice (the Windows EPERM fallback writes the same body a second time), each chunk written as it is produced, and the read-back is skipped while the file is still the same inode at the same length this session published. The same rewrite is 509ms, 44MiB, and no pause longer than 3ms; fork() on the same transcript is 515ms and 158MiB against 558ms and 612MiB. A second writer still changes both the inode and the length, so its entries are still read back and kept.
  • Restoring a session reads its externalized payloads under a bound, and a session with none pays almost nothing to find that out. The load walked the whole transcript through Promise.all over every array element and every object key, awaiting at each node: 2,000 ordinary tool entries with no externalized payload spent 17.9ms and about 27MiB of churn to discover there was nothing to read, now 1.7ms and 5MiB. A transcript with 200 externalized 512KiB payloads opened all 200 files at once and held 200 decoded buffers beside the 200 strings they decode into, peaking 122MiB above the transcript it produced; the restore now collects every reference in one synchronous walk and reads them eight at a time, which is 30.8ms against 50.7ms and a peak 100MiB above a 100MiB payload, with the event loop free to run while the reads are in flight.
  • A diff no longer rebuilds the two files it was handed in order to look up bracket context. generateDiffString split both sources into lines, and the boundary lookup joined each line array back into a source because it was not given one, so every diff copied both whole files a second time: 18MB of copies for one preview pass on a 9MB file, and a streamed edit preview does that pass again each time arguments arrive. The lookup now receives the text each side already holds.
  • A streamed edit preview no longer re-reads and re-parses its whole target on every chunk the model types. The replace mode's preview read the file again each pass, and both diff generators looked up their bracket-context rows by scanning the entire source twice per pass — once for the file on disk, once for the file the edit would produce — which the native parse cache cannot answer above 4MiB because it retains nothing that large. Streaming a one-line replacement against an 11.7MiB, 100,000-line file spent 1.9s per pass, so two seconds of 30Hz arguments landed 4 previews, read 46.8MiB, burned 7.8s of CPU, and showed a preview 2.1s behind what had been typed (3.8s at the tail), with the args-complete pass blocking the event loop for a further 1.9s. The streaming pass now reads through a cache keyed by modification time and size, the args-complete pass still reads fresh, and a source over the size the parse cache retains renders without off-window boundary rows instead of paying a whole-source scan per redraw. The same two seconds now land 49 previews, read the file once, spend 2.4s of CPU, and stay 36ms behind the stream with a worst case of 103ms; the final pass costs 36ms. A read window on a file that large stops paying the same scan.
  • The MCP loader no longer imports a type it stopped using when it started forwarding discover options through one object. No behavior change; the workspace lint gate was failing on it.
  • The first frame no longer waits for a hardware probe. Building the system prompt looked up the GPU name, and on a cold cache that lookup spawns lspci or nvidia-smi: 224-557ms measured on one workstation, spent before anything is drawn, for a prompt line no frame displays. The probe now runs unwaited and writes its cache for the next launch, so exactly one launch per machine omits the GPU row and every launch after it has the name from its first build. The answer a launch starts with is the answer it keeps: a probe landing mid-session does not add the row to a later build, because that row lives in the cached prompt prefix and re-anchoring the prefix costs more than the line is worth. scripts/bench-startup.ts measures the boot path and docs/internal/startup-budget.md records the baseline it was measured against.
  • A session that died on an uncaught exception is logged as an error, and a terminal that closed with nothing in flight is not logged as a warning. The session_exit record carries two facts — the kind of teardown and how many tool calls it orphaned — and its severity was chosen from the first one alone, so warn covered a crash and a closed window equally: across 19 local log files, 23 exits were recorded at warn, 17 of them a sighup with no pending work and 4 of them fatal. The level is now error for an unhandled throw or rejection, warn when tool calls were left without results, and debug for a signal, a process.exit or a normal dispose that lost nothing. sessionExitLogLevel in session/exit-diagnostics.ts owns the ladder.
  • The anchored HUD stops animating when the mode it belongs to stops. Its motion frame was unref()ed, which keeps a timer from holding the process open and does nothing to stop it firing, and nothing disarmed it on teardown — so a stopped mode went on stepping its rails, rendering both blocks, and reading the settings singleton for as long as the process lived. Teardown disarms it, and a torn-down mode refuses to arm a new one, which matters because every board write runs the arm site.
  • The working directory leaves the cached prompt prefix. The base system prompt is what the provider caches, and it stated the directory verbatim — "Today is <date>, and the current working directory is '<path>'" — so a re-root that changed nothing else still discarded the cached prefix for the whole conversation to restate one path. Measured on this repository, moving from the root to packages/utils altered exactly one line of a 92,921-character prompt; across 19 local log files, 210 of 232 recorded invalidations were a cwd-change, about 85,000 characters re-read each time. The date and the directory now arrive as a session-state message with the turn, restated only when they change, the way recalled memories already do. The rebuild on re-root is unchanged, because the rules, skills and workspace tree really are directory-derived — what changed is that a move which alters nothing but the path rebuilds to byte-identical bytes and records no invalidation, while a move to a project with different rules still does. The "not a project root" paragraph also stops naming the path, since the message already carries it. Two directory-derived facts stay in the prefix and still invalidate on purpose: the workspace tree, which is a picture of the directory and is off by default, and the active-repo-context block.
  • A discovery failure from software nobody started is no longer a warning. Model discovery runs against every provider veyyon knows about, three of which are local runtimes it probes on loopback without being told to, so a machine that does not run llama.cpp, LM Studio or Ollama collected a warning per refresh for each of them: 110 of 137 model discovery failed for provider records across 19 local log files were a refused connection to a port with nothing behind it, and the 27 records that named something actionable — an Anthropic 404, six xAI OAuth 403s, an aborted Devin request, one llama.cpp 502 — were four fifths buried. A provider with a stored credential, an explicitly configured base URL, or an endpoint that is not on this machine still warns with every field it carried before, and so does an unconfigured one whose endpoint answers at all, because something is listening and is broken. A loopback port that refuses the connection is recorded at debug. The three implicit local runtimes are one table rather than three near-identical blocks, so a fourth is exercised by the same tests the moment it lands.
  • Image generation is disabled by default. Enable Generate Image in the Tools settings to expose generate_image.
  • The Todos board above the composer keeps the tree list it has always been — a header naming the phase the plan is on, one row per phase with its tally, and the tasks of the phase being worked nested under connectors — and gains a rail as its left edge. Every row starts on block.rail, which is the one arrangement the house rail animation can find, so light travels down the block while the plan is being worked and the rail is flat while it is not. The stages already finished are not drawn: the header's phase n/total states how far the plan has come, and a column of closed tallies made the block read as one undifferentiated chunk.
  • The task in flight carries one small square alternating between ▫ and ▪, at the row it belongs to. A cell from the density ramp fills the whole cell and a terminal cell is half as wide as it is tall, so a ramp cell at the task indent read as a rectangle switching on and off, louder than the row it marked. Every other row keeps the checkbox vocabulary and is still.
  • The board moves only while the agent is streaming, compacting or running post-prompt work, and stops on the frame a turn ends or an interrupt lands. A task marked in progress persists across the turn boundary, so a board keyed on task state alone moved for as long as the operator sat reading a plan nobody was working on, and the anchored clock repainted two regions to draw the same thing. todoBoardMarkerAnimates and todoBoardRailTravels are the one place that is decided, for the mark, the rail, and the clock alike.
  • An open plan's transcript card is one line naming the header, the done count, the phase and the task the write moved, because the anchored board already draws the list and the two surfaces were drawing it twice.
  • A pending task a detached subagent is working on takes the accent and the in-flight mark, which is what states someone other than the main agent is on it.
  • The /secret help is grouped by the question being asked rather than by declaration order. One heading covered eleven verbs, three of which delete something — a secret, a whole vault, and a vault file that cannot be read — so choosing between rm, clear and discard meant reading all eleven lines. There are now four groups: storing a credential, seeing what you have, changing one secret, and removing secrets.
  • Every documentation page a user reads lives in one tree. Twenty-nine topic files sat flat under docs/ beside a handbook that covered the same subjects, so a reader looking for the settings catalog, the RPC surface, the theme schema or the keybinding defaults found a page that was in neither the handbook's table of contents nor its search index, and a contributor adding a page had two plausible homes and no rule for choosing. The flat files were reference material and their handbook counterparts were guides, so nothing was merged away: each one moved beside the guide it belongs to (reference/settings.md, reference/rpc.md, architecture/secrets.md, context/context-files.md), every one is in SUMMARY.md, and docs/README.md is the map of which tree a page belongs in. docs/internal/ stays internal, and the runnable extension, hook and marketplace examples moved to packages/coding-agent/examples/ because they are code.
  • There is one way to capture a picture of the product, and the documentation describes one. Two capture paths shipped side by side — a recorder driving a real terminal on a private display, and a tape renderer with its own baseline block — and each was documented as the only authority, so a before-and-after pair could be assembled from one arm of each and still satisfy the words in the repository. The tape path is gone: its tapes, its drivers, its container install, and the gallery --screenshot flag that fed it. A settings differential is now recorded from the same scene twice, seeded through SCENE_SETTINGS, and scripts/there-is-only-one-capture-path.test.ts fails on a tape file, a path named after the tool, a mention of it in any tracked file, or its reappearance in the recorder image.
  • Every extension path the operator named on the command line, in config.yml, or through the SDK is loaded as a named path rather than as repository code. The project-trust gate withheld any extension whose resolved path fell inside the working tree, which caught --extension ./my-ext.ts and the extensions config list as well as the files a clone ships, so a path the operator supplied had to be approved through veyyon trust before it would load. Named paths are now passed to the gate at every load site (the CLI, the SDK's two loadExtensions calls, the tool registry, and the three task entry points), and a subagent inherits the named subset explicitly instead of re-deriving it. test/security/a-path-the-operator-named-is-not-repository-code.test.ts pins the distinction, and a load site that forgets to pass its named paths gates more rather than less.
  • The repository rules and the documentation they point at are split by what they are. AGENTS.md states the rules and names the script that enforces each one; the mechanism and the history behind them moved to docs/internal/bun-surface.md (the Bun surface, its counts, the worker-hosting contract) and docs/internal/repo-gates.md (the two workflows, why the Rust gate sits in the fast one, why the changelog gate runs on push). The handbook lost its self-referential openers and its anthropomorphic voice: a setting sets, a command prints, a check rejects. docs/handbook/src/reference/settings-reference.md is generated, so its wording is fixed in scripts/gen-settings-reference.ts and regenerated.
  • A launch no longer builds a schema library it will not use. The theme reader, the config reader, the usage report and the five provider discovery readers each declared validators at module scope, so every session spent 362ms constructing them before a prompt was drawn, for files and requests most sessions never touch: importing main.ts measured 1411ms and now measures ~1262ms. The theme file is checked by validateThemeJson, which returns the missing tokens and the problems as separate lists — the reader used to recover the missing-token list with a regular expression over a validator's prose — and derives the required-token list from the ThemeColor and ThemeBg tables, so a new token is required without anyone maintaining a second list. test/startup/a-launch-does-not-load-a-schema-library-it-will-not-use.test.ts times a launch path's second import of the library and fails when a new module puts one back.
  • colors.toolText is gone from the bundled themes. Twenty-three theme files declared it, fourteen of them as an empty string, and nothing read it: it was not in ThemeColor, so the validator never required it and no renderer ever asked for it. A custom theme that sets it is unaffected, because an unknown color token still passes.
  • Every tool block's call row, result rows and renderer-less output hang from the same rail at the transcript's inset column.
  • An inline tool renderer's own leading indent is removed before its rows are framed, so every tool's title starts at one column.
  • The receipt row and the anchored boards start on the rail at the transcript's inset column instead of one cell inside it.
  • The Subagents block plays one rail sweep for the whole block instead of flashing the rail beside whichever agent last reported.
  • The rail's travelling light advances at four rows a second instead of eight.
  • A streaming edit or write block carries the rail light on its newest row rather than at a position taken from the clock.
  • An overlay's first frame is its settled frame: a card no longer unfolds row by row and no highlight sweeps across it as it opens.
Fixed
  • Memory credentials resolve through the config-value grammar. mnemopi.llmApiKey and mnemopi.embeddingApiKey were handed to Mnemopi as raw setting text, so ${VAR} reached the memory host as the credential and !command sent the command text; the failure arrived as a 401 naming nothing. A named variable that is unset now sends no credential at all.
  • A read that clips a wide line no longer blocks an insertion beside it. The read tool applies a per-line column cap, and a clipped row was recorded as neither seen nor anything else, so the edit tool refused every hunk anchored there — including INS.PRE, which leaves that row byte-identical. Adding one line above a multi-kilobyte row meant reading the whole row back with :raw first. The clipped rows are now recorded as clipped, which grants exactly one thing: INS.PRE and INS.POST beside them. SWAP, DEL and the .BLK forms on a clipped row are still refused, and a line the read never rendered at all is still refused for every form.
  • The anchored todos board's in-flight glyph stops one ink level below a full cell. It ran the status spinner's own density ramp, which peaks on █ once a cycle; in a dense status row a full cell is not the largest ink present, but on the board it is the largest ink any row draws, so at the top of the ramp the pulse read as a block appearing rather than as a cell breathing. The board's ramp is derived from the active one by dropping its top level, · : ░ ▒ ▓ ▒ ░ :, so a theme that overrides spinnerFrames is carried with no second setting to keep aligned, and a preset whose frames are not a density ramp — ascii's | / - \ — is left alone. The status line keeps the full ramp.
  • The collab guest allowlist no longer names a command that does not exist. theme was listed and is not a builtin, and help was listed as its own entry when it is an alias of /welcome, so a guest typing /help was checked against a name the registry resolves elsewhere. The allowlist is checked against the builtin registry, and test/collab/a-guest-runs-only-commands-that-exist.test.ts fails on an entry that names a missing command or an alias.
  • An MCP server whose config still holds an unresolved ${VAR} in command, args, cwd, url or envPassthrough is no longer started. An unset variable with no default used to stay in the value as literal text, so it reached the spawn as a program name or an argument, or a URL as a hostname, and the failure that followed named the variable's text rather than the field. The connection is refused first, with the field and the variable named and no value quoted.
  • A project-trust decision is written through the shared atomic-write helper, so a store cannot be left half-written by a crash or a full disk. It used to write in place, which is the one file whose corruption withholds every extension, hook and MCP server a project supplies.
  • The HD recorder resolves its own working directory and proves it can write there before it records. A container that could not write the output directory failed at the publish step, an hour into a take, with the frames already discarded. WORK_DIR selects the directory and a write probe runs in the container during preflight.
  • A config value that names an environment variable is no longer replaced by the variable's own name when the variable is unset. apiKey: GITHUB_TOKN or a CI secret that never reached the job used to resolve to the string GITHUB_TOKN, which veyyon then sent as the credential and the far end answered with its opinion of a bad token, mentioning neither the variable nor the typo. The value now resolves to nothing: a provider key is not installed, an MCP connection is refused before any request is made, and a warning names the variable and the setting it belongs to without quoting any value.
  • Stopping an MCP stdio server now ends every process it started. A server run through a wrapper (npx, uvx, docker run, a repository script) was left behind when the wrapper was killed, so each reload and each failed handshake leaked one server process. Teardown now signals the whole tree, escalates once if the tree ignores the polite signal, and is bounded so a reload cannot hang on it.
  • An MCP credential that cannot be presented refuses the connection instead of connecting without it. A revoked credential, a refresh token the auth broker holds and redacts locally, and a credential store that could not be read all ended at one log line, after which the connect went ahead with no Authorization header at all — so the operator saw the server's answer to an anonymous request (an HTTP 401, a provider error page, a lockout after enough of them) and nothing about the credential or the command that fixes it. Each state now names itself and the action: /mcp reauth <name> for a rejected credential, the same through the broker for a broker-held refresh token, /mcp reconnect <name> for a store that failed. Nothing is sent, so the far side gets no failed-auth attempt to count. A refresh that fails while the access token is still valid keeps connecting, and a server with no stored credential still connects as configured.
  • A short-lived MCP credential that rotates is re-read instead of re-sent. A !command header or env value — !op read op://vault/mcp/token, !gcloud auth print-access-token — was cached under the command text, which is byte-identical before and after the secret behind it changes, so once a value was cached the product kept sending it: the server answered 401, the transport retried with the same header, and only restarting the process helped. Worse, the auth-retry hook was installed only for servers with a stored OAuth credential, so a server authenticated purely by a command had no refresh path at all. A 401 or 403 now re-runs that server's commands and retries with what they print, /mcp reconnect <name> re-reads one server's credentials, and /mcp reload re-reads every configured server's; a command used outside MCP config, such as a provider apiKey, keeps its cached value. An automatic reconnect after a dropped transport deliberately does not re-read, because a lost connection is no evidence about a credential and a password-manager command re-run per reconnect is an unlock prompt per reconnect. Two endpoints answering 401 at the same moment is one rotation and costs one execution of the command, and a failing command's 30-second back-off is not shortened by any of this.
  • A misspelled per-tool approval policy blocks the tool instead of silently unblocking it. tools.approval.<tool> accepted allow, deny and prompt and returned "unconfigured" for everything else, so a hand-edited bash: denyy was indistinguishable from no policy at all — and on yolo, where an unconfigured tool is allowed, the typo auto-approved the tool it was written to stop. Any value present under that key and not one of the three now denies that tool, on every rung and under the /yolo session bypass; deny rather than prompt, because the bypass lifts a prompt. An absent key, an empty record, and a key whose value is undefined are still unconfigured, so a clean install is unaffected, and only the named tool is affected — the rest of the record still applies. A warning at startup names the setting, the value found, the fact that the tool is denied, and the three values that work, so a fail-closed policy is not a silent one.
  • The context gauge stops ignoring the provider count the running turn already produced. The in-flight estimate is kept only until a step of the turn reports real usage, and which steps count as "this turn" was decided by comparing an index against the count of everything the prompt submitted. A turn submits more than the question — the session-state line carrying the date and directory, recalled memories — so that count landed past the turn's own assistant steps and every provider figure they carried was rejected: a step reporting a 9,000-token prompt was reported as the 68-token turn-start estimate, and the gauge stayed there for the rest of a long tool turn. The boundary is now where the turn begins, and what the prompt already counted is identified by the message itself rather than by how many there were, so the tail is neither skipped nor counted twice.
  • A bounded range read touches the file once. read path:50000-50019 on a 3.5MiB, 100,000-line source read 3.08x the file's bytes: the line streamer scanned it for the window, bracket context read and split all of it, and the snapshot tag read and normalized all of it again. The window, the context rows and the tag now come from one materialization, which measures 1.08x. A file whose bytes are not already LF-only keeps the streamed window and shares only the text, because byte accounting counts source bytes and a CRLF line carries a byte the normalized one does not; a leading BOM is now detected from the bytes, since Bun.file().text() drops it and the streamer displays it. Wall time is unchanged: 93% of a large range read is tree-sitter parsing the whole file for bracket context, which is a separate cost.
  • A session booted against another profile's agent directory now discovers that profile's MCP servers. discoverAndLoadMCPTools copied three of its caller's discover options by hand and dropped agentDir, so the headless and SDK path connected the process-active profile's mcp.json instead of the one the caller named. The options type now extends the discover options and the loader forwards them by rest, so a field cannot go missing the same way again.
  • A goal set before the first turn is no longer deleted with the session file. A session whose journal held only the goal record plus a model pick counted as "draft-only metadata" and was removed on close once its draft was gone, which is the ordinary shape of a session where someone sets an objective and closes the window: the objective was the only copy and it went with the file. Whether a journal is droppable is now a question about the list rather than about one entry — a goal that is still the last mode change keeps the file, a goal the operator dropped leaves mode_change("none") behind and is selector state again, and a plan-mode entry written by plan.defaultOnStartup is dropped exactly as before.
  • A resume that declines to restore a goal says so, and turning Goal Mode off no longer destroys the objective. With goal.enabled off, a session carrying a goal recorded mode_change("none") on the next reconcile: not a suppression but a deletion, so turning the setting back on restored nothing and the session came back with no goal and nothing on screen or in the log saying why. The record now stays on the branch, inert, and the operator is told which objective is stored and inactive. A stored record the shape check cannot read is still cleared — nothing could restore it — and that clearing is reported too. Both records name which stored goal the decision was about, so a log read after the fact identifies the objective rather than only the mode entry.
  • An active goal keeps driving after a provider hiccup the session recovered from. A retry resumes the killed turn with a fresh agent_start, and goal mode reset its "did this turn call anything" evidence on every one, so a continuation that ran tools and then lost the transport came back as a retried attempt that only talked and was read as the model having nothing left to do: the goal stood down mid-work, the footline still said Goal, and the session sat idle until someone typed a message. Tool calls a killed attempt already made now survive into the retry of the same turn, auto_retry_start being the only notice the mode gets that the work continues, since the dead attempt's own agent_end is superseded by the recovery. A turn the mode does see end in error — retries spent — no longer decides anything either: it used to latch the stand-down permanently. Consecutive dead turns are tolerated up to three, then goal mode stops driving and says so, so a provider that is genuinely gone cannot spin the goal forever.
  • Why a turn was aborted has one name, and only an operator interrupt pauses an active goal. Every abort routes through one method whose goal-facing reason defaulted to "the operator stopped this", so a site that aborted a turn to do its own work and did not spell the option out paused the goal exactly as an Esc would; plan mode's silent approval abort was such a site. The two readings are now the exported GoalAbortReason, both seams take that type, the pause is decided in one place rather than in a fast-path condition that could not add a case, and a third reason cannot be added without classifying it — the sweep that exercises them is keyed by the union itself.
  • A verification stamp on an internal doc survives a documentation path rename. The freshness gate compares the doc's last commit date against its stamp date, so folding the flat topic pages into the handbook — an edit that rewrote link targets and nothing else — turned eight verified pages into "re-verify and re-stamp", for a change no verification covers: where a sibling page lives says nothing about whether this page matches the code. A stale stamp is now cleared only when the doc's text differs from the snapshot it was stamped against once every *.md path token is folded away, the snapshot being the commit that WROTE the stamp rather than the code commit the stamp names. Any prose edit still fails, a dropped path reference still fails, a stamp dated before the commit that wrote it is refused the exemption, and every page that keeps its stamp this way is printed by name.
  • veyyon prompt answers in the format it was asked for. --json was read fifth in a chain of early returns, so five of the six views the command has returned their text table before reaching it: --prompts --json, --prompt <id> --json, --tools --json, --section <id> --json and --statement <id> --json all printed padded columns and exited 0, and a consumer that asked for JSON got a parse error with nothing saying the flag had been dropped. Which view an invocation means is now decided in one place, separately from which format it is rendered in, so every view has a JSON form: the prompt list carries each id with its template path, the tool table carries the per-tool token split beside the prompt's own cost, and a refusal — an unknown prompt, section or statement id — carries its message and its non-zero exit as a parseable object. A rule this configuration leaves out reports present: false with the condition that would include it, rather than an error, because a rule that is off is not a failure.
  • A chosen web search provider is the only provider web_search uses. Picking one — through providers.webSearch or a per-call provider argument — hoisted it to the front of the full chain and left the other twelve behind it, so an engine that answered with nothing handed the query to a different engine: choosing DuckDuckGo could send the query to a keyed provider the operator was deliberately not spending, and choosing a keyed provider could fall through to a credential-free one that returns bot-challenge pages. A per-call argument for an engine with no credential skipped the choice entirely and ran the whole auto chain. auto is now the one value that ranges over the roster, one owner (selectSearchProviders) decides the list for both routes, and a configuration that cannot be satisfied — a provider chosen and excluded, or every provider excluded — is refused with a message naming providers.webSearch and providers.webSearchExclude rather than widened into a search nobody asked for. The zero-available message names the chosen provider and the credential it wants.
  • An eval-only prompt override reaches the model instead of only the inspection commands. VEYYON_EVAL_PROMPTS replaced text in the aggregate prompt registry, but a module that sends a prompt imports its row table directly, so the override announced itself and changed nothing a model was sent: with tools/bash replaced, veyyon prompt --tools still reported the shipped 971-token description. A benchmark arm built on that measures its own control while the results table names it a treatment. Every row table under src/prompts/ now applies the override where text is read, and the same command reports 4 tokens for the replaced description and 12757 rather than 13724 for the whole tool set.
  • A prompt override naming an id no registry holds is refused at prompt assembly, where every registry in the build is known, and reported with the nearest registered ids. The check used to live inside each registry, which cannot tell a typo from an id a sibling owns: @veyyon/ai's registry is constructed first and holds no tool descriptions, so a valid tools/bash override killed the process at startup. src/prompts/all-registries.ts is now the single owner of which registries a model can be sent from, and veyyon prompt --prompts reads the same list. The refusal names each unknown id on its own line and explains what an id is in the same words the bench runner uses for the same mistake, from @veyyon/utils.
  • sanitizeStatusText strips DCS, SOS, PM, APC, and 8-bit ANSI escape sequences via @veyyon/utils instead of delegating to node:util's stripVTControlCharacters, preventing string sequence payloads from leaking into status lines under Bun 1.4.
  • A setting the system prompt depends on rebuilds that prompt whoever writes it. Two owners decided that: the settings screen asked the prompt-gate registry, and the session asked a private table of eight paths that never restated six of the registry's live gates. So writing personality, tools.format, inlineToolDescriptors, includeModelInPrompt, tui.renderMermaid or tools.intentTracing from anywhere but the settings screen — a slash command, an SDK or ACP host, a plugin — changed the configuration and left the model reading a prompt that described the previous one, with nothing logged; and flipping one of the five paths both lists held rebuilt the prompt twice for one change. The trigger now lives once, with the prompt, in the session's effective-setting listener, and reads the registry for which settings reach the model. The session's own table keeps only the three that gate no prompt text at all (async.enabled, subagent.isolation.mode, subagent.maxNestedSpawnDepth, which decide the task tool's description and schema), and a failed rebuild is logged as a warning rather than at debug, because the settings screen is no longer there to report it.
  • Minimized shell output no longer carries the tail of an escape sequence as text. The minimizer's strip_ansi knew CSI and OSC, and every other escape fell to the rule that drops the escape byte and keeps what follows — so a command whose progress bar parks the cursor with ESC 7 handed the model 7…8, reset contributed a stray c, an editor's charset select contributed (B, and a sixel image or a kitty graphic arrived as its entire payload in the middle of a line. It now reads the same four grammars the TypeScript half reads, from the same cross-language corpus, and a truncated sequence still keeps every byte after the escape so a capture cut at a buffer boundary loses nothing.
  • A session now subscribes to settings changes unconditionally. The subscription was an optional call on a method Settings always has, left behind from a week when two suites built a session around a stand-in that lacked it: a real gap would have taken the prompt rebuild, the todo-reminder reset on disable and every live CPU-limit change with it, and said nothing. Those suites hand over a real Settings, so the only thing the optional call could still do was hide the next one.
  • read accepts a line selector on history://, so history://Scout:120-160 pages a transcript instead of answering Unknown agent: Scout:120-160 while listing Scout among the known agents. history was missing from the selector allowlist, so the whole <id>:<selector> string reached the protocol handler as an agent id; the error then pointed at the agent rather than at the selector, which reads as a lost transcript. history, issue, memory, pr and veyyon were also missing from the internal-URL prefix list, so those five were measured against the cwd boundary as filenames and had their backslashes rewritten as path separators. Both tables are now checked against the router itself.
  • A board that arrives already finished draws nothing rather than playing the exit sweep for rows it never showed. The clearing animation was armed whenever the incoming plan was complete and the previous one was empty, which is exactly the shape of a resumed session and of any single call that hands the HUD a closed plan, so the anchored region swept a strike across tasks and ran light down a rail that had never been on screen. The sweep now requires a board that was open on screen and closed in this update.
  • veyyon:// serves a page that moved. The resolver matched the exact path and nothing else, so a reference to a page by its old location — in a rule file, a saved session, a comment, an issue — answered "not found" for a page that is still shipped, from a new directory. An exact path still wins; a bare name that exactly one page carries resolves to it; and a name several pages carry is refused with both candidates named, because guessing between features/secrets.md and architecture/secrets.md is worse than saying which two exist.
  • The generated settings reference renders the pipes and placeholders its cells contain. A table cell is split on | before inline code is parsed, so the bashInterceptor.patterns default — which carries (cat|head|tail) — rendered as a dozen stray columns with most of its text dropped, and a <name> placeholder in a description was read as an HTML tag and deleted, leaving ~/.veyyon/personalities/.md as the instruction for where a personality file goes. Both are escaped where the cell is written, and the handbook parity check compares what the renderer prints rather than what the source spells.
  • veyyon acp run directly points at the log directory that exists. Its stderr notice, and the comments in the auth-broker CLI and the browser-open fallback, all named ~/.veyyon/logs/, which is the pre-migration bare-root path; logs are per profile, at ~/.veyyon/profiles/<name>/logs/, so a client integrator following the message found nothing there.
  • A misuse of the tab API inside a browser cell names the API instead of failing as a TypeError several frames deep. tab.$$(".row") answered tab.$$ is not a function and tab.hover(".btn") answered tab.hover is not a function, neither of which says what the facade does have; a call that omitted its argument reached the implementation and crashed on selector.trim() of undefined, which names a property of the argument rather than the argument. The facade is now wrapped at both call sites — the worker's tab API and the cmux run facade — so an unknown member reports the members that exist, with the closest known replacement spelled out for the two that were reached for by name ($$ → tab.observe() or tab.page.$$, hover → tab.page.hover), and a missing, blank or non-string required argument reports which argument of which method is missing. Neither method was added: a new tool surface owes its own proof, and the guard points at what is already there. Language probes (then, constructor, toString, …) still answer undefined, so the object stays awaitable and inspectable.
  • A subagent that sends its whole yield result as a JSON string gets the result recorded instead of a refusal it cannot act on. result: "{\"data\": {…}}" answered result must be an object containing either data or error, which names no shape to send: one recorded child re-sent the identical stringified payload five times, rewording the prose around it and adding fields that were never the problem, and its parent got nothing. 38 sessions hit that refusal. The string is now parsed the way the argument-repair pass already parses a stringified argument object — yield is the one tool that sets lenientArgValidation, so it never reached that pass — and a result that still is not an object is refused with a message naming what arrived (a string, an array, nothing) and both accepted shapes. That refusal is also bounded now: only the empty-result branch counted against the retry budget, so a caller that kept sending a string could retry forever while its parent waited; the fourth consecutive one aborts the child.
  • Neither anchored block above the composer wraps any more. Both clamped every row to one cell inside the width they were handed, and both were handed the terminal's full column count — but they are mounted in a Text carrying a one-cell margin on each side, and that component soft-wraps its content two cells earlier than the terminal does. Every row was therefore two cells too wide, and the tail of it landed on a line of its own at the margin, outside the block's rail: a capture of two live lanes at 131 columns shows each lane's model badge alone on the line beneath it. Both call sites now derive their budget from the same margin the mount resolves, so tight layout is followed rather than guessed at. The widths were not the bug — both blocks obeyed the bound they were given, which is why a sweep of every column count from 1 to 220 could not see it — so the regression suite drives the real interactive mode and asserts the mounted block renders exactly the rows it emitted.
  • A model can now create the session's initial persistent goal directly from an ordinary task prompt. When goal support is enabled, the goal tool remains available before entry, while paused, and after completion; creating a goal persists goal mode, updates the status UI, and arms continuation without requiring the operator to run /goal set first. Plan and vibe modes still keep the goal tool out of their restricted tool sets.
  • Dropping a todo task is no longer counted as doing it. The closing tally counted every TERMINAL status as done, so op: "drop" on one of six tasks answered Dropped: Add error handling. Next: … Overall: 2/6 done, 4 open. on one line, claiming two completions where the board held one completion and one abandonment. The arithmetic was right — closed and open are complements — but the word was not, and a model reading its own board back could not tell work it finished from work it gave up on. A tally now reads Overall: 1/6 done, 1 dropped, 4 open., says nothing about dropped work when there is none, and admits only completed to the done side, so a terminal status added later lands in dropped rather than being absorbed into done.
  • A finished working loader is unmounted instead of merely stopped, so its row cannot outlive the work it was announcing. Clearing the loader dropped the reference and left the component mounted in the status container, which kept drawing ▌ <task> · 0:00 [esc] — a frozen clock and an interrupt hint for something that had already finished — on every subsequent frame. A stopped-but-mounted chrome row is byte-identical frame after frame, which is exactly what the renderer treats as settled content, so once the viewport scrolled past it the row committed into the terminal's own scrollback and stayed there permanently, wedged between two finished tool cards.
  • /secret list says what the composer chip is counting. The chip counts what the obfuscator will substitute and the list read the vault, so a session masking ten auto-detected environment values showed 10 masked beside "No active secrets. Nothing is being substituted right now." — both true of the same session, and no command in the product could name any of the ten. Values detected in the environment or declared in secrets.yml now carry the variable name or the file path as a label, the list reports them from the same counter the chip reads, and a value that arrived with no label at all is counted and reported as such rather than silently dropped from the total. A label is not a name: it makes a value findable and grants no #NAME# expansion. The empty-vault answer no longer claims nothing is being substituted while something is.
  • Every link in the documentation resolves. Folding twenty-nine flat topic pages into the handbook moved each one two directories deeper without rewriting the links inside it, so 76 internal links pointed at paths that no longer existed — a page about MCP configuration reaching for ../../../mcp-config.md, the advisor page citing source files three levels above the repository — and three anchors named headings the merged reference page never had. Each link is repointed at the page it means, and bun scripts/check-doc-links.ts reports none dead.
  • A wrapped row of a streaming diff keeps its gutter, so a line too long for the block still reads as one row of the diff.
  • A streaming multi-file edit preview is bounded as one block rather than per file, so the preview stays inside the window it is budgeted for.
  • The Todos board draws nothing rather than overflowing its mount when the terminal is too narrow for one task row.
  • The eval tool's trailing rows — the JSON tree, the timeout line, the notice and the warning — hang from the rail with the rest of the block.
Removed
  • veyyon gallery no longer takes --screenshot, --out, --font or --font-size. The flags rendered the gallery into a PNG through a second capture stack — a headless browser driving a tape recorder — that produced frames at a different terminal, font and colour configuration from the one every other proof uses, so two frames of the same surface could not be compared. veyyon gallery prints the gallery; a picture of a real screen comes from the recorder described in the handbook's verification page, and that is the only capture path.
  • The system prompt no longer computes a date it does not render. The date moved into the per-turn session-state message when the working directory left the cached prefix, and the date and dateTime values were still assembled for the prompt template on every build, where no section reads them. test/session/the-session-state-block-states-the-host-local-day.test.ts states which surface owns the date now: the block states the host's local calendar day, west and east of UTC, and one instant in two zones produces two different days.
Changed
  • The todo board is back to the tree list it was before 1.1.0: one status glyph per task, open work first, the phase in parentheses after the content, and the withheld count on the last row. The panel form shipped in 1.1.0 is reverted — Roman-numeral phase headings, per-phase fractions, the header progress gauge, the indented rail body and the fourteen-frame entrance are all gone. The board is static again: it is drawn once when the result lands and never repaints, so nothing on it moves after a write and no interval is armed for it.
Fixed
  • A todo call that names no operation is refused by validation instead of by the executor. op was plainly optional for one release so a Claude/Cursor TodoWrite payload (which carries todos and no op) would validate, and that made the schema lie about every other shape: {"task":"Scaffold"} and {"operation":"start","task":"Scaffold"} both validated CLEAN — a missing optional field is legal, and an undeclared key is not refused for an ArkType-authored tool — after which the executor answered Missing op; pass op explicitly, naming a field the call had just been told it could leave out. The repair layer reported the same call clean, so all three layers declined to act and a model that believed it had named its operation retried the identical payload. op is now required by a schema-level narrow that admits its absence only alongside todos, so the compatibility whole-board write keeps working and every other shape is refused where the repair loop and the model-facing error path can act on it. A narrow rather than a required property, and rather than a union of the two shapes, because a top-level union converts to anyOf and the Anthropic tool-schema builder reads properties off the root, so a union would advertise the tool as an object with no fields at all.
  • operation and action are repaired onto op for any tool that declares one (todo, goal). A model reaching for the operation field writes the whole word; the value used to sit in the call untouched, because an undeclared key on an ArkType-authored tool is not refused, and the tool then reported the field missing.
  • A todo call carrying an empty todos list is refused instead of guessed at. An empty container is not a read: with merge: false it says "replace my board with nothing", which is destructive, and with merge: true it says nothing at all. It used to be inferred as an init and could clear the board. The refusal names the operation that does mean it — An empty "todos" list cannot initialize or clear todos. Pass op explicitly: op "rm" clears the board — and the recorded board is returned unchanged.
  • An unrecognized op reaching applyOpsToPhases returns the board and names the op instead of returning undefined phases and crashing the next read of it. The tool schema cannot produce one, but the /todo slash command and any extension build ops themselves and never pass through it.

Veyyon is a fork of oh-my-pi 16.5.2 (MIT, by Can Boluk). Everything before the fork is upstream history, not a veyyon release. See oh-my-pi's releases for it.

History

Built on oh-my-pi.

Veyyon is a fork of oh-my-pi (MIT) by Can Boluk, shipped as its own product. Full commit history and per-release detail live on GitHub.