Skip to content

v0.3.0

Choose a tag to compare

@volt-release-tagger-hansjm10 volt-release-tagger-hansjm10 released this 11 Oct 02:23
Immutable release. Only release title and notes can be modified.
598b520

Standalone Windows executables in this release are not code-signed. Windows may show a SmartScreen warning when you first run them.

Highlights

  • Interactive Volt runs its conversations in the Volt daemon's conversation workers, so closing the terminal leaves a conversation running and several terminals can attach to the same one. (#585)
    Volt starts the daemon when none runs and shows the terminal while the conversation starts; what you type meanwhile is sent once it is ready. volt -c, -r, and --session reattach to a conversation that is still running, including a turn in progress. Quitting while a turn runs asks whether to stop it or leave it running in the background. When a conversation's worker exits or the daemon restarts, the terminal keeps its transcript, shows "Reconnecting..." or "Host restarting...", and resumes where it was; after volt daemon stop it starts the daemon again after 10 seconds. A daemon started this way exits after five minutes without conversations, terminals, or paired devices. A daemon of another Volt version is restarted when it runs nothing; otherwise Volt asks you to run volt daemon restart.
  • The Volt daemon now runs in standalone binaries, --omit=optional installs, and on Intel Macs, serving the TUI and conversation workers with phone transport unavailable, and the TUI connects to it on Windows and in standalone binaries.
    A daemon without phone transport refuses pairing with guidance in volt daemon status, volt remote pair, and /remote; workspace, worktree, and device revocation commands keep working. volt daemon status exits 0 when phone transport is unavailable only because the build lacks it (native_binding_missing); volt remote status still exits nonzero. A standalone binary starts its daemon and workers by re-running its own executable, and its daemon, including one the login service starts, resolves PATH and the rest of the environment from the user's login shell like an npm install does. Local clients, conversation workers, and relays now prove their token instead of sending it, bound to a fresh daemon challenge and the socket they dialed, and only trust a daemon that proves it back, so a stale Windows pipe name taken over by another account is never trusted and a hello it captures works nowhere else.
  • Extensions can run background work that every client sees, follows, and cancels. (#585)
    An extension registers a kind with volt.registerWorkKind(name, kind) and starts its work with ctx.startWork(name, { title, input }, run): the work is recorded in the conversation as ext:<extension>/<name>, reports progress, checkpoints, and output, and ends with a result that can ride the model's next turn (delivery: "message") or wake it ("wake"). Only the registering extension starts its kinds, and reloading the extensions interrupts their running work. A paired device cannot read the output of work whose extension kind the host no longer knows. A stop that delivers queued input no longer starts an empty turn when only such a quiet notice is queued.
  • Extensions can be enabled and disabled while a session runs, from /extensions, /extensions enable|disable <id>, or the set_extension_enabled intent, and every open conversation follows without /reload. (#585)
    extensions.<id>.enabled in global or trusted project settings decides whether an extension runs; a disabled extension's factory never runs. Disabling one removes everything it contributed (hooks, commands, intents, shortcuts, completion providers, providers, status items, panels, its title, and pending dialogs), cancels its running work, and removes its tools at the next turn boundary once its running tool calls finish. Enabling one whose permissions are not acknowledged asks the client that enables it to acknowledge them; a paired device with host.manage.v1 may disable extensions but enables only acknowledged ones. Extensions hear activate and deactivate (startup, enable, disable, reload), session_start gains reason enable and session_shutdown reason disable. The extensions query lists every extension with its state and permissions, and work records keep the remote capabilities their kind required.

Breaking Changes

  • agent: Removed AgentHarness, its Session storage, and agent-core's built-in compaction, JSONL and in-memory session storage, skill and prompt-template loading, NodeExecutionEnv and the ./node entry point, and AgentState; the Conversation kernel is agent-core's only stateful runtime. (#585)
    Open a conversation over a log with await Conversation.open({ log, stream, resolveModel, tools, systemPrompt, policy }) instead of constructing AgentHarness with a Session. Use InMemoryConversationLog or implement ConversationLog; the log replaces SessionStorage, and its ordinals replace projection cursors, guards, and mutation receipts. Submit input with prompt, steer, and followUp and await the admission's completion instead of run(), prompt(), or reserveRun()/runReserved(); delivery owners are gone because the conversation commits each delivery itself. Harness event handlers become ConversationPolicy hooks (transformContext, beforeProviderPayload, afterProviderResponse, beforeToolCall, afterToolCall, messageEnd, prepareDelivery, nextAction, requestBoundary, retry, compaction), and subscribe delivers ConversationEvents. Resolve credentials in your stream function. Coding Agent provides its own compaction, storage, skills, and prompt templates.

    Removed exports: AgentHarness, AgentHarnessError, AgentHarnessOptions, AgentHarnessEvent, AgentHarnessStreamOptions (use ConversationStreamOptions), AgentHarnessNextActionPolicy (use ConversationPolicy["nextAction"]), the other AgentHarness* types, the harness event types (QueueUpdateEvent, ToolCallEvent, ToolResultEvent, and the rest), AgentState, NodeExecutionEnv, Session, SessionStorage and its snapshot, cursor, mutation, and receipt types, SessionError, the SessionTreeEntry entry types, buildSessionContext and SessionContext (use fold and buildContext), the delivery-owner types (AgentDeliveryOwner, AgentDelivery, and their contexts and outcomes), AgentRunResult, AgentRunSnapshot, and toError. AgentHarnessAdmissionGate is now AdmissionGate, and suspended admission throws ConversationError with code busy.

  • ai: The AI library now streams through clients created with createAiClient, each with its own providers, models, OAuth providers, and credential source, instead of process-wide registries; it no longer reads API keys from environment variables, and the volt-ai login command is removed.
    To migrate, create a client with createAiClient({ providers: builtInProviders(), models: builtInModels(), credentials }) and call client.stream, complete, streamSimple, completeSimple, refreshPromptCache, and generateImages where you called the global functions; register custom APIs with client.registerProvider and OAuth implementations with client.registerOAuthProvider (the built-ins come from builtInOAuthProviders() in @hansjm10/volt-ai/oauth). Pass API keys per request or through a CredentialSource, whose resolve({ model, signal }) the client consults before each request; a rejection fails the request with an auth error. registerFauxProvider is now createFauxProvider, which returns a provider to register on a client.

    In @hansjm10/volt-agent-core, agentLoop and agentLoopContinue require a stream function (for example client.streamSimple), and AgentLoopConfig.getApiKey is removed.

    In Volt, extensions' registerProvider calls are unchanged. Each model registry owns a client (ctx.modelRegistry.client) that resolves credentials from auth.json, OAuth refresh, environment variables, and models.json, so extensions make model calls with ctx.modelRegistry.client.complete(...) instead of resolving keys themselves. compact, generateSummary, and generateBranchSummary take a required stream function instead of apiKey, headers, and env. Use Volt's /login instead of npx @hansjm10/volt-ai login.

  • ai: calculateCost no longer writes usage.cost in place; it returns a new cost that records the priceVersion of the price table it used, and @hansjm10/volt-ai now exports TypeBox schemas for messages, content blocks, usage, and model metadata.
    To migrate a custom provider or extension that called calculateCost(model, usage) for its side effect, assign the result instead: usage = { ...usage, cost: calculateCost(model, usage) }. The second argument only needs the token counts (input, output, cacheRead, cacheWrite, and optionally cacheWrite1h).

    The schemas are also exported from the @hansjm10/volt-ai/schemas subpath, which loads only TypeBox.

    The RPC contract artifact now declares the optional usage.cost.priceVersion on assistant messages and the optional promptCache model metadata the host already sent with RpcModel and RpcCatalogModel.

  • ai: The AI library no longer reads VOLT_* environment variables, no longer carries clientMessageId on user messages, and no longer applies the conversation replay policy inside providers; callers apply the new applyReplayPolicy(messages) before each request.
    Volt itself reads VOLT_CODEX_REQUEST_DIAGNOSTICS and VOLT_OAUTH_CALLBACK_HOST from the process environment as before, and takes prompt cache retention from the promptCache.retention setting. Direct users of @hansjm10/volt-ai pass cacheRetention: "long" (default "short"), requestDiagnostics: true for OpenAI Codex request fingerprints, and callbackHost in OAuth login options instead. resolvePromptCacheRetention(model, cacheRetention, options) no longer takes an env argument.

    UserMessage and UserMessageSchema no longer have clientMessageId. Volt stores a client input identity on the session entry beside the message ({"type":"message","message":{...},"clientMessageId":"..."}) instead of inside it; existing session files are not migrated. Transcript projections are unchanged.

    Providers now send the messages they are given. Apply applyReplayPolicy(messages) (drops errored and aborted turns with their tool results, explains rejected tool calls, synthesizes missing tool results) to the history before calling stream, complete, streamSimple, completeSimple, or refreshPromptCache; agentLoop and Conversation in @hansjm10/volt-agent-core apply it for you. onPayload no longer receives ProviderPayloadMetadata, and toolResultMessageIndices is removed: Volt marks background-job results collected when they are in the replayed context of a request whose payload hook left the payload unchanged and whose response completed successfully.

  • ai: Every built-in provider now streams through one shared runner with one retry policy: maxRetries (default 0) retries only rate-limit, overload, server, network, and timeout failures, and only before a response starts streaming.
    Each retry waits the server-requested delay, or 1s doubling per attempt, capped at maxRetryDelayMs; a server-requested delay above the cap fails immediately. Provider SDKs no longer retry on their own, so Amazon Bedrock no longer makes its two built-in SDK retries: set maxRetries (in Volt, retry.provider.maxRetries) to retry at the provider level. OpenAI Codex no longer retries quota, authentication, or other rejected requests. An aborted request now always reports "Request was aborted", and a stream emits its start event once the provider accepts the request.

    Custom providers can implement only request building and fragment parsing: pass a StreamProvider definition to createProviderStream, which owns the event stream, abort mapping, retries, and the terminal event.

  • ai: Failed assistant messages now carry a typed error ({ kind, retryable, providerCode?, message }) instead of errorMessage, and Volt decides automatic retries and context-overflow compaction from that error rather than by matching error text.
    kind is one of rate_limit, overloaded, server, network, timeout (retryable), or quota, auth, invalid_request, context_overflow, refusal, invalid_tool_call, stream_limit, aborted, unknown. Providers set it from HTTP status codes and provider error types, so quota and billing limits are never retried and a provider's own wording no longer changes the decision.

    To migrate, read message.error?.message where you read message.errorMessage, and check message.error?.retryable or message.error?.kind instead of matching its text. Custom providers report failures through the stream runner (createProviderStream): throw ProviderStreamError(kind, message) or supply mapError; error fragments pushed into AssistantStreamNormalizer take error: ProviderError instead of errorMessage, and fauxAssistantMessage takes error instead of errorMessage. Session files and the RPC contract store the assistant message's error object (RpcProviderError) in place of errorMessage; the agent proxy protocol's error event carries error as well. An extension that rewrote errorMessage in message_end to trigger compaction should set error.kind to "context_overflow" instead.

  • daemon: Conversation leases are gone: the session list reports a conversation as attached or detached in its worker, and phones are never refused busy while a desktop TUI takes a conversation over. (#585)
    Every conversation runs in a daemon worker that keeps it for its whole life, so nothing hands a conversation between processes any more. A paired device's sessions query reports runtimeState as attached (a TUI's or a phone's stream of the session is open, or its worker is still starting) or detached (a worker keeps it open with no client attached), and omits it when no worker hosts the session; tui-owned, daemon-active, daemon-detached, and daemon-draining no longer occur. A phone's stream relayed to a worker that closed it ends with relay close reason worker_disconnected (formerly tui_disconnected); lease_transferred no longer occurs. volt daemon status and volt remote status list conversation workers instead of leases, and /remote and /worktree reach the daemon through the TUI's own connection to it.

    Migration: phone clients decode runtimeState as attached | detached and auto-connect to attached sessions; drop handling of the drain busy retry and of lease_transferred. The daemon control protocol drops lease_acquire, lease_release, lease_granted, lease_pending, lease_denied, viewer_abort, viewer_end, relay_rpc, relay_rpc_result, relay_notification_delivery, worker_abort, worktree_bind.acquireLease, status_result.leases, the lease_transferred worker stop reason, and the worktrees and rpc_grants capabilities; only conversation workers send change_observe. Restart a running daemon (volt daemon restart) after upgrading. The SDK exports DEFAULT_INTEGRATED_DETACHED_RUNTIME_TTL_MS, parseIntegratedDetachedRuntimeTtlMs, scheduleDetachedRuntimeRetention, and their option types no longer; the daemon's worker registry owns retention.

  • extensions: A session's extensions now start once, however many clients attach: session_start fires once, and every attached client sees extension UI and errors. (#585)
    The host binds a session's extensions once. Further clients (a second RPC client, a second terminal, a phone) attach without another session_start. Dialogs, approvals, notifications, status, panels, and the title belong to the conversation and go to every attached client that accepts them, and the first answer to a dialog or approval wins. A pending dialog or approval stays pending when a client disconnects or the conversation's branch changes, and is shown again to a client that attaches; it ends when answered, when its timeout passes, when the extensions reload, or when the conversation closes. Extension errors reach every client. ctx.newSession(), ctx.fork(), ctx.switchSession(), ctx.navigateTree(), ctx.reload(), ctx.abort(), and ctx.shutdown() act for the client whose request is running, or for the oldest attached client outside any client's request; calls for a client that has left do nothing.

    Migration: extensions that counted on one session_start per connected client must track clients another way; keep session_start handlers idempotent per session. SDK hosts replace await session.bindExtensions(bindings) with await session.attachExtensionClient({ id, mode, remote, commandContextActions, abortHandler, shutdownHandler, onError }).ready; the returned detach() removes the client. To show dialogs and approvals, give the client a live view (acceptsHostRequest(kind) and apply(update)) and answer requests with conversation.liveState.answer(requestId, response, client.id).

  • extensions: Managed context preparation moved from ctx.work to ctx.services, and the daemon's session-to-pull-request association is now called the change association, both on the wire and on disk. (#585)
    Extension authors rename ctx.work to ctx.services (ctx.services.tasks.start, ctx.services.context.requestWait) and volt.getWorkStatus() to volt.getServicesStatus(). The exported ExtensionWork* types are now ExtensionServices* (for example ExtensionServicesContext, ExtensionServicesTaskContext, ExtensionServicesReadResult, ExtensionServicesLimits), except ExtensionWorkService, which is now ExtensionServiceName, and the SDK option extensionWorkLimits is now extensionServicesLimits.

    Protocol clients read the session list's and session_contexts query's changeContext instead of workContext; its shape is unchanged. In @hansjm10/volt-protocol, RpcSessionWorkContextSchema and RpcSessionWorkPullRequestSchema are now RpcSessionChangeContextSchema and RpcSessionChangePullRequestSchema, the RPC_WORK_* limits are RPC_CHANGE_* (RPC_WORK_CHANGE_ID_MAX_CHARS is RPC_CHANGE_ID_MAX_CHARS), and the daemon control request work_observe is change_observe. The daemon stores associations in changes.json; it ignores an existing work-state.json, so sessions rediscover their pull requests the next time they are observed on their branch.

  • extensions: The terminal-only extension API is removed: extensions show their UI only as data (status items, panels, notifications, dialogs, forms, and presentations), which every client renders the same way. (#585)
    To migrate, replace each removed member: ctx.ui.setWidget with ctx.ui.setPanel(name, {placement, node}); ctx.ui.setFooter with ctx.ui.setStatus(key, text) status items; ctx.ui.setHeader with an aboveEditor panel or ctx.ui.notify; ctx.ui.custom with ctx.ui.form, ctx.ui.dialog, ctx.ui.select/confirm/input/editor, or a panel; ctx.ui.onTerminalInput with volt.registerShortcut(key, {intent}) mapping a key to an intent from volt.registerIntent or to a command; ctx.ui.addAutocompleteProvider with volt.registerCompletionProvider(name, {trigger, complete}); ctx.ui.theme and ctx.ui.getTheme with semantic tokens in StyledText and UiNode data. ctx.ui.setWorkingMessage, setWorkingVisible, setWorkingIndicator, setHiddenThinkingLabel, setEditorComponent, getEditorComponent, getToolsExpanded, and setToolsExpanded have no replacement (use ctx.ui.setStatus, setEditorText, pasteToEditor, getEditorText, and shortcuts); the client owns tool expansion. ctx.ui.getEditorText is asynchronous and ctx.ui.setTheme takes a theme name only.

    Tool definitions render only through present(): renderCall, renderResult, disposeRenderState, renderShell, rendersDuration, and the ToolDefinition third type parameter are removed, as are ToolRenderContext and ToolRenderResultOptions. Custom messages render only through volt.registerMessagePresenter: volt.registerMessageRenderer, MessageRenderer, and MessageRenderOptions are removed. Dismissed dialogs resolve undefined; ExtensionUIDismissedError is removed. The types TerminalInputHandler, WorkingIndicatorOptions, AutocompleteProviderFactory, EditorFactory, ExtensionWidgetOptions, WidgetPlacement, ExtensionTerminalUI, ReadonlyFooterDataProvider, KeybindingsManager, and AppKeybinding are no longer exported. @hansjm10/volt-coding-agent no longer exports TUI components (CustomEditor, BorderedLoader, DynamicBorder, FooterComponent, the selectors, keyHint, renderDiff, and the rest) or the ANSI theme helpers (Theme, ThemeColor, initTheme, getMarkdownTheme, getSelectListTheme, getSettingsListTheme, highlightCode, getLanguageFromPath). SDK clients attaching with attachExtensionClient no longer pass ui, themes, or userInput: extension UI and request_user_input questions reach a client as host requests through its live view, and ExtensionClientThemes is removed.

  • extensions: Every extension now declares a manifest whose id is its identity, and an extension without a valid manifest fails to load with an error that says what to fix. (#585)
    A single-file extension (a .ts or .js file, or a directory's index.ts) exports its manifest: add import { defineManifest } from "@hansjm10/volt-coding-agent" and export const manifest = defineManifest({ id: "my-extension", displayName: "My Extension" }). The id is lowercase letters, digits, and -, at most 64 characters; volt, core, builtin, host, and ext are reserved. Single-file extensions load only from your and a trusted project's extension directories, paths in settings, and -e paths.

    A package replaces "volt": { "extensions": [...] } in package.json with its manifest: "volt": { "id": "my-package", "displayName": "My Package", "entry": "src/index.ts" }, keeping skills, prompts, and themes as before. A package declares one extension; entry must be a file inside the package, and the extension's version is the package's version. Packages installed from npm or git load an extension only through this manifest, which Volt reads without running package code. A package filter's extensions key now only turns the package's extension on (omitted) or off ([]).

    Two extensions cannot share an id: a user or -e extension beats a project extension, otherwise the one loaded first wins, and the other is reported and not loaded.

    SDK hosts pass extensionFactories: [{ manifest, factory }] instead of bare factory functions. ExtensionError.extensionPath is now extensionId (the manifest id, or a label such as <runtime> for host errors), and RegisteredTool, ResolvedCommand, ExtensionFlag, and ExtensionShortcut carry extensionId instead of extensionPath.

    Extension work kinds are ext:<manifest id>/<kind> (EXTENSION_WORK_KIND_PATTERN in @hansjm10/volt-protocol now refuses reserved ids). Protocol clients invoke extension commands as the intent extension.command.<manifest id>.<command> instead of an opaque per-catalog name. Command names are a letter or digit, then at most 63 letters, digits, _, and -; when an earlier extension took a name, the later command is /<manifest id>:<command> instead of /<command>:<n>. Volt no longer serves @hansjm10/volt-tui to extensions; an extension that still imports it must depend on it itself.

  • extensions: Extensions see ctx.mode as "rpc" in the interactive TUI, as in every host clients drive; ExtensionMode no longer has "tui", and command handlers see who invoked them as ctx.invokedBy. (#585)
    To migrate, replace each ctx.mode === "tui" check. For UI, check ctx.hasUI (true in every "rpc" host; a dialog no client answers resolves to its default). To allow something only a user at the host may do, such as running commands, check ctx.invokedBy === "local" together with ctx.hasUI in the command handler: "local" is the TUI, a stdio RPC client, or the SDK; a paired remote device, or a client the host no longer knows, is "remote". The startup project trust prompt's project_trust context also reports "rpc".

  • extensions: /new, /resume, /fork, /clone, /import, and the matching extension and RPC actions now open the next session before the current one closes, so a session that fails to open (locked by another process, missing cwd) leaves you in the current session instead of exiting the TUI. (#585)
    The new session's extension instance receives session_start before the old instance receives session_shutdown, and the old instance's session_shutdown handlers no longer reach the client's UI. A session that fails to open receives no session_shutdown. ctx.newSession(), ctx.fork(), and ctx.switchSession() reject inside a subagent's conversation.

    Migration: keep extension state per instance, not in module-level variables shared between the old and new instance; the two briefly coexist. Do not rely on session_shutdown running before the next session_start, or on showing UI from a session_shutdown that moves to another session. SDK hosts drop runWithStableSession(), runSessionInterruption(), trackClientInputAdmission(), waitForSessionOperations(), and isSessionOperationInProgress (AgentSessionRuntime is replaced by ConversationHost): commands act on conversation.session directly, HostedConversation.whileOpen(operation) keeps the current session open until an operation that disposal must wait for settles, and a session change refuses to leave a session that is running a turn, a bash command, a session mutation, or work.

  • extensions: Extensions now have typed settings that /extensions, volt config, and paired clients edit as a form, and the permissions an extension declares are shown at install and checked by the extension API. (#585)
    The settings key that lists local extension paths is renamed from extensions to extensionPaths, in global, project, and profile settings. extensions now holds per-extension state by manifest id: extensions.<id>.settings stores an extension's setting values. Volt ignores an extensions path list and warns until it is renamed.

    An extension declares settings in its manifest (a flat object of string, string enum, boolean, and integer settings) and reads the effective values from volt.settings: each default, then the global value, then a trusted project's value. Changes arrive as the settings_changed event in every open conversation, and volt.updateSettings(values, { scope }) stores values. Type them with ExtensionAPI<ExtensionSettingsOf<typeof manifest>>. Manifests whose defaults, bounds, patterns, or required names do not hold together, or whose string settings name a credential, are refused; keep credentials in the auth storage. Move configuration an extension read from its own environment variables or config files into manifest settings.

    The extension API now enforces declared permissions: volt.exec rejects without exec; volt.registerProvider, volt.unregisterProvider, and provider registration through ctx.modelRegistry throw without providers; and ctx.modelRegistry.authStorage, getApiKeyAndHeaders, getApiKeyForProvider, and login throw without secrets. Without secrets, model requests through ctx.modelRegistry.client go only to catalog models as the catalog has them, and volt.setModel always sets the catalog's model with the given provider and id. Add the permissions your extension uses to its manifest's permissions. volt install, volt update, /store install, and /store update list a package's unacknowledged permissions and ask; declining an install removes the package, and declining a /store update to a new catalog pin keeps the current pin. Acknowledgments are kept in ~/.volt/agent/extension-permissions.json.

    /extensions lists the conversation's extensions and opens one's detail and settings (/extensions <id> opens it directly); installed packages are under "Installed packages". In volt config, Enter on an extension row marked "settings" opens its settings, and Space toggles a row.

  • extensions: Extensions can show panels of UI data above or below the editor and in fullscreen's sidebar, ask dialogs and forms that every client renders, register intents, map shortcuts to them, and complete editor tokens; ctx.ui.getEditorText() is now asynchronous and volt.registerShortcut maps a key to an intent instead of running a handler. (#585)
    New: ctx.ui.setPanel(name, {title?, placement?, node}) shows a UiNode panel (at most 32 KB; 16 per extension) whose actions and forms send only the extension's own intents and commands; ctx.ui.dialog({title, body?, actions}) resolves with the chosen action's id and ctx.ui.form({title, fields}) with the submitted values; volt.registerIntent(name, {label, input?, remote?, requires?, handler}) returns the intent's name, extension.intent.<manifest id>.<name>, and runs handler with input checked against input; volt.registerCompletionProvider(name, {trigger, remote?, complete}) answers the editor_completions query (at most 50 items within one second). ctx.ui.notify and ctx.ui.setStatus also take styled text (spans with semantic tokens), and ANSI styling in their strings becomes tokens.

    To migrate, await ctx.ui.getEditorText(), which resolves with the text of the invoking client's editor, or undefined when that client has no editor or does not answer within 2 seconds. Replace volt.registerShortcut(key, {description, handler}) with an intent and a shortcut: const run = volt.registerIntent("my-action", {label: "My action", handler: (_input, ctx) => ...}); volt.registerShortcut(key, {description, intent: run}); users rebind it in keybindings.json under the intent's name. Status items, panels, and the window title now need an extension's own context: calling them on a withSession replacement context throws. Status keys are 1 to 128 characters (an extension sets at most 32 items of at most 1 KB).

    Protocol clients: extension status is keyed ext_status/<extension id>/<name> with {extension, text} (styled text), panels are ext_panel/<extension id>/<name>, ext_title carries extension, and ext_widget is removed. A patch{key, ops} live item changes a panel's node or a work item's detail in place (remote clients receive patches too). notice.message is styled text, directive adds insert_editor_text, and host requests add dialog ({value} answers with an action id) and editor_text (asked only of the invoking client, or the first attached client). Extension intents arrive as extension.intent.<id>.<name> frames whose input the host checks against the schema in their descriptor; paired devices reach only intents and completion providers their extension marked remote.

  • extensions: volt.on() now throws for an event name Volt does not define, so an extension that subscribes to a misspelled or nonexistent event fails to load with an error naming the extension and the event instead of loading a handler that never runs. (#582)
    Subscribe only to the events listed in the extensions documentation. Remove subscriptions to events that do not exist: for example, replace volt.on("session_switch", ...) with volt.on("session_start", ...), which fires with reason new, resume, or fork when the session changes. TypeScript extensions that type volt as ExtensionAPI already get a compile error for an unknown name.

  • lsp: Language server install prompts are now host actions kept in the conversation: an approved install shows as a work item with its progress and output, a client granted host management can cancel it, and a prompt still open when Volt stops is recorded as interrupted and never runs. (#585)
    An install is offered only while a client that accepts approvals is attached; denying, dismissing, or letting the prompt time out records the action as cancelled without running it. Aborting a run leaves an install the user approved running.

    Protocol clients see a host action as host_action work (the client fold's work, and the live work/<workId> value while it runs): its approval host request uses the work id as its requestId. The live host_action/<id> value is removed; read progress from work/<workId> and the outcome from the work_finished entry. cancel_work and work_output on a host action need host.manage.v1 on the remote profile. The action_completed push notification kind is removed; it was never sent.

    SDK callers answer approvals by attaching a client that accepts approval to session.liveState instead of passing hostInteraction to createAgentSession or calling session.setHostInteraction(). The HostInteraction, HostActionDecision, HostActionUpdate, and related exports are removed; session.hostActions.run(request, executor) runs a host action, and LspManager takes hostActions instead of hostInteraction.

  • jobs: Background jobs are now work items kept in the conversation: a finished job's output stays readable after a restart, a job running when Volt stops ends interrupted, and a completed or failed job wakes the conversation with a notice. (#585)
    A job that finishes while a jobs wait watches it, or that the model already read, queues no notice, and a turn that ends on a final response, a policy, or a tool stop no longer wakes the conversation for the jobs it started. Job ids are work ids, and jobs read of a job from an earlier session or runtime returns its recorded result instead of failing.

    Protocol clients see jobs as job work items (the client fold's work, and the live work/<workId> value while one runs): replace the cancel_job{jobId} intent with cancel_work{workId}, the job_output{jobId} query with work_output{workId}, and the live jobs value with work/<workId>. Completion notices are work_notice custom messages instead of background_job_notification. @hansjm10/volt-protocol no longer exports the RpcBackgroundJob* schemas or LiveJobsValueSchema.

    SDK callers read jobs through session.jobs and running work through session.hasRunningWork and session.work.waitForIdle() instead of session.backgroundJobs, session.hasBackgroundJobs, and session.waitForBackgroundJobs(); HostedConversation.work is the session's work registry.

  • rpc: The RPC contract artifact moved to the new @hansjm10/volt-protocol package, which now also publishes the conversation log entry schemas and the UiNode UI schema.
    Read the contract from packages/protocol/contract/protocol-schema.json in the repository, or from @hansjm10/volt-protocol/contract/protocol-schema.json in the published package. packages/coding-agent/contract/rpc-schema.json is gone and the coding agent package no longer ships a contract directory; there is no compatibility copy. Existing wire definitions keep their names and shapes; the artifact adds LogEntry.*, LogEntryPayload.*, and Ui* definitions and an x-volt-limits.uiNode block. Extensions can import the schemas from @hansjm10/volt-protocol.

  • remote: A phone that starts a new session, forks, or switches now moves alone, and the other clients of its session stay on it. (#585)
    The phone gets its intent's accepted{conversation}, then its stream ends with ended{moved} naming the new session. The new session is recorded as the phone's last session, so target: "last" lands there, and it opens in a conversation worker when the phone reconnects. The session left behind stays open for its other clients, or detached until retention closes it, and keeps running a turn in progress. Executing a plan in a new session from a phone now works: the plan's execution is queued in the new session and starts when the phone reconnects. A session id no longer stands for another session.

    Migration: phone clients reconnect with target: "session" and the new session's id after ended{moved}. The handshake selection session_rekeyed and its conversation.requestedSessionId, and the conversation_bootstrap reason session_rebind, are removed; drop any handling of them. Restart a running daemon (volt daemon restart) after upgrading so it matches the TUI.

  • remote: Paired devices now speak Volt protocol 1 over Iroh ALPN volt/1: a phone subscribes to its conversation's log, receives entries and a live lane on the remote profile, sends intents, and runs queries, whether the daemon or a desktop TUI serves the conversation. (#585)
    The remote profile carries transcript views only, redacts workspace and worktree paths and provider signatures from every frame at one send, bounds snapshots to the last 200 entries (older ones through history) and resumes to 1,000 entries (a snapshot replaces a longer gap), caps each connection at 16 subscriptions and a refilling budget of 16 snapshot, replay, and query reads, resets a device that falls more than 64 MiB behind on a stream (128 MiB across its streams) instead of slowing the conversation, and ends a stream whose frame exceeds 4 MiB with fatal{frame_too_large}. Every frame rechecks the device's grant: a stream whose grant or workspace authorization changed ends with fatal{revoked}, and revoking a device or changing its access closes its connections at once. A phone's session change answers accepted{conversation} and ends its stream with ended{moved}. Workspace discovery and management streams serve their purpose's queries and intents. Approvals reach only devices granted host.manage.v1, and extension form patterns that could backtrack without bound are refused. MCP calls report progress as live tool items, and changed now also announces sessions, intents, extensions, and host (keep-awake, web search key, and the shared theme, now the host_status query's theme). Completion notifications are push only. The settings query names the active settings profile that the compaction intents expect, and a branch-fenced intent is rejected stale when the branch switches while it runs.

    Migration: update paired apps to protocol 1 (see docs/iroh-remote-protocol.md); a device on volt-rpc/0 can no longer connect. Restart the daemon after upgrading. In the SDK, serve a paired device's stream with serveIrohRemoteConnection instead of runIrohRemoteRpcMode; the Iroh command filter, outbound filter, and transcript text exports (createIrohRemoteFilteredRpcTransport, createIrohRemoteOutboundFilteredRpcTransport, sanitizeIrohRemoteOutbound, getIrohRemoteRpcCommandCapabilities, the handleIrohRemote*RpcCommand handlers, the IROH_REMOTE_*_RPC_TYPE constants, and their types) and ConversationTranscriptCommittedEvent are removed; uploadIrohRemoteDeviceLog uploads device logs, and attachCompletionNotifications pushes a device's completion notifications.

  • protocol: The four review start intents, review_uncommitted, review_branch, review_pr, and review_commit, are now one review intent with a target. (#725)
    Send review with target set to uncommitted, branch, pr, or commit, plus the fields of that target (base for branch; number and url for pr; ref, required, for commit) and the same controls as before. A field that belongs to another target, or a commit review without ref, is rejected invalid_input. tools and url stay local only. Completions use intent_completions with intent: "review" and the field base, number, url, or ref. The result is still {workId}. The old intent names are no longer accepted; the protocol contract in packages/protocol/contract/protocol-schema.json lists the new schema.

  • review: Review anchors, handoff aliases, the General discussion, and finding discussions are now kept in the conversations' own logs instead of separate store tables. (#585)
    Opening a session store from an earlier version upgrades it to schema v5, which drops the review anchor, alias, discussion, and discussion-child tables without carrying their rows over. Reviews run before the upgrade stay readable and actionable in each conversation that holds them, as unanchored reports: they no longer link handoff copies to their source, their finding discussions are no longer linked to their source review, and starting discussions or moving the General for them needs a new review run. Older Volt versions cannot open an upgraded store.

    A review run is anchored by the conversation that ran it, from the moment it starts. Fork, clone, and import never carry review or work records, so a copy of a review stays a local report.

    Protocol clients: the review.general query result no longer has generalRevision; a General moves when its source records the move, and of two concurrent moves from the same General one fails.

    SDK callers: SessionManager.getReviewDiscussion() returns the discussion link a child's log records (discussionId, runId, findingId, source, contextSnapshot) instead of a store lookup, and SessionManager.getReviewState() returns a conversation's review state. The session store no longer has review anchor, alias, or discussion operations; SQLiteSessionStoreClient.findReviewRun, findReviewDiscussion, and findReviewDiscussionChild read its derived review indexes.

  • review: Reviews now run as work items kept in the conversation: every client sees a running review's progress and can cancel it, a review running when Volt stops ends interrupted instead of staying unfinished, and opening a finished review opens a session seeded with its findings. (#585)
    A review's work id is its run id. Aborting the conversation's run leaves a review running. Reviews no longer write unfinished run records or volt.review.usage entries: a running review's accounting is its work item's detail, written once per pass, and the finished run's volt.review.run record keeps the final accounting. Runs and usage entries written by earlier versions with status unfinished are ignored.

    Protocol clients follow a review as a review work item (the client fold's work, and the live work/<workId> value while it runs): the review intent answers {workId} instead of {workflowId}, replace the review_cancel_workflow{workflowId} intent with cancel_work{workId}, open a finished review's findings with open_work{workId} (or review_open_session), and replace the live workflow/<workflowId> value with work/<workId>. The review.workflows query is now review.runs; it no longer returns activeWorkflows, and its runs always carry endedAt. Review completion pushes are work_finished notifications with workId and workKind instead of review_completed with workflowId; a push relay must accept the new kind and fields (the example Firebase relay does). @hansjm10/volt-protocol no longer exports the RpcWorkflow*, RpcReviewWorkflowDescriptor, RpcReviewWorkflowLifecycleStatus, RpcProjectionTruncation, or LiveWorkflowValueSchema schemas, and ReviewWorkflowStartedSchema is now ReviewStartedSchema.

    SDK callers start and observe reviews through the conversation's work registry instead of HostedConversation.reviewWorkflows; runReviewWorkflow takes the registry as work, and its hooks and executeReviewWorkflow no longer emit workflow events.

  • rpc: The legacy RPC command, response, and event schemas are removed from @hansjm10/volt-protocol and its contract artifact, and hosts now reject identifiers and conversation input past their byte limits. (#585)
    The contract artifact (contract/protocol-schema.json) declares only protocol 1 frames, log entries, the daemon control protocol, the Iroh handshake, and UiNode. The RpcCommand, RpcResponse, RpcServerEvent, and RpcClientMessage definitions, the UI action, transcript, session state, and stream frame definitions, and the x-volt-open-events note are gone. x-volt-limits drops the session state, transcript, session tree, message image, and error code sections; the remote profile's bounds are under remoteProfile, and the queue depth is conversationInput.queueMaxItems. Intent descriptors carry IntentStateValue, IntentPresentation, and IntentSlashAlias, and intent_completions returns IntentOptions. Intents and queries reject an identifier longer than 256 UTF-8 bytes, a message longer than 512 KiB, and more than 8 images as invalid_input; a frame whose intentId, queryId, or subscriptionId is longer than 256 UTF-8 bytes ends the connection with fatal{invalid_frame}. An embedded runRpcMode(..., { exitProcess: false }) now rejects with the log's error when its conversation loses its log, instead of resolving. The daemon audit log records a completion notification it skipped for a device without an enabled push target as push_notification_skipped (was push_notification_fallback).

    Migration: import protocol types from @hansjm10/volt-protocol (ClientFrame, HostFrame, IntentDescriptor, IntentInput, QueryResult, LiveValue) instead of the Rpc* and UiAction* types @hansjm10/volt-coding-agent re-exported (RpcCommand, RpcResponse, RpcSessionState, RpcExtensionUIRequest, RpcTranscriptItem, UiActionDescriptor, and the rest); payload types such as RpcGitContext, RpcCatalogModel, and RpcSessionListItem are exported by @hansjm10/volt-protocol. Session event listeners receive fast_mode_changed{enabled} instead of ui_action_state_changed. Subagent handles no longer have getState() or getTranscript(): subscribe to the child's conversation, or read handle.conversation.

  • rpc: RPC mode, JSON mode, and SDK loopback clients now speak Volt protocol 1: a client subscribes to a conversation's log by position, receives its entries and a live lane, sends intents, and runs queries. (#585)
    volt --mode rpc starts with hello and welcome; a client subscribes to the conversation welcome names (after: "snapshot", or the newest ordinal it saw to resume) and receives snapshot, entry, head, and live frames. Commands are intent frames ({type, intentId, input}) answered accepted or rejected, with the durable client message id as a prompt's intent id; reads are query frames. Streaming text, tool progress, the run phase, token use, Git and prompt-cache status, running work, extension status, and dialogs are live values and items; extension errors are notice items. Session changes answer accepted{conversation} and end the old subscription moved. volt --mode json writes one local-profile subscription's frames (a snapshot, entries, live frames, then ended) instead of session events.

    Migration: rewrite RPC clients against docs/rpc.md. State reads (get_state, get_transcript, get_messages, get_session_stats, get_last_assistant_text, get_fork_messages, get_session_tree, get_pending_host_actions) come from the subscription: the client fold (clientFold in @hansjm10/volt-protocol), the live phase, usage, and git values, and the history and content queries. extension_ui_request/extension_ui_response and host_action_request/host_action_response become host requests (host_request/<id> live values answered with host_response; declare the kinds you answer in hello.accepts.hostRequests). invoke_ui_action, get_ui_actions, and get_commands become intents and the intents query; cycle_model and cycle_thinking_level become set_model and set_thinking_level with the models query's cycleScope; models_changed becomes changed{catalog: "models"}; subagent_get_state and subagent_get_transcript become a subscription to the subagent's conversation. JSON mode readers take entry frames for committed messages and live items for streaming text. In the SDK, RpcClient becomes spawnRpcClient, createInProcessRpcClient becomes createLoopbackClient, and both return a ProtocolClient (intent(), query(), prompt(), waitForIdle(), state, live); RpcTransportClient is no longer exported, so serve other transports with serveConnection(transport, localProfile, { host, conversation }). SubagentManager no longer takes requestTimeoutMs, and subagent events are the child session's events.

  • rpc: Stopping intents (abort, abort_bash, abort_retry, cancel_work) now run as soon as they arrive instead of waiting behind a long intent such as compact, and local protocol clients can navigate the session tree, label entries, reload, import and export JSONL, take queued input back, delete and rename stored sessions, and read the conversation's info, resources, and tools. (#585)
    A stop no longer waits for the frames sent before it. A client that needs an earlier intent admitted before it stops a run waits for that intent's accepted first.

    The live fold moved to @hansjm10/volt-protocol: import foldLiveFrame, foldLiveItems, foldLiveCommit, emptyLiveFold, liveCommitOf, patchLiveValue, and LivePatchError from there instead of @hansjm10/volt-coding-agent.

    Every client also sees the host's own notices (a cancelled or failed compaction, retries that gave up, an Anthropic subscription login), when a scheduled retry starts and what failed before it, when work started and finished, and the keys extensions bind to intents; local clients also see how many paired devices are attached (live presence). Paired devices are refused every addition that names host paths or the host's stored sessions.

  • sdk: Agent sessions now run on the conversation kernel over their session log, so AgentSession.create is async, the model, thinking level, and Fast mode come from the log, and queued input survives restarts.
    Create sessions with await AgentSession.create(config); the constructor is private. A configured model or thinking level the branch does not name is committed as a model_change or thinking_level_change entry when the session opens, and setModel, setThinkingLevel, and setFastModeEnabled commit entries; read the current values from the session instead of keeping your own copy. A resumed session whose model has no credentials commits its fallback model.

    Every prompt, steer, follow-up, and extension message is a durable client input. Queued input is recovered after a restart, clearQueue() settles it as withdrawn (clients still receive client_input_outcome with reason queue_cleared), and input sent without a clientMessageId gets a local identity. A steer or follow-up sent while the session is idle starts a turn instead of waiting for the next prompt. A prompt whose delivery transaction rolls back now fails instead of staying queued, and a turn policy that stops a turn before its first request withdraws the prompt that turn would have delivered.

    While a session is open, its SessionManager writes go through the session and are refused after dispose(). appendCompaction, branch, resetLeaf, and branchWithSummary are refused on a live session; use session.compact() and session.navigateTree(). The client-input and delivery writers reserveClientInput, markClientInputQueued, transitionClientInput, rollbackClientInput, commitCanonicalCommand, issueCanonicalProjection, commitDelivery, retainDelivery, terminalizeDelivery, attestDeliveryNoEffect, and verifyDeliveryReceipt are removed; the session records client inputs itself. In agent-core, a next-action policy now sees pause as the default action when a turn would compact between requests and may request instead, a turn that a tool batch stopped still compacts past the threshold, and a retry whose loop fails ends with success: false.

  • sdk: The SDK hosts sessions in a ConversationHost instead of AgentSessionRuntime, and an extension command run through an RPC prompt can now change sessions. (#585)
    Every mode (TUI, print, JSON, stdio RPC, daemon phone streams, subagents) is a client of a ConversationHost. The host binds a session's extensions once, in the host's extensionMode (ctx.mode), and attaches each client's surface (UI, session actions, error listener) whenever the client joins a session, including the one a session change moves it to. A session change opens the new session, fires its session_start, then closes the old one with its session_shutdown. An extension command that calls ctx.newSession(), ctx.fork(), or ctx.switchSession() from a stdio RPC or phone prompt no longer fails with "durable client input outcome is ambiguous": its own prompt input completes as it leaves the session.

    Migration: replace createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager }) with new ConversationHost({ factory, agentDir, extensionMode }) and host.open({ kind: "adopt", sessionManager, cwd }), then attach a HostClient (id, anchor, surface, and move: { kind: "in_place", prepare?, onMoved }) with host.attach(client, conversation). Replace runtime.newSession(), switchSession(), fork(), importFromJsonl(), and executePlan() with openNewSession(host, client, ...), openStoredSession(), openStoredSessionById(), openFork(), openImport(), and executePlan(host, client, ...). Move the work of setRebindSession, subscribeSessionReplaced, and setBeforeSessionInvalidate into the client's onMoved and prepare; put the client's extension UI, session actions, and error listener in its surface instead of attaching to each new session yourself. Read runtime.session, services, diagnostics, lost, and conversationProjectionFeed from the HostedConversation (projectionFeed), and close it with host.close(conversation) or host.dispose() instead of runtime.dispose(). CreateAgentSessionRuntimeFactory is now ConversationFactory, CreateAgentSessionRuntimeResult is ConversationFactoryResult, and AgentSessionRuntimeDiagnostic is AgentSessionDiagnostic. runPrintMode and runRpcMode take (host, conversation, options) (InteractiveMode takes a connector; see the ConversationConnector entry), and the RPC option disposeRuntimeOnClose is removed. SubagentManager's onRuntimeCreated event carries the child's host and conversation instead of runtime.

  • sdk: InteractiveMode connects through a ConversationConnector; SDK apps start it with InProcessConnector. (#585)
    The TUI's client follows its session changes by reconnecting: /new, /resume, /fork, /clone, /import, opening review results, and a plan executed in a new session open the new session in the host, end the client's connection, and the client connects to the new session, which shows from a snapshot. The session it left closes once the new one started, as before.

    Migration: replace new InteractiveMode(runtime, options) with new InteractiveMode(InProcessConnector.start({ host, conversation, modelScopePatterns }), options), where host is a ConversationHost and conversation the conversation it opened; options.modelScopePatterns moved to InProcessConnector.start. ConversationConnector, ConnectorTarget, ConnectorOpenOptions, OpenedConversation, connectThrough(), InProcessConnector, and InProcessConnectorOptions are exported. To drive a client yourself, connectThrough(connector, options) returns a ProtocolClient (option followMoves: "reconnect") that follows the connector's moves. An in-process connector runs without the Volt daemon: it serves no phones, and /remote and /worktree report the daemon unavailable.

  • sdk: The steering and follow-up queue is read from the session log's client inputs, so every queued entry names the client input it delivers.
    queue_update session events list { clientMessageId, text }: queueEntryId is removed, and input sent without a clientMessageId carries the local- identity the session gave it instead of a local-queue: identity. The queue limit counts every queued input, extension messages included.

    SessionManager.getClientInputRecoveryPlan() and getRecoverableQueuedClientInputs() are removed. Use clientInputRecovery(sessionManager.getConversationState()) from @hansjm10/volt-agent-core; its records are the recoverable queued inputs.

    A client_input_outcome session event reports the outcome of identified input that was queued, including input queued before a restart.

  • rpc: Every RPC command, remote host command, and TUI slash command now runs through one intent and query registry, and a paired device reaches each one only within the capabilities its grant holds. (#585)
    The protocol gains the workspace and push intents and queries (register_push_target, unregister_workspace, create_worktree, remove_worktree, prepare_pr_review, worktrees, workspace_directories, agent_options, session_contexts, pr_review), web_search_status beside host_status, and set_default_model, set_default_thinking_level, and the MCP device and browser sign-in (mcp.auth_start_device, mcp.auth_start_browser) as intents. A malformed grant holds no capabilities. set_model and set_thinking_level with the default persisted save that default after the conversation's model or thinking level changes and its events are published, not before. mcp.auth_start_browser without a redirectUrl starts the server's configured sign-in instead of failing. Malformed input that the intent schemas catch is rejected as invalid_input.

    Migration: runRpcMode no longer takes requireRemoteSafeUiActions; serve a paired device's stream with serveIrohRemoteConnection (see the paired-device protocol entry). registerPushTarget now receives the typed RpcRegisterPushTargetArgs.

  • extensions: ctx.newSession(), ctx.fork(), and ctx.switchSession() now resolve with the id of the session the client moved to. (#585)
    They resolve with { cancelled: true } or { cancelled: false, sessionId, seeded } (SessionIntentResult). Executing a plan in a new session records the hand-off in the source session, and a review fix of every finding acknowledges the run in the source session, before the source closes; a failure there keeps you in the source session. Opening a review's findings no longer fails with "opened without findings": the findings are written into the new session before it opens.

    Migration: read the new session's id from sessionId instead of reading ctx.sessionManager after the switch. A cancelled result no longer carries seeded; check cancelled before reading the other fields. A call that no client handles now reports { cancelled: true } instead of { cancelled: false, seeded: false }. SDK callers use SessionIntentResult in place of the removed AgentSessionReplacementResult.

  • sdk: SessionManager is now the session catalog and a read view of one session, sessions are written through a SessionWriter, and every session write resolves only after it is saved.
    The write methods moved off SessionManager. Before a session opens, write through sessionManager.logWriter (appendMessage, appendCustomEntry, appendCustomMessageEntry, appendModelChange, appendThinkingLevelChange, appendFastModeChange, appendPlanningState, appendSessionInfo, appendLabelChange, recordStartingGitContext, recordPrReviewBinding, and the branch writes appendCompaction, branch, resetLeaf, and branchWithSummary). While an AgentSession is open, write through session.sessionWriter; the log writer refuses writes while a session holds the log. Code that runs in either phase takes the SessionWriter interface, whose sessionManager is the view it writes. The model, thinking level, Fast mode, plan, name, and label writes resolve to void; read the entry they appended from getLeafId(). recordStartingGitContext(gitContext) no longer takes a session id.

    Writes are async and resolve after the write commits; until then their entries are not in getEntries() or getBranch(), and nothing observes an entry before its save commits. A write whose save is rolled back rejects with SessionAtomicAppendError and leaves the session unchanged and writable. AgentSession.updatePlan, updatePlanProgress, requestReplan, submitPlan, changePlan, discardPlan, setFastModeEnabled, and setSessionName are async too, and the extension API's volt.appendEntry, volt.setLabel, and volt.setSessionName return a Promise<void>. To migrate, await each write before reading what it wrote. SessionManager.flush(), materialize(), getIndexedOrdinal(), and AgentSession.flushPlanningState() are removed: await the writes instead, and use closePersistence() to wait for every write before closing. SessionManager.getCommittedOrdinal() is renamed getOrdinal().

    Extension ctx.newSession({ setup }) now passes setup an async SessionWriter for the new session instead of its SessionManager; read the new session through writer.sessionManager.

    A SessionManager holds one session for its whole life: newSession() and createBranchedSession() are removed. Create a session with SessionManager.create(cwd, sessionDir, options) or SessionManager.inMemory(cwd, options) (which now takes id, parentSession, and origin), and copy one branch of a session into a new one with SessionManager.createBranched(source, leafId).

    buildSessionContext (the function and the method) and SessionContext are removed in favor of the conversation fold. sessionManager.getConversationState() returns the fold of the session's log: its context holds messages, model, thinkingLevel, and fastMode (now a boolean), and its planning is null until the branch commits a plan state. For log entries outside a session, use fold and buildContext from @hansjm10/volt-agent-core. prepareCompaction(pathEntries, messages, settings, context) now takes the branch's context messages.

  • sdk: AgentSession drops the members only the TUI used, and ExtensionRunner and ModelCycleResult are no longer exported. (#585)
    To migrate: executeBash(command, onChunk, options) becomes runUserBash(command, { excludeFromContext, operations }) (extensions see user_bash first; output shows as the live bash value), and recordBashResult has no replacement beyond runUserBash with the operations that produce the result. getSteeringMessages(), getFollowUpMessages(), and getQueuedWorkNotices() are replaced by the queue_update event or clientInputRecovery(sessionManager.getConversationState()). cycleModel() and cycleThinkingLevel() are replaced by the models query's cycleScope and setModel(model, { source: "cycle" }) or setThinkingLevel(level); toggleAgentMode() by setAgentMode(mode). getUserMessagesForForking() and getLastAssistantText() are replaced by reading sessionManager.getEntries() and messages. InteractiveMode's options.settingsScope says where the TUI reads its display settings before its client connects.

  • sessions: Forked, cloned, and imported sessions now start with a forked_from entry that records their source, followed by a copy of only the source's branch up to that point, and imports get a new session ID.
    /fork, /clone, --fork <id>, and imports (/import, or a path to --session or --fork) copy the branch from the root to the fork point, then its labels; other branches stay in the source. --fork <id> and imports previously copied every branch. An imported session gets a new ID instead of the snapshot's; pass --session-id with --fork <path> to choose it. The snapshot header's parent locator is no longer restored on import. SDK code reads the lineage with SessionManager.getForkedFrom(), and SessionManager.createBranched(source, null) creates a fork of an empty branch. In @hansjm10/volt-protocol, forked_from.entryId is null for an empty branch.

  • sessions: A session can be open for writing in only one Volt process at a time; opening it from volt -p, --mode json, --mode rpc, or an SDK embedding while another process has it open is refused with a conversation_locked error that names the session.
    To migrate, quit the session in the process that has it open, or switch that process to another session, then retry. Listing, searching, exporting, and forking from a session still work while it is open elsewhere. SDK code that opened the same session twice in one process must close the first SessionManager before calling SessionManager.open again, or read it with the new SessionManager.openReadOnly. Protocol clients get the intent rejected with reason locked, and phones receive the conversation_locked handshake outcome. Interactive Volt and phones attach to a session a conversation worker has open instead of opening it again.

    A session whose saved state can no longer be confirmed (another writer appended, the session was deleted, or a commit's outcome could not be resolved) now always ends with an error instead of being reconciled and reloaded in place: the TUI exits with an error that suggests /resume, print runs exit with an error, volt --mode rpc reports the error and exits with code 1 without a stack trace, and the conversation's worker closes it. Reopen the session with /resume (or --session); it resumes from what was saved. Extensions observe the end through the aborted ctx.signal of their commands and then receive session_shutdown as usual; session writes after the loss throw. SDK code replaces SessionManager.getConversationAuthorityStatus(), subscribeConversationAuthorityChanges(), assertConversationAuthorityAvailable(), and retireConversationAuthority() with the lost promise on AgentSession and HostedConversation, and replaces drainPersistence() with closePersistence(), which no longer rejects for a lost log. SessionConversationStateUnavailableError and SessionAtomicAppendError.authority are removed.

  • settings: Prompt cache retention is now the promptCache.retention setting, and Volt no longer reads VOLT_CACHE_RETENTION. (#744)
    The setting is "short" (the default) or "long". It applies to conversations that start afterwards and to open ones after /reload. An invalid value is reported when settings load and "short" applies.

    Migration: replace VOLT_CACHE_RETENTION=long with "promptCache": { "retention": "long" } in settings.json, globally or in a profile.

  • store: The store reads catalog schema version 2, in which every package pins a reviewed commit and lists its manifest id, version, permissions, and review record; the verified flag is gone. (#585)
    volt store and /store show each catalog package's permissions and review (commit, reviewer, date, and notes), and install and update a catalog package only at its reviewed commit: --ref and --track no longer apply to catalog ids, and volt store update <id> moves an install that tracks a branch to the catalog's pin. volt store show and install plans show the package's manifest (id, display name, entry, permissions, and settings) instead of the raw volt field.

    Migration: a custom catalog (VOLT_STORE_CATALOG_URL) moves to "schemaVersion": 2. Drop verified; add version, permissions, and review ({ "commit", "reviewer", "date": "YYYY-MM-DD", "notes" }) to each entry, with id, name, version, and permissions equal to the package's manifest id, displayName, version, and permissions; repo, author, license, categories, and resources are now required. Each source must be git:https://github.com/<owner>/<repo>@<40-character commit> and review.commit must be that commit; npm and local catalog sources are refused. Packages drop image and video from the volt field of package.json, which now holds only the manifest and skills, prompts, and themes; previews belong in the catalog entry.

  • store: The azure-devops, build-ios-apps, review-loop, rtk, and terminal-bench-harbor store packages take their configuration from extension settings, which /extensions and volt config edit as a form. (#585)
    Migration: after updating the packages, move .volt/azure-devops.json into the azure-devops project settings (/ado-config save writes the current values there), VOLT_XCODEBUILDMCP_WORKFLOWS into the build-ios-apps workflows setting, and RTK_DISABLED=1 into the rtk enabled setting (off). Settings live in settings.json under extensions.<id>.settings. review-loop gains maxLoops and baseBranch, and terminal-bench-harbor model, taskLimit, and concurrentTrials. azure-devops no longer declares the fs-write permission.

  • sessions: Stored the sessions of every working directory in one default session store.
    Every working directory's sessions are now in one store, ~/.volt/agent/sessions/sessions.sqlite (under VOLT_CODING_AGENT_DIR when set). Custom session directories (--session-dir, VOLT_CODING_AGENT_SESSION_DIR, the sessionDir setting) keep their own store.

    Migration: sessions in the per-directory stores of earlier versions, ~/.volt/agent/sessions/--<directory>--/sessions.sqlite, are not moved, read, or deleted. Open one with volt -r --session-dir ~/.volt/agent/sessions/--<directory>--; opening it upgrades that store to the new schema.

    The current folder in volt -c, volt -r, /resume, and SessionManager.list, search, and findContinuation is exactly the directory Volt runs in, by its real path: a subdirectory's or a worktree checkout's sessions are listed under All. --session-id with the ID of another directory's session is refused instead of opening it.

    SDK: getDefaultSessionDir(agentDir?) and getDefaultSessionDirPath(agentDir?) no longer take a cwd and return <agentDir>/sessions; listAll() and searchAll() read that one store.

  • subagents: Subagents are now work items kept in the conversation that started them: a subagent running when Volt stops is suspended instead of lost, and resumes only when asked. (#585)
    Each child is recorded as subagent work before its first prompt, and its final report stays readable after a restart. A suspended subagent costs nothing until it is resumed with the subagent tool's { resume: "<id>" } mode or resume_work, which reopens its conversation and lets it finish its task; the first turn after a restart lists suspended subagents to the model. Stopping a turn no longer stops a subagent started outside a tool call; cancel_work does. Subagent statuses use work terms: aborted is now cancelled, and a list shows suspended and interrupted runs.

    Protocol clients start a subagent with start_subagent{agent, prompt} (answering {workId, conversation}) instead of subagent_start, stop it with cancel_work{workId} instead of subagent_abort, and follow it through the live work/<workId> value instead of subagent/<id>; subagent_dispose is gone, since a subagent's conversation closes with its work. A subagent outlives the connection that started it, not its conversation. A client may read only the child conversations its conversation links by subagent work (work_started.child.conversation); paired devices may neither start, cancel, nor resume subagents. Logs no longer hold subagent_spawn entries, and @hansjm10/volt-protocol no longer exports SubagentSpawnEntryPayloadSchema or LiveSubagentValueSchema.

    SDK callers: a SubagentManager passed as subagentToolManager belongs to that one session. SessionWriter.appendSubagentSpawn, SessionManager.getSubagentSpawnEntries, and the spawnRecord start option are removed (pass toolCallId instead); SubagentActivityStatus, SubagentRegistryStatus, SubagentToolStatus, and SubagentToolOverallStatus are replaced by SubagentRunStatus, and resumeDelegation takes no allowedTools (a resumed child gets the session's tool policy).

  • extensions: Tool calls and custom messages now render from presentations, UI data that every client shows the same way: every built-in tool and the host's own messages present themselves, and extensions present their own tools, custom messages, and work. (#585)
    bash, read, write, edit, grep, find, ls, lsp (diagnostics as a table with severity tokens), inspect, web_search (one card per result), web_fetch (the page's details and its text as literal lines), the mcp gateway, request_user_input, jobs (a table of jobs with cancel actions), the planning tools (the plan's checklist as steps), and subagent (a timed step per child, a card per child with an action that opens its conversation) present themselves. Background jobs show as a job card with the job's live status and output under the call. Progress steps show how long each step ran. Edit previews no longer read the file before the edit runs: a pending edit shows its replacements as a diff, and the file's diff with line numbers once it ran. The HTML export presents every tool call and presented custom message.

    New: volt.registerTool({..., present}) takes a pure, synchronous present({args, argsComplete, state, result, cwd}) that returns {title, activity?, summary?, body?, actions?, hidden?, showsDuration?} of UiNode data; volt.registerMessagePresenter(customType, present) presents custom messages as {title?, summary?, body}; and volt.registerWorkKind(name, {..., detail}) presents a running item's detail from its title, input, progress, and output (a kind that requires remote capabilities is presented without its input and output, since every client sees the detail). Presenters run on the host and must be synchronous; a presenter that throws, returns a promise, or returns data that is invalid or larger than 64 KB (16 KB for paired devices) gets the generic presentation (the tool's name, its arguments, and its output), as do calls of tools without a presenter, including a disabled extension's. Actions in presentations send only the extension's own intents and commands, and open_work/cancel_work for its own work.

    To migrate, give tools a present function and custom messages a presenter (the terminal renderers are removed; see the terminal-only extension API entry). An extension tool that takes a built-in tool's name without its own present() presents as the built-in, binding only the extension's own actions. volt.registerMessagePresenter throws for the host's message types (work_notice, review, subagent_recovery, volt-plan-checkpoint, volt-plan-execution), which volt.sendMessage also refuses. ToolExecutionComponent and CustomMessageComponent are no longer exported.

    Protocol clients render tool calls from TranscriptItem.presentation (a ToolPresentation); a custom message whose type has a presenter carries a MessagePresentation. The per-tool fields summary, path, args, details, output, outputTruncated, diffPreview, and patchPreview are removed from the transcript view (a result's content is fetched with the content query), and a tool item's text is now its presentation's title and how the call ended ($ npm test (completed)). The live tool start item carries the running call's presentation, later items carry patch ({summary?, body?} patches of the trees the client holds, at most every 100 ms, so streaming output arrives as appended lines) or a whole presentation, and partial carries only content (partial.details and its toolDetailsMaxSerializedBytes limit are removed). A client that cannot apply a patch resubscribes after its position. Paired devices receive presentations redacted like every other frame, with image nodes as their description and within 16 KB. Work values' detail changes arrive as patch items, and a background job's live value now changes with its output, so clients read work_output again when the value changes instead of polling it.

  • tui: The remote.background setting is removed: interactive Volt always starts the Volt daemon, which now hosts its conversations. (#585)
    Migration: delete remote.background from your settings.json; it no longer has any effect. Interactive Volt starts the daemon on demand, and a daemon started that way exits after five minutes without conversations, terminals, or paired devices. To keep a daemon running across logins, run volt daemon install-service; volt daemon stop stops it. Print, JSON, and RPC modes still run without the daemon.

  • tui: /work and Alt+J open one work inspector for jobs, subagents, reviews, host actions, and extension work, replacing /jobs, /subagents, and their keys; a conversation stays open, and a session switch waits, while its work runs. (#585)
    The inspector lists every work item with its state (suspended and awaiting approval included), progress, kind detail, result, and newest output, and offers what each kind allows: Ctrl+K cancels after a confirmation, Ctrl+R resumes suspended work, and Enter opens the conversation the work runs in or produced. A subagent opens read-only, also after it ended or a restart suspended it. The footer shows open work on one line; a finished job's launch card shows its recorded outcome after a restart instead of "Running at capture"; an extension's own notice text renders as Markdown; notices of finished work queued for the next turn show above the editor; and work that delivers no notice says how it ended in a status line.

    Migration: use /work instead of /jobs and /subagents. In keybindings.json, rename app.jobs.open to app.work.open, app.jobs.cancel to app.work.cancel, and app.jobs.follow to app.work.follow; app.work.resume (Ctrl+R) is new, and app.subagents.open, app.subagents.previous, and app.subagents.next are removed. Leaving a conversation (/new, /resume, /fork, and their intents) is refused while its work runs; cancel the work or wait for it. A daemon keeps a detached conversation while work runs or an operation holds it, but no longer for work awaiting approval or suspended work.

    Protocol clients: open_work on a subagent whose conversation closed answers its conversation, and subscribing to a closed subagent conversation your conversation links answers a snapshot of its log, then ended{closed}. Work progress text, step labels, and step keys a host cut no longer show the start of a host path on paired devices, and a work checkpoint or live work value stays within its bound after redaction (its detail, then steps, are dropped). Extensions can no longer send work_notice custom messages, which are the host's. SDK callers use HostedConversation.isActive() and waitForIdle() instead of session.hasRunningWork and session.work.waitForIdle() for retention, WorkRegistry.busy() for running work, and WorkRecord.checkpoints; a kind's maxActive now counts suspended work, and an item records at most 256 checkpoints.

  • extensions: Volt no longer loads Pi extension packages: the pi package.json manifest key and the @earendil-works/pi-* and @mariozechner/pi-* import aliases are removed.
    To migrate a Pi extension package, replace its pi key in package.json with a volt manifest (see the extension manifest entry), import from @hansjm10/volt-ai, @hansjm10/volt-agent-core, and @hansjm10/volt-coding-agent instead of the Pi package names, and depend on @hansjm10/volt-tui yourself if the extension imports it. Then apply the other extension API changes in this release.

Improvements

  • compaction: Compaction now gives each summarization request its own 10-minute deadline under a 30-minute overall limit, records per-request timing, and writes a debug record when compaction fails. (#569)

  • daemon: Compaction failures in daemon-hosted sessions are now recorded in the daemon log with the session, reason, model, and error. (#470)

  • daemon: Conversations run in worker processes the daemon supervises, conversations in the same workspace share a worker, and running conversations pick up settings and logins saved by other Volt processes without a restart. (#585)
    A conversation opens in a running worker of its workspace that was started with the same environment, extensions, tool policy, and project trust, up to six conversations per worker, instead of starting a process of its own. A detached, idle conversation closes after the daemon's retention time while the others in its worker keep running, and a worker exits once it hosts no conversation. A worker that crashes interrupts every conversation it hosts; their clients reconnect and reopen them. A worker that loses its daemon finishes its running turn (at most 60 seconds) and exits, and a restarted daemon waits for those workers before it serves.

    Each worker writes its own log under daemon/workers/, and volt daemon status lists every worker's pid and log. A worker reloads its conversations' settings when the global or project settings.json changes, and its credentials and models when auth.json or models.json changes, and tells attached clients to refetch them.

  • extensions: The request_user_input tool's questions and an extension's ctx.ui.setTheme now reach clients through the protocol, so RPC clients and paired phones can answer the tool's questions too. (#585)
    The model is offered request_user_input while an attached client accepts user_input host requests: the TUI, an RPC client, or a phone granted conversation control. The questions go to every such client, the first answer wins, and they wait for a client while none is attached. ctx.ui.getAllThemes() lists the host's themes in every mode, and ctx.ui.setTheme(name) sends local clients a set_theme directive instead of saving the theme as the user's: a TUI whose user picked a theme keeps it, and phones never receive it.

  • extensions: ctx.newSession(), ctx.fork(), and ctx.switchSession() that a phone invokes on a daemon-hosted session run withSession once the phone reconnects to the new session, and report seeded: true. (#585)

  • review: Review a branch together with its uncommitted changes with /review branch-uncommitted [base], and in the review intent as target branch_uncommitted.

  • review: Extensions can register a review engine with volt.registerReviewEngine, and /review --engine <name> runs a review on it with the engine's own options as flags, completions, and a usage line.
    The host runs the engine as review work, validates what the engine submits, and records the run. The /review launcher asks which engine reviews when an extension offers one for the target and adds the engine's options to the options form, and the reviewEngine setting makes an engine the default. Protocol clients pass engine and engineParams to the review intent.

  • review: /review starts the review as work and opens the work list on it instead of replacing the editor with a loader, so several reviews can run at once; a review that completes there opens its findings.
    /review reads its command line, usage, and completions from the host's declaration of the review command, shows one options form after you pick a target, and echoes the equivalent command.

  • rpc: A tree navigation (navigate_tree) runs beside a client's other intents like bash and compact, so input sent while it summarizes the branch it leaves queues at once. (#585)
    set_model takes source: "cycle" for a client's model-cycle control, which extensions see on model_select, and the conversation_info query reports whether the conversation's project is trusted (projectTrusted, local clients only).

  • rpc: RPC clients' ! commands now go through extensions' user_bash hooks and show as they run, and opening a session in another project asks that client the project trust question.
    Protocol clients can also change the settings the host reads (set_settings), switch settings profiles (set_profile), choose the models the model-cycle control steps through (set_model_scope), inspect, restart, and trace language servers (lsp.status, lsp.restart, lsp.set_trace), save a debug report (debug_report), and sign in to or out of providers (auth.providers, auth.login, auth.logout). Sign-in pages, device codes, and API key entry reach only the client that started the sign-in, never a paired device, and the key never appears in a frame the host sends. MCP audit records now name the caller as model, local, remote, or cli.

  • subagents: Running subagents report the tool they are using as their work progress, in the TUI and on remote clients.

  • subagents: Subagents working in their parent's directory now share one Git status tracker instead of each running its own git status scans. (#47)
    The parent session's Git status now also updates when its subagents change files. SDK hosts can share trackers the same way by passing a GitContextProviderPool to createAgentSessionServices.

  • tui: Keys you are typing when the project trust prompt or an extension's confirmation appears no longer answer it.
    The trust prompt lists its "Do not trust" answers first, a hook's confirmation lists "No" first, and letters do not move the selection. A decision for this session only applies to the same terminal's later conversations of that project, never to another terminal's. Dismissing the prompt leaves that conversation untrusted and saves nothing; quitting before you answer opens nothing. A managed worktree whose own checkout holds project resources is asked about even when its parent checkout holds none.

  • tui: Bash, read, and write calls with long output collapse to a line count, such as 600 lines or exit 1 · 600 lines, instead of a few lines of the output.
    A write counts its content up while the model generates it and shows short content whole once written; its diagnostics and errors stay visible collapsed.

  • tui: Plan tool cards now show what each call changed: the steps a progress update moved and their notes, the steps a draft update added, a submitted plan's synopsis, and a replan's reason, without the internal revision number.

  • tui: The footer lists open work one row per tool call, with elapsed time and progress, instead of only counts.

  • tui: Tool calls show a status icon instead of a [success] or [failure] label, with their output indented and dimmed under the title and one expand hint for the newest call.
    Read results no longer end with the continuation note meant for the model, and say how many lines of the file they left. Edits show their +N -M beside the path instead of repeating the path above the diff.

  • tui: The TUI runs /model, /scoped-models, /settings, /profile, /login, /logout, /lsp, /mcp, /extensions, /usage, /debug, /review, /plan, /build, and /fast through its host as a client, and masks API keys as you type them. (#585)
    Settings the host reads (personality, transport, review model, images, timeouts, warnings, compaction, queue modes) change through the host; the TUI's own display settings are written by the TUI. A subscription sign-in shows its page in the sign-in dialog and opens it only when it is an http or https address; the code it asks for is entered in the same dialog. An API key shows as bullets and never enters the editor or its history.

    An extension's ctx.reload() reloads the TUI's keybindings and theme too.

    /store installs, updates, and removes packages as the TUI and offers to reload the conversation, which loads the change; an installed extension starts with the reload instead of at once.

    Protocol clients can pin the pull request a review with target pr reviews with url (the url completions offer the current branch's pull request; local clients only), and set_settings and the settings query carry warnings.

  • tui: The TUI reads its own display settings, themes, and startup notices through its protocol client, so after moving to a session in another project or settings profile it applies that project's display settings and themes. (#585)
    The model scope line shows under the startup header instead of above the TUI.

  • tui: The TUI sends what you type, ! commands, and its model, thinking, and mode keys to its host as a client, and messages sent during a compaction wait in the conversation's queue for every client. (#585)
    The queue shown above the editor is the conversation's: the dequeue key and Esc take it back from the host. ! and !! commands run on the host and show their output as they run; one run during a turn waits below the transcript until the turn ends. The slash menu, extension shortcuts, and completion triggers come from the conversation's commands, so a prompt template's argument hint shows in the menu, and the TUI reports an extension shortcut that one of its own keys takes.

    Protocol clients can send steer and follow_up while a compaction or a tree navigation runs: the input waits in the queue and is delivered once it ends. A bash intent no longer holds back the intents and queries sent after it on the same connection; bash intents still run one at a time. The resources query reports keys two extensions bind among its diagnostics. Extension command argument completions use ExtensionCompletionItem (label is optional).

  • tui: The TUI's session commands (/clear, /resume, /fork, /clone, /tree, /name, /session, /copy, /share, /export, /import, /compact, /reload, /worktree, and /quit) go to its host as a client. (#585)
    The /resume picker lists and searches sessions through the host and deletes only sessions of the current folder; it says so for a session of another folder in the All scope. A session resumed or imported whose folder is gone still asks to continue in the current one, and so does an extension command's ctx.switchSession(). Messages sent during /compact wait in the conversation's queue without waiting for the compaction to answer. /reload shows the errors the reloaded setup reports, such as a models.json the host could not read. A /worktree session is stored where the session it leaves is, and is bound to its worktree once the TUI moved to it.

    Protocol clients: a compact intent runs in the same lane of its own as bash, so the intents and queries sent after it (input, withdraw_queued) no longer wait for the compaction. sessions items carry a local-only sessionDir. An extension command's ctx.fork() sends the invoking local client a set_editor_text directive with the text it forked before once the client shows the fork, and ctx.navigateTree() one with the message it navigated before when the client's editor is empty.

  • tui: The TUI's footer, working, retry, and compaction indicators, alerts, plan, work inspector, and extension panels follow the conversation as a client of its host. (#585)
    The host's own notices (a compaction cancelled or failed, retries that gave up, an Anthropic subscription login billing extra usage) show as they arrive; the subscription warning also shows at startup with the other notices the host keeps. In /work, a subagent's conversation lists its own work, so the conversations it started open read-only too, closed ones included. Tab from an empty editor moves to the actions, forms, and trees of extension panels; Tab past the last one or Esc returns to the editor.

  • tui: The TUI draws its transcript as a client of its host, from the conversation's log, its live lane, and the presentations the host computes. (#585)
    Switching sessions shows the new session once it loaded, and a session that loses its log ends the TUI as before.

  • tui: A session whose queued input the TUI replays at startup shows, and takes typing, while that input runs, instead of after it finished. (#585)

Fixes

  • ai: Together's DeepSeek V4 Pro, now listed as deepseek-ai/DeepSeek-V4-Pro-0813, keeps its high reasoning level.
  • compaction: Fixed compaction failing on every attempt with a provider error about tool calls missing results when the session held a turn whose tool calls were rejected or never answered.
  • compaction: Fixed a session failing every request, compaction included, with a provider error about a tool result that has no tool call, after a compaction summarized away the turn that made the call.
  • daemon: Fixed long sessions slowing the daemon by copying the whole conversation each time a message committed. (#749)
  • daemon: The login service from volt daemon install-service keeps starting after brew upgrade node removes the previous Homebrew Node version. (#563)
    The service now records Homebrew's opt/<formula>/bin/node link instead of the versioned Cellar path. Services installed earlier switch to it at the next volt update; to switch now, run volt daemon install-service.
  • daemon: volt update now points an installed login service at the updated installation when the daemon is not running, so the service still starts after an update moves volt. (#556)
    The service is rewritten without starting the daemon. Previously, a pnpm global update or a renamed package left the service pointing at a removed path, and the daemon failed to start at the next login. On macOS the service is unloaded until the next login; run volt daemon install-service to start it sooner.
  • daemon: volt update no longer breaks the installation while the daemon is running; it asks to stop the daemon, updates, and starts it again. (#546)
    The daemon starts from wherever the package manager put the updated package, including pnpm global installs and renamed packages. A daemon the login service was running is restarted through the service. Non-interactive updates refuse instead and print the stop, update, and start commands. If the install step still fails, the error now says the installation may be incomplete and explains how to recover.
  • deps: Updated brace-expansion, fast-uri, ip-address, and proxy-addr to versions that fix published security advisories.
  • extensions: Extension form field patterns with two repeats that can trade characters, such as a*a* or \w+.\w+, are now refused when the form is asked, so an answer cannot make the host backtrack for polynomial time. (#585)
  • extensions: Extensions can no longer write the host's volt.review.* log entries through volt.appendEntry or the writer a new session's setup gets.
  • mcp: Fixed the mcp tool failing for a server that has not connected yet and for resource, prompt, and tool listings with missing optional fields. (#683)
  • remote: Listed, opened, and switched to conversations a terminal started in a workspace's subdirectory or managed worktree checkout on paired phones. (#722)
  • remote: A pairing request that names no workspace is now refused instead of registering Volt's agent directory as a shared workspace named "voltd".
    The daemon control protocol's pair_request now requires workspaceName. volt remote pair and /remote already send it.
  • remote: Volt no longer registers a filesystem root, your home directory, or a directory containing or inside its agent directory as a phone-visible workspace without asking.
    Starting Volt in one of those directories asks whether paired devices with access to all workspaces may reach it: yes registers it as usual, no registers it local to this host. Volt asks once; volt remote workspace add shares it later. A workspace can now be local to the host: paired devices never see or reach it unless their grant names it, and volt daemon status marks it local only.
  • review: Cancelling a PR review no longer leaves Git processes running in the review checkout, which could keep Windows from removing it.
  • review: Cancelling a PR review while its checkout is being validated no longer reports the checkout as changed.
  • sessions: Closing a session waits for the Git status scan it stopped to exit, so Volt can remove the session's worktree right after it closes on Windows.
  • sessions: Fixed session store opens on Windows failing with EPERM while SQLite was removing a database sidecar file (#483).
  • sessions: Opening a session store while another Volt process initializes or upgrades it no longer fails with "store is busy". (#712)
  • settings: A path list under "extensions" in a profile is now reported with the profile it is in and the "extensionPaths" setting to use.
  • store: Installing azure-devops from /store now loads the extension instead of failing on a missing @earendil-works/volt-ai module, and catalog packages install from volt-hq/Volt. (#583)
  • store: volt store install and volt store update review an extension's permissions as volt install does, and an update without a terminal keeps a catalog package at its installed pin when the new pin asks for permissions the installed one did not. (#585)
    In a terminal, an install asks to acknowledge the permissions not acknowledged yet and removes the package when declined; an update to a new catalog pin asks for the new ones and reinstalls the previous pin when declined (or removes the package when the previous pin cannot be reinstalled). Ending a permission prompt with Ctrl+C or Ctrl+D now declines, in volt install and volt update too, instead of exiting with the package installed. Without a terminal, permissions are listed unacknowledged, and an update to a new catalog pin reads the pin's manifest before installing it and skips the package (exit code 1) when it declares a permission the installed pin does not, or cannot be read.
  • store: volt install reviews the permissions of a package installed by a path relative to the current directory, and volt update <source> reviews the package it updated however its source is spelled. (#585)
    /store in the TUI does the same, and when you decline an update's new permissions it reinstalls the reviewed pin, removes the package if that fails, and reviews the reinstalled revision when its source follows a branch, as volt store update does. Declining that revision's permissions too now removes the package in both, instead of leaving it installed unacknowledged, and an install whose package cannot be found to review is removed.
  • tui: Fixed fullscreen mode redrawing slowly as a conversation grew, which made scrolling a long conversation laggy.
  • tui: Fixed the footer showing "cache expired" mid-turn while requests were still renewing the prompt cache. (#724)
  • update: On Windows, the recovery instructions printed when volt update cannot restore a native addon now show normal file paths instead of paths prefixed with \\?\.
  • update: volt update no longer breaks an npm installation while other volt processes, such as a daemon for another agent directory or an open session, have its native modules loaded. (#555)