Repository navigation
v0.3.0
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--sessionreattach 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; aftervolt daemon stopit 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 runvolt daemon restart. - The Volt daemon now runs in standalone binaries,
--omit=optionalinstalls, 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 involt daemon status,volt remote pair, and/remote; workspace, worktree, and device revocation commands keep working.volt daemon statusexits 0 when phone transport is unavailable only because the build lacks it (native_binding_missing);volt remote statusstill 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 withvolt.registerWorkKind(name, kind)and starts its work withctx.startWork(name, { title, input }, run): the work is recorded in the conversation asext:<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 theset_extension_enabledintent, and every open conversation follows without/reload. (#585)
extensions.<id>.enabledin 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 withhost.manage.v1may disable extensions but enables only acknowledged ones. Extensions hearactivateanddeactivate(startup,enable,disable,reload),session_startgains reasonenableandsession_shutdownreasondisable. Theextensionsquery lists every extension with its state and permissions, and work records keep the remote capabilities their kind required.
Breaking Changes
-
agent: Removed
AgentHarness, itsSessionstorage, and agent-core's built-in compaction, JSONL and in-memory session storage, skill and prompt-template loading,NodeExecutionEnvand the./nodeentry point, andAgentState; theConversationkernel is agent-core's only stateful runtime. (#585)
Open a conversation over a log withawait Conversation.open({ log, stream, resolveModel, tools, systemPrompt, policy })instead of constructingAgentHarnesswith aSession. UseInMemoryConversationLogor implementConversationLog; the log replacesSessionStorage, and its ordinals replace projection cursors, guards, and mutation receipts. Submit input withprompt,steer, andfollowUpand await the admission'scompletioninstead ofrun(),prompt(), orreserveRun()/runReserved(); delivery owners are gone because the conversation commits each delivery itself. Harness event handlers becomeConversationPolicyhooks (transformContext,beforeProviderPayload,afterProviderResponse,beforeToolCall,afterToolCall,messageEnd,prepareDelivery,nextAction,requestBoundary,retry,compaction), andsubscribedeliversConversationEvents. Resolve credentials in yourstreamfunction. Coding Agent provides its own compaction, storage, skills, and prompt templates.Removed exports:
AgentHarness,AgentHarnessError,AgentHarnessOptions,AgentHarnessEvent,AgentHarnessStreamOptions(useConversationStreamOptions),AgentHarnessNextActionPolicy(useConversationPolicy["nextAction"]), the otherAgentHarness*types, the harness event types (QueueUpdateEvent,ToolCallEvent,ToolResultEvent, and the rest),AgentState,NodeExecutionEnv,Session,SessionStorageand its snapshot, cursor, mutation, and receipt types,SessionError, theSessionTreeEntryentry types,buildSessionContextandSessionContext(usefoldandbuildContext), the delivery-owner types (AgentDeliveryOwner,AgentDelivery, and their contexts and outcomes),AgentRunResult,AgentRunSnapshot, andtoError.AgentHarnessAdmissionGateis nowAdmissionGate, and suspended admission throwsConversationErrorwith codebusy. -
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 thevolt-ailogin command is removed.
To migrate, create a client withcreateAiClient({ providers: builtInProviders(), models: builtInModels(), credentials })and callclient.stream,complete,streamSimple,completeSimple,refreshPromptCache, andgenerateImageswhere you called the global functions; register custom APIs withclient.registerProviderand OAuth implementations withclient.registerOAuthProvider(the built-ins come frombuiltInOAuthProviders()in@hansjm10/volt-ai/oauth). Pass API keys per request or through aCredentialSource, whoseresolve({ model, signal })the client consults before each request; a rejection fails the request with anautherror.registerFauxProvideris nowcreateFauxProvider, which returns a provider to register on a client.In
@hansjm10/volt-agent-core,agentLoopandagentLoopContinuerequire a stream function (for exampleclient.streamSimple), andAgentLoopConfig.getApiKeyis removed.In Volt, extensions'
registerProvidercalls 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 withctx.modelRegistry.client.complete(...)instead of resolving keys themselves.compact,generateSummary, andgenerateBranchSummarytake a required stream function instead ofapiKey,headers, andenv. Use Volt's/logininstead ofnpx @hansjm10/volt-ai login. -
ai:
calculateCostno longer writesusage.costin place; it returns a new cost that records thepriceVersionof the price table it used, and@hansjm10/volt-ainow exports TypeBox schemas for messages, content blocks, usage, and model metadata.
To migrate a custom provider or extension that calledcalculateCost(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 optionallycacheWrite1h).The schemas are also exported from the
@hansjm10/volt-ai/schemassubpath, which loads only TypeBox.The RPC contract artifact now declares the optional
usage.cost.priceVersionon assistant messages and the optionalpromptCachemodel metadata the host already sent withRpcModelandRpcCatalogModel. -
ai: The AI library no longer reads
VOLT_*environment variables, no longer carriesclientMessageIdon user messages, and no longer applies the conversation replay policy inside providers; callers apply the newapplyReplayPolicy(messages)before each request.
Volt itself readsVOLT_CODEX_REQUEST_DIAGNOSTICSandVOLT_OAUTH_CALLBACK_HOSTfrom the process environment as before, and takes prompt cache retention from thepromptCache.retentionsetting. Direct users of@hansjm10/volt-aipasscacheRetention: "long"(default"short"),requestDiagnostics: truefor OpenAI Codex request fingerprints, andcallbackHostin OAuth login options instead.resolvePromptCacheRetention(model, cacheRetention, options)no longer takes anenvargument.UserMessageandUserMessageSchemano longer haveclientMessageId. 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 callingstream,complete,streamSimple,completeSimple, orrefreshPromptCache;agentLoopandConversationin@hansjm10/volt-agent-coreapply it for you.onPayloadno longer receivesProviderPayloadMetadata, andtoolResultMessageIndicesis 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 atmaxRetryDelayMs; 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: setmaxRetries(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 itsstartevent once the provider accepts the request.Custom providers can implement only request building and fragment parsing: pass a
StreamProviderdefinition tocreateProviderStream, 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 oferrorMessage, and Volt decides automatic retries and context-overflow compaction from that error rather than by matching error text.
kindis one ofrate_limit,overloaded,server,network,timeout(retryable), orquota,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?.messagewhere you readmessage.errorMessage, and checkmessage.error?.retryableormessage.error?.kindinstead of matching its text. Custom providers report failures through the stream runner (createProviderStream): throwProviderStreamError(kind, message)or supplymapError; error fragments pushed intoAssistantStreamNormalizertakeerror: ProviderErrorinstead oferrorMessage, andfauxAssistantMessagetakeserrorinstead oferrorMessage. Session files and the RPC contract store the assistant message'serrorobject (RpcProviderError) in place oferrorMessage; the agent proxy protocol'serrorevent carrieserroras well. An extension that rewroteerrorMessageinmessage_endto trigger compaction should seterror.kindto"context_overflow"instead. -
daemon: Conversation leases are gone: the session list reports a conversation as
attachedordetachedin its worker, and phones are never refusedbusywhile 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'ssessionsquery reportsruntimeStateasattached(a TUI's or a phone's stream of the session is open, or its worker is still starting) ordetached(a worker keeps it open with no client attached), and omits it when no worker hosts the session;tui-owned,daemon-active,daemon-detached, anddaemon-drainingno longer occur. A phone's stream relayed to a worker that closed it ends with relay close reasonworker_disconnected(formerlytui_disconnected);lease_transferredno longer occurs.volt daemon statusandvolt remote statuslist conversation workers instead of leases, and/remoteand/worktreereach the daemon through the TUI's own connection to it.Migration: phone clients decode
runtimeStateasattached | detachedand auto-connect toattachedsessions; drop handling of the drainbusyretry and oflease_transferred. The daemon control protocol dropslease_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, thelease_transferredworker stop reason, and theworktreesandrpc_grantscapabilities; only conversation workers sendchange_observe. Restart a running daemon (volt daemon restart) after upgrading. The SDK exportsDEFAULT_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_startfires 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 anothersession_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(), andctx.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_startper connected client must track clients another way; keepsession_starthandlers idempotent per session. SDK hosts replaceawait session.bindExtensions(bindings)withawait session.attachExtensionClient({ id, mode, remote, commandContextActions, abortHandler, shutdownHandler, onError }).ready; the returneddetach()removes the client. To show dialogs and approvals, give the client aliveview (acceptsHostRequest(kind)andapply(update)) and answer requests withconversation.liveState.answer(requestId, response, client.id). -
extensions: Managed context preparation moved from
ctx.worktoctx.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 renamectx.worktoctx.services(ctx.services.tasks.start,ctx.services.context.requestWait) andvolt.getWorkStatus()tovolt.getServicesStatus(). The exportedExtensionWork*types are nowExtensionServices*(for exampleExtensionServicesContext,ExtensionServicesTaskContext,ExtensionServicesReadResult,ExtensionServicesLimits), exceptExtensionWorkService, which is nowExtensionServiceName, and the SDK optionextensionWorkLimitsis nowextensionServicesLimits.Protocol clients read the session list's and
session_contextsquery'schangeContextinstead ofworkContext; its shape is unchanged. In@hansjm10/volt-protocol,RpcSessionWorkContextSchemaandRpcSessionWorkPullRequestSchemaare nowRpcSessionChangeContextSchemaandRpcSessionChangePullRequestSchema, theRPC_WORK_*limits areRPC_CHANGE_*(RPC_WORK_CHANGE_ID_MAX_CHARSisRPC_CHANGE_ID_MAX_CHARS), and the daemon control requestwork_observeischange_observe. The daemon stores associations inchanges.json; it ignores an existingwork-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.setWidgetwithctx.ui.setPanel(name, {placement, node});ctx.ui.setFooterwithctx.ui.setStatus(key, text)status items;ctx.ui.setHeaderwith anaboveEditorpanel orctx.ui.notify;ctx.ui.customwithctx.ui.form,ctx.ui.dialog,ctx.ui.select/confirm/input/editor, or a panel;ctx.ui.onTerminalInputwithvolt.registerShortcut(key, {intent})mapping a key to an intent fromvolt.registerIntentor to a command;ctx.ui.addAutocompleteProviderwithvolt.registerCompletionProvider(name, {trigger, complete});ctx.ui.themeandctx.ui.getThemewith semantic tokens inStyledTextandUiNodedata.ctx.ui.setWorkingMessage,setWorkingVisible,setWorkingIndicator,setHiddenThinkingLabel,setEditorComponent,getEditorComponent,getToolsExpanded, andsetToolsExpandedhave no replacement (usectx.ui.setStatus,setEditorText,pasteToEditor,getEditorText, and shortcuts); the client owns tool expansion.ctx.ui.getEditorTextis asynchronous andctx.ui.setThemetakes a theme name only.Tool definitions render only through
present():renderCall,renderResult,disposeRenderState,renderShell,rendersDuration, and theToolDefinitionthird type parameter are removed, as areToolRenderContextandToolRenderResultOptions. Custom messages render only throughvolt.registerMessagePresenter:volt.registerMessageRenderer,MessageRenderer, andMessageRenderOptionsare removed. Dismissed dialogs resolveundefined;ExtensionUIDismissedErroris removed. The typesTerminalInputHandler,WorkingIndicatorOptions,AutocompleteProviderFactory,EditorFactory,ExtensionWidgetOptions,WidgetPlacement,ExtensionTerminalUI,ReadonlyFooterDataProvider,KeybindingsManager, andAppKeybindingare no longer exported.@hansjm10/volt-coding-agentno 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 withattachExtensionClientno longer passui,themes, oruserInput: extension UI andrequest_user_inputquestions reach a client as host requests through itsliveview, andExtensionClientThemesis removed. -
extensions: Every extension now declares a manifest whose
idis 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.tsor.jsfile, or a directory'sindex.ts) exports its manifest: addimport { defineManifest } from "@hansjm10/volt-coding-agent"andexport const manifest = defineManifest({ id: "my-extension", displayName: "My Extension" }). The id is lowercase letters, digits, and-, at most 64 characters;volt,core,builtin,host, andextare reserved. Single-file extensions load only from your and a trusted project's extension directories, paths in settings, and-epaths.A package replaces
"volt": { "extensions": [...] }in package.json with its manifest:"volt": { "id": "my-package", "displayName": "My Package", "entry": "src/index.ts" }, keepingskills,prompts, andthemesas before. A package declares one extension;entrymust be a file inside the package, and the extension's version is the package'sversion. Packages installed from npm or git load an extension only through this manifest, which Volt reads without running package code. A package filter'sextensionskey now only turns the package's extension on (omitted) or off ([]).Two extensions cannot share an id: a user or
-eextension 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.extensionPathis nowextensionId(the manifest id, or a label such as<runtime>for host errors), andRegisteredTool,ResolvedCommand,ExtensionFlag, andExtensionShortcutcarryextensionIdinstead ofextensionPath.Extension work kinds are
ext:<manifest id>/<kind>(EXTENSION_WORK_KIND_PATTERNin@hansjm10/volt-protocolnow refuses reserved ids). Protocol clients invoke extension commands as the intentextension.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-tuito extensions; an extension that still imports it must depend on it itself. -
extensions: Extensions see
ctx.modeas"rpc"in the interactive TUI, as in every host clients drive;ExtensionModeno longer has"tui", and command handlers see who invoked them asctx.invokedBy. (#585)
To migrate, replace eachctx.mode === "tui"check. For UI, checkctx.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, checkctx.invokedBy === "local"together withctx.hasUIin 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'sproject_trustcontext 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 receivessession_startbefore the old instance receivessession_shutdown, and the old instance'ssession_shutdownhandlers no longer reach the client's UI. A session that fails to open receives nosession_shutdown.ctx.newSession(),ctx.fork(), andctx.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_shutdownrunning before the nextsession_start, or on showing UI from asession_shutdownthat moves to another session. SDK hosts droprunWithStableSession(),runSessionInterruption(),trackClientInputAdmission(),waitForSessionOperations(), andisSessionOperationInProgress(AgentSessionRuntimeis replaced byConversationHost): commands act onconversation.sessiondirectly,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 fromextensionstoextensionPaths, in global, project, and profile settings.extensionsnow holds per-extension state by manifest id:extensions.<id>.settingsstores an extension's setting values. Volt ignores anextensionspath list and warns until it is renamed.An extension declares
settingsin its manifest (a flat object of string, string enum, boolean, and integer settings) and reads the effective values fromvolt.settings: each default, then the global value, then a trusted project's value. Changes arrive as thesettings_changedevent in every open conversation, andvolt.updateSettings(values, { scope })stores values. Type them withExtensionAPI<ExtensionSettingsOf<typeof manifest>>. Manifests whose defaults, bounds, patterns, orrequirednames 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.execrejects withoutexec;volt.registerProvider,volt.unregisterProvider, and provider registration throughctx.modelRegistrythrow withoutproviders; andctx.modelRegistry.authStorage,getApiKeyAndHeaders,getApiKeyForProvider, andloginthrow withoutsecrets. Withoutsecrets, model requests throughctx.modelRegistry.clientgo only to catalog models as the catalog has them, andvolt.setModelalways sets the catalog's model with the given provider and id. Add the permissions your extension uses to its manifest'spermissions.volt install,volt update,/store install, and/store updatelist a package's unacknowledged permissions and ask; declining an install removes the package, and declining a/store updateto a new catalog pin keeps the current pin. Acknowledgments are kept in~/.volt/agent/extension-permissions.json./extensionslists the conversation's extensions and opens one's detail and settings (/extensions <id>opens it directly); installed packages are under "Installed packages". Involt 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 andvolt.registerShortcutmaps a key to an intent instead of running a handler. (#585)
New:ctx.ui.setPanel(name, {title?, placement?, node})shows aUiNodepanel (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 andctx.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 runshandlerwith input checked againstinput;volt.registerCompletionProvider(name, {trigger, remote?, complete})answers theeditor_completionsquery (at most 50 items within one second).ctx.ui.notifyandctx.ui.setStatusalso 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, orundefinedwhen that client has no editor or does not answer within 2 seconds. Replacevolt.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 inkeybindings.jsonunder the intent's name. Status items, panels, and the window title now need an extension's own context: calling them on awithSessionreplacement 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 areext_panel/<extension id>/<name>,ext_titlecarriesextension, andext_widgetis removed. Apatch{key, ops}live item changes a panel's node or a work item's detail in place (remote clients receive patches too).notice.messageis styled text,directiveaddsinsert_editor_text, and host requests adddialog({value}answers with an action id) andeditor_text(asked only of the invoking client, or the first attached client). Extension intents arrive asextension.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 markedremote. -
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, replacevolt.on("session_switch", ...)withvolt.on("session_start", ...), which fires with reasonnew,resume, orforkwhen the session changes. TypeScript extensions that typevoltasExtensionAPIalready 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_actionwork (the client fold'swork, and the livework/<workId>value while it runs): itsapprovalhost request uses the work id as itsrequestId. The livehost_action/<id>value is removed; read progress fromwork/<workId>and the outcome from thework_finishedentry.cancel_workandwork_outputon a host action needhost.manage.v1on the remote profile. Theaction_completedpush notification kind is removed; it was never sent.SDK callers answer approvals by attaching a client that accepts
approvaltosession.liveStateinstead of passinghostInteractiontocreateAgentSessionor callingsession.setHostInteraction(). TheHostInteraction,HostActionDecision,HostActionUpdate, and related exports are removed;session.hostActions.run(request, executor)runs a host action, andLspManagertakeshostActionsinstead ofhostInteraction. -
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 ajobs waitwatches 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, andjobs readof a job from an earlier session or runtime returns its recorded result instead of failing.Protocol clients see jobs as
jobwork items (the client fold'swork, and the livework/<workId>value while one runs): replace thecancel_job{jobId}intent withcancel_work{workId}, thejob_output{jobId}query withwork_output{workId}, and the livejobsvalue withwork/<workId>. Completion notices arework_noticecustom messages instead ofbackground_job_notification.@hansjm10/volt-protocolno longer exports theRpcBackgroundJob*schemas orLiveJobsValueSchema.SDK callers read jobs through
session.jobsand running work throughsession.hasRunningWorkandsession.work.waitForIdle()instead ofsession.backgroundJobs,session.hasBackgroundJobs, andsession.waitForBackgroundJobs();HostedConversation.workis the session'sworkregistry. -
rpc: The RPC contract artifact moved to the new
@hansjm10/volt-protocolpackage, which now also publishes the conversation log entry schemas and theUiNodeUI schema.
Read the contract frompackages/protocol/contract/protocol-schema.jsonin the repository, or from@hansjm10/volt-protocol/contract/protocol-schema.jsonin the published package.packages/coding-agent/contract/rpc-schema.jsonis gone and the coding agent package no longer ships acontractdirectory; there is no compatibility copy. Existing wire definitions keep their names and shapes; the artifact addsLogEntry.*,LogEntryPayload.*, andUi*definitions and anx-volt-limits.uiNodeblock. 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'saccepted{conversation}, then its stream ends withended{moved}naming the new session. The new session is recorded as the phone's last session, sotarget: "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 afterended{moved}. The handshake selectionsession_rekeyedand itsconversation.requestedSessionId, and theconversation_bootstrapreasonsession_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 throughhistory) 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 withfatal{frame_too_large}. Every frame rechecks the device's grant: a stream whose grant or workspace authorization changed ends withfatal{revoked}, and revoking a device or changing its access closes its connections at once. A phone's session change answersaccepted{conversation}and ends its stream withended{moved}. Workspace discovery and management streams serve their purpose's queries and intents. Approvals reach only devices grantedhost.manage.v1, and extension form patterns that could backtrack without bound are refused. MCP calls report progress as live tool items, andchangednow also announcessessions,intents,extensions, andhost(keep-awake, web search key, and the shared theme, now thehost_statusquery'stheme). Completion notifications are push only. Thesettingsquery names the active settings profile that the compaction intents expect, and a branch-fenced intent is rejectedstalewhen the branch switches while it runs.Migration: update paired apps to protocol 1 (see docs/iroh-remote-protocol.md); a device on
volt-rpc/0can no longer connect. Restart the daemon after upgrading. In the SDK, serve a paired device's stream withserveIrohRemoteConnectioninstead ofrunIrohRemoteRpcMode; the Iroh command filter, outbound filter, and transcript text exports (createIrohRemoteFilteredRpcTransport,createIrohRemoteOutboundFilteredRpcTransport,sanitizeIrohRemoteOutbound,getIrohRemoteRpcCommandCapabilities, thehandleIrohRemote*RpcCommandhandlers, theIROH_REMOTE_*_RPC_TYPEconstants, and their types) andConversationTranscriptCommittedEventare removed;uploadIrohRemoteDeviceLoguploads device logs, andattachCompletionNotificationspushes a device's completion notifications. -
protocol: The four review start intents,
review_uncommitted,review_branch,review_pr, andreview_commit, are now onereviewintent with atarget. (#725)
Sendreviewwithtargetset touncommitted,branch,pr, orcommit, plus the fields of that target (baseforbranch;numberandurlforpr;ref, required, forcommit) and the same controls as before. A field that belongs to another target, or a commit review withoutref, is rejectedinvalid_input.toolsandurlstay local only. Completions useintent_completionswithintent: "review"and the fieldbase,number,url, orref. The result is still{workId}. The old intent names are no longer accepted; the protocol contract inpackages/protocol/contract/protocol-schema.jsonlists 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.generalquery result no longer hasgeneralRevision; 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, andSessionManager.getReviewState()returns a conversation's review state. The session store no longer has review anchor, alias, or discussion operations;SQLiteSessionStoreClient.findReviewRun,findReviewDiscussion, andfindReviewDiscussionChildread 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 writeunfinishedrun records orvolt.review.usageentries: a running review's accounting is its work item's detail, written once per pass, and the finished run'svolt.review.runrecord keeps the final accounting. Runs and usage entries written by earlier versions with statusunfinishedare ignored.Protocol clients follow a review as a
reviewwork item (the client fold'swork, and the livework/<workId>value while it runs): thereviewintent answers{workId}instead of{workflowId}, replace thereview_cancel_workflow{workflowId}intent withcancel_work{workId}, open a finished review's findings withopen_work{workId}(orreview_open_session), and replace the liveworkflow/<workflowId>value withwork/<workId>. Thereview.workflowsquery is nowreview.runs; it no longer returnsactiveWorkflows, and its runs always carryendedAt. Review completion pushes arework_finishednotifications withworkIdandworkKindinstead ofreview_completedwithworkflowId; a push relay must accept the new kind and fields (the example Firebase relay does).@hansjm10/volt-protocolno longer exports theRpcWorkflow*,RpcReviewWorkflowDescriptor,RpcReviewWorkflowLifecycleStatus,RpcProjectionTruncation, orLiveWorkflowValueSchemaschemas, andReviewWorkflowStartedSchemais nowReviewStartedSchema.SDK callers start and observe reviews through the conversation's
workregistry instead ofHostedConversation.reviewWorkflows;runReviewWorkflowtakes the registry aswork, and its hooks andexecuteReviewWorkflowno longer emit workflow events. -
rpc: The legacy RPC command, response, and event schemas are removed from
@hansjm10/volt-protocoland 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, andUiNode. TheRpcCommand,RpcResponse,RpcServerEvent, andRpcClientMessagedefinitions, the UI action, transcript, session state, and stream frame definitions, and thex-volt-open-eventsnote are gone.x-volt-limitsdrops the session state, transcript, session tree, message image, and error code sections; the remote profile's bounds are underremoteProfile, and the queue depth isconversationInput.queueMaxItems. Intent descriptors carryIntentStateValue,IntentPresentation, andIntentSlashAlias, andintent_completionsreturnsIntentOptions. Intents and queries reject an identifier longer than 256 UTF-8 bytes, a message longer than 512 KiB, and more than 8 images asinvalid_input; a frame whoseintentId,queryId, orsubscriptionIdis longer than 256 UTF-8 bytes ends the connection withfatal{invalid_frame}. An embeddedrunRpcMode(..., { 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 aspush_notification_skipped(waspush_notification_fallback).Migration: import protocol types from
@hansjm10/volt-protocol(ClientFrame,HostFrame,IntentDescriptor,IntentInput,QueryResult,LiveValue) instead of theRpc*andUiAction*types@hansjm10/volt-coding-agentre-exported (RpcCommand,RpcResponse,RpcSessionState,RpcExtensionUIRequest,RpcTranscriptItem,UiActionDescriptor, and the rest); payload types such asRpcGitContext,RpcCatalogModel, andRpcSessionListItemare exported by@hansjm10/volt-protocol. Session event listeners receivefast_mode_changed{enabled}instead ofui_action_state_changed. Subagent handles no longer havegetState()orgetTranscript(): subscribe to the child's conversation, or readhandle.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 rpcstarts withhelloandwelcome; a client subscribes to the conversationwelcomenames (after: "snapshot", or the newest ordinal it saw to resume) and receivessnapshot,entry,head, andliveframes. Commands are intent frames ({type, intentId, input}) answeredacceptedorrejected, with the durable client message id as a prompt's intent id; reads arequeryframes. 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 arenoticeitems. Session changes answeraccepted{conversation}and end the old subscriptionmoved.volt --mode jsonwrites one local-profile subscription's frames (a snapshot, entries, live frames, thenended) 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 (clientFoldin@hansjm10/volt-protocol), the livephase,usage, andgitvalues, and thehistoryandcontentqueries.extension_ui_request/extension_ui_responseandhost_action_request/host_action_responsebecome host requests (host_request/<id>live values answered withhost_response; declare the kinds you answer inhello.accepts.hostRequests).invoke_ui_action,get_ui_actions, andget_commandsbecome intents and theintentsquery;cycle_modelandcycle_thinking_levelbecomeset_modelandset_thinking_levelwith themodelsquery'scycleScope;models_changedbecomeschanged{catalog: "models"};subagent_get_stateandsubagent_get_transcriptbecome a subscription to the subagent's conversation. JSON mode readers takeentryframes for committed messages andliveitems for streaming text. In the SDK,RpcClientbecomesspawnRpcClient,createInProcessRpcClientbecomescreateLoopbackClient, and both return aProtocolClient(intent(),query(),prompt(),waitForIdle(),state,live);RpcTransportClientis no longer exported, so serve other transports withserveConnection(transport, localProfile, { host, conversation }).SubagentManagerno longer takesrequestTimeoutMs, 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 ascompact, 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'sacceptedfirst.The live fold moved to
@hansjm10/volt-protocol: importfoldLiveFrame,foldLiveItems,foldLiveCommit,emptyLiveFold,liveCommitOf,patchLiveValue, andLivePatchErrorfrom 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.createis async, the model, thinking level, and Fast mode come from the log, and queued input survives restarts.
Create sessions withawait AgentSession.create(config); the constructor is private. A configured model or thinking level the branch does not name is committed as amodel_changeorthinking_level_changeentry when the session opens, andsetModel,setThinkingLevel, andsetFastModeEnabledcommit 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 aswithdrawn(clients still receiveclient_input_outcomewith reasonqueue_cleared), and input sent without aclientMessageIdgets 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
SessionManagerwrites go through the session and are refused afterdispose().appendCompaction,branch,resetLeaf, andbranchWithSummaryare refused on a live session; usesession.compact()andsession.navigateTree(). The client-input and delivery writersreserveClientInput,markClientInputQueued,transitionClientInput,rollbackClientInput,commitCanonicalCommand,issueCanonicalProjection,commitDelivery,retainDelivery,terminalizeDelivery,attestDeliveryNoEffect, andverifyDeliveryReceiptare removed; the session records client inputs itself. In agent-core, a next-action policy now seespauseas 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 withsuccess: false. -
sdk: The SDK hosts sessions in a
ConversationHostinstead ofAgentSessionRuntime, and an extension command run through an RPCpromptcan now change sessions. (#585)
Every mode (TUI, print, JSON, stdio RPC, daemon phone streams, subagents) is a client of aConversationHost. The host binds a session's extensions once, in the host'sextensionMode(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 itssession_start, then closes the old one with itssession_shutdown. An extension command that callsctx.newSession(),ctx.fork(), orctx.switchSession()from a stdio RPC or phonepromptno 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 })withnew ConversationHost({ factory, agentDir, extensionMode })andhost.open({ kind: "adopt", sessionManager, cwd }), then attach aHostClient(id,anchor,surface, andmove: { kind: "in_place", prepare?, onMoved }) withhost.attach(client, conversation). Replaceruntime.newSession(),switchSession(),fork(),importFromJsonl(), andexecutePlan()withopenNewSession(host, client, ...),openStoredSession(),openStoredSessionById(),openFork(),openImport(), andexecutePlan(host, client, ...). Move the work ofsetRebindSession,subscribeSessionReplaced, andsetBeforeSessionInvalidateinto the client'sonMovedandprepare; put the client's extension UI, session actions, and error listener in itssurfaceinstead of attaching to each new session yourself. Readruntime.session,services,diagnostics,lost, andconversationProjectionFeedfrom theHostedConversation(projectionFeed), and close it withhost.close(conversation)orhost.dispose()instead ofruntime.dispose().CreateAgentSessionRuntimeFactoryis nowConversationFactory,CreateAgentSessionRuntimeResultisConversationFactoryResult, andAgentSessionRuntimeDiagnosticisAgentSessionDiagnostic.runPrintModeandrunRpcModetake(host, conversation, options)(InteractiveModetakes a connector; see theConversationConnectorentry), and the RPC optiondisposeRuntimeOnCloseis removed.SubagentManager'sonRuntimeCreatedevent carries the child'shostandconversationinstead ofruntime. -
sdk:
InteractiveModeconnects through aConversationConnector; SDK apps start it withInProcessConnector. (#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)withnew InteractiveMode(InProcessConnector.start({ host, conversation, modelScopePatterns }), options), wherehostis aConversationHostandconversationthe conversation it opened;options.modelScopePatternsmoved toInProcessConnector.start.ConversationConnector,ConnectorTarget,ConnectorOpenOptions,OpenedConversation,connectThrough(),InProcessConnector, andInProcessConnectorOptionsare exported. To drive a client yourself,connectThrough(connector, options)returns aProtocolClient(optionfollowMoves: "reconnect") that follows the connector's moves. An in-process connector runs without the Volt daemon: it serves no phones, and/remoteand/worktreereport 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_updatesession events list{ clientMessageId, text }:queueEntryIdis removed, and input sent without aclientMessageIdcarries thelocal-identity the session gave it instead of alocal-queue:identity. The queue limit counts every queued input, extension messages included.SessionManager.getClientInputRecoveryPlan()andgetRecoverableQueuedClientInputs()are removed. UseclientInputRecovery(sessionManager.getConversationState())from@hansjm10/volt-agent-core; itsrecordsare the recoverable queued inputs.A
client_input_outcomesession 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_statusbesidehost_status, andset_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_modelandset_thinking_levelwith 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_browserwithout aredirectUrlstarts the server's configured sign-in instead of failing. Malformed input that the intent schemas catch is rejected asinvalid_input.Migration:
runRpcModeno longer takesrequireRemoteSafeUiActions; serve a paired device's stream withserveIrohRemoteConnection(see the paired-device protocol entry).registerPushTargetnow receives the typedRpcRegisterPushTargetArgs. -
extensions:
ctx.newSession(),ctx.fork(), andctx.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
sessionIdinstead of readingctx.sessionManagerafter the switch. A cancelled result no longer carriesseeded; checkcancelledbefore reading the other fields. A call that no client handles now reports{ cancelled: true }instead of{ cancelled: false, seeded: false }. SDK callers useSessionIntentResultin place of the removedAgentSessionReplacementResult. -
sdk:
SessionManageris now the session catalog and a read view of one session, sessions are written through aSessionWriter, and every session write resolves only after it is saved.
The write methods moved offSessionManager. Before a session opens, write throughsessionManager.logWriter(appendMessage,appendCustomEntry,appendCustomMessageEntry,appendModelChange,appendThinkingLevelChange,appendFastModeChange,appendPlanningState,appendSessionInfo,appendLabelChange,recordStartingGitContext,recordPrReviewBinding, and the branch writesappendCompaction,branch,resetLeaf, andbranchWithSummary). While anAgentSessionis open, write throughsession.sessionWriter; the log writer refuses writes while a session holds the log. Code that runs in either phase takes theSessionWriterinterface, whosesessionManageris the view it writes. The model, thinking level, Fast mode, plan, name, and label writes resolve tovoid; read the entry they appended fromgetLeafId().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()orgetBranch(), and nothing observes an entry before its save commits. A write whose save is rolled back rejects withSessionAtomicAppendErrorand leaves the session unchanged and writable.AgentSession.updatePlan,updatePlanProgress,requestReplan,submitPlan,changePlan,discardPlan,setFastModeEnabled, andsetSessionNameare async too, and the extension API'svolt.appendEntry,volt.setLabel, andvolt.setSessionNamereturn aPromise<void>. To migrate, await each write before reading what it wrote.SessionManager.flush(),materialize(),getIndexedOrdinal(), andAgentSession.flushPlanningState()are removed: await the writes instead, and useclosePersistence()to wait for every write before closing.SessionManager.getCommittedOrdinal()is renamedgetOrdinal().Extension
ctx.newSession({ setup })now passessetupan asyncSessionWriterfor the new session instead of itsSessionManager; read the new session throughwriter.sessionManager.A
SessionManagerholds one session for its whole life:newSession()andcreateBranchedSession()are removed. Create a session withSessionManager.create(cwd, sessionDir, options)orSessionManager.inMemory(cwd, options)(which now takesid,parentSession, andorigin), and copy one branch of a session into a new one withSessionManager.createBranched(source, leafId).buildSessionContext(the function and the method) andSessionContextare removed in favor of the conversation fold.sessionManager.getConversationState()returns the fold of the session's log: itscontextholdsmessages,model,thinkingLevel, andfastMode(now a boolean), and itsplanningisnulluntil the branch commits a plan state. For log entries outside a session, usefoldandbuildContextfrom@hansjm10/volt-agent-core.prepareCompaction(pathEntries, messages, settings, context)now takes the branch's context messages. -
sdk:
AgentSessiondrops the members only the TUI used, andExtensionRunnerandModelCycleResultare no longer exported. (#585)
To migrate:executeBash(command, onChunk, options)becomesrunUserBash(command, { excludeFromContext, operations })(extensions seeuser_bashfirst; output shows as the livebashvalue), andrecordBashResulthas no replacement beyondrunUserBashwith the operations that produce the result.getSteeringMessages(),getFollowUpMessages(), andgetQueuedWorkNotices()are replaced by thequeue_updateevent orclientInputRecovery(sessionManager.getConversationState()).cycleModel()andcycleThinkingLevel()are replaced by themodelsquery'scycleScopeandsetModel(model, { source: "cycle" })orsetThinkingLevel(level);toggleAgentMode()bysetAgentMode(mode).getUserMessagesForForking()andgetLastAssistantText()are replaced by readingsessionManager.getEntries()andmessages.InteractiveMode'soptions.settingsScopesays where the TUI reads its display settings before its client connects. -
sessions: Forked, cloned, and imported sessions now start with a
forked_fromentry 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--sessionor--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-idwith--fork <path>to choose it. The snapshot header's parent locator is no longer restored on import. SDK code reads the lineage withSessionManager.getForkedFrom(), andSessionManager.createBranched(source, null)creates a fork of an empty branch. In@hansjm10/volt-protocol,forked_from.entryIdisnullfor 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 aconversation_lockederror 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 firstSessionManagerbefore callingSessionManager.openagain, or read it with the newSessionManager.openReadOnly. Protocol clients get the intent rejected with reasonlocked, and phones receive theconversation_lockedhandshake 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 rpcreports 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 abortedctx.signalof their commands and then receivesession_shutdownas usual; session writes after the loss throw. SDK code replacesSessionManager.getConversationAuthorityStatus(),subscribeConversationAuthorityChanges(),assertConversationAuthorityAvailable(), andretireConversationAuthority()with thelostpromise onAgentSessionandHostedConversation, and replacesdrainPersistence()withclosePersistence(), which no longer rejects for a lost log.SessionConversationStateUnavailableErrorandSessionAtomicAppendError.authorityare removed. -
settings: Prompt cache retention is now the
promptCache.retentionsetting, and Volt no longer readsVOLT_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=longwith"promptCache": { "retention": "long" }insettings.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
verifiedflag is gone. (#585)
volt storeand/storeshow each catalog package's permissions and review (commit, reviewer, date, and notes), and install and update a catalog package only at its reviewed commit:--refand--trackno longer apply to catalog ids, andvolt store update <id>moves an install that tracks a branch to the catalog's pin.volt store showand install plans show the package's manifest (id, display name, entry, permissions, and settings) instead of the rawvoltfield.Migration: a custom catalog (
VOLT_STORE_CATALOG_URL) moves to"schemaVersion": 2. Dropverified; addversion,permissions, andreview({ "commit", "reviewer", "date": "YYYY-MM-DD", "notes" }) to each entry, withid,name,version, andpermissionsequal to the package's manifestid,displayName,version, andpermissions;repo,author,license,categories, andresourcesare now required. Eachsourcemust begit:https://github.com/<owner>/<repo>@<40-character commit>andreview.commitmust be that commit; npm and local catalog sources are refused. Packages dropimageandvideofrom thevoltfield of package.json, which now holds only the manifest andskills,prompts, andthemes; 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
/extensionsandvolt configedit as a form. (#585)
Migration: after updating the packages, move.volt/azure-devops.jsoninto the azure-devops project settings (/ado-config savewrites the current values there),VOLT_XCODEBUILDMCP_WORKFLOWSinto the build-ios-appsworkflowssetting, andRTK_DISABLED=1into the rtkenabledsetting (off). Settings live insettings.jsonunderextensions.<id>.settings. review-loop gainsmaxLoopsandbaseBranch, and terminal-bench-harbormodel,taskLimit, andconcurrentTrials. azure-devops no longer declares thefs-writepermission. -
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(underVOLT_CODING_AGENT_DIRwhen set). Custom session directories (--session-dir,VOLT_CODING_AGENT_SESSION_DIR, thesessionDirsetting) 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 withvolt -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, andSessionManager.list,search, andfindContinuationis exactly the directory Volt runs in, by its real path: a subdirectory's or a worktree checkout's sessions are listed under All.--session-idwith the ID of another directory's session is refused instead of opening it.SDK:
getDefaultSessionDir(agentDir?)andgetDefaultSessionDirPath(agentDir?)no longer take a cwd and return<agentDir>/sessions;listAll()andsearchAll()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 assubagentwork 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 orresume_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_workdoes. Subagent statuses use work terms:abortedis nowcancelled, and a list showssuspendedandinterruptedruns.Protocol clients start a subagent with
start_subagent{agent, prompt}(answering{workId, conversation}) instead ofsubagent_start, stop it withcancel_work{workId}instead ofsubagent_abort, and follow it through the livework/<workId>value instead ofsubagent/<id>;subagent_disposeis 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 holdsubagent_spawnentries, and@hansjm10/volt-protocolno longer exportsSubagentSpawnEntryPayloadSchemaorLiveSubagentValueSchema.SDK callers: a
SubagentManagerpassed assubagentToolManagerbelongs to that one session.SessionWriter.appendSubagentSpawn,SessionManager.getSubagentSpawnEntries, and thespawnRecordstart option are removed (passtoolCallIdinstead);SubagentActivityStatus,SubagentRegistryStatus,SubagentToolStatus, andSubagentToolOverallStatusare replaced bySubagentRunStatus, andresumeDelegationtakes noallowedTools(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, synchronouspresent({args, argsComplete, state, result, cwd})that returns{title, activity?, summary?, body?, actions?, hidden?, showsDuration?}ofUiNodedata;volt.registerMessagePresenter(customType, present)presents custom messages as{title?, summary?, body}; andvolt.registerWorkKind(name, {..., detail})presents a running item's detail from its title, input, progress, and output (a kind thatrequiresremote 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, andopen_work/cancel_workfor its own work.To migrate, give tools a
presentfunction 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 ownpresent()presents as the built-in, binding only the extension's own actions.volt.registerMessagePresenterthrows for the host's message types (work_notice,review,subagent_recovery,volt-plan-checkpoint,volt-plan-execution), whichvolt.sendMessagealso refuses.ToolExecutionComponentandCustomMessageComponentare no longer exported.Protocol clients render tool calls from
TranscriptItem.presentation(aToolPresentation); a custom message whose type has a presenter carries aMessagePresentation. The per-tool fieldssummary,path,args,details,output,outputTruncated,diffPreview, andpatchPrevieware removed from the transcript view (a result's content is fetched with thecontentquery), and a tool item'stextis now its presentation's title and how the call ended ($ npm test (completed)). The livetoolstart item carries the running call'spresentation, later items carrypatch({summary?, body?}patches of the trees the client holds, at most every 100 ms, so streaming output arrives as appended lines) or a wholepresentation, andpartialcarries onlycontent(partial.detailsand itstoolDetailsMaxSerializedByteslimit 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'detailchanges arrive aspatchitems, and a background job's live value now changes with its output, so clients readwork_outputagain when the value changes instead of polling it. -
tui: The
remote.backgroundsetting is removed: interactive Volt always starts the Volt daemon, which now hosts its conversations. (#585)
Migration: deleteremote.backgroundfrom yoursettings.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, runvolt daemon install-service;volt daemon stopstops it. Print, JSON, and RPC modes still run without the daemon. -
tui:
/workand 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
/workinstead of/jobsand/subagents. Inkeybindings.json, renameapp.jobs.opentoapp.work.open,app.jobs.canceltoapp.work.cancel, andapp.jobs.followtoapp.work.follow;app.work.resume(Ctrl+R) is new, andapp.subagents.open,app.subagents.previous, andapp.subagents.nextare 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_workon a subagent whose conversation closed answers its conversation, and subscribing to a closed subagent conversation your conversation links answers a snapshot of its log, thenended{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 sendwork_noticecustom messages, which are the host's. SDK callers useHostedConversation.isActive()andwaitForIdle()instead ofsession.hasRunningWorkandsession.work.waitForIdle()for retention,WorkRegistry.busy()for running work, andWorkRecord.checkpoints; a kind'smaxActivenow counts suspended work, and an item records at most 256 checkpoints. -
extensions: Volt no longer loads Pi extension packages: the
pipackage.json manifest key and the@earendil-works/pi-*and@mariozechner/pi-*import aliases are removed.
To migrate a Pi extension package, replace itspikey in package.json with avoltmanifest (see the extension manifest entry), import from@hansjm10/volt-ai,@hansjm10/volt-agent-core, and@hansjm10/volt-coding-agentinstead of the Pi package names, and depend on@hansjm10/volt-tuiyourself 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/, andvolt daemon statuslists every worker's pid and log. A worker reloads its conversations' settings when the global or projectsettings.jsonchanges, and its credentials and models whenauth.jsonormodels.jsonchanges, and tells attached clients to refetch them. -
extensions: The
request_user_inputtool's questions and an extension'sctx.ui.setThemenow reach clients through the protocol, so RPC clients and paired phones can answer the tool's questions too. (#585)
The model is offeredrequest_user_inputwhile an attached client acceptsuser_inputhost 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, andctx.ui.setTheme(name)sends local clients aset_themedirective 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(), andctx.switchSession()that a phone invokes on a daemon-hosted session runwithSessiononce the phone reconnects to the new session, and reportseeded: true. (#585) -
review: Review a branch together with its uncommitted changes with
/review branch-uncommitted [base], and in thereviewintent as targetbranch_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/reviewlauncher asks which engine reviews when an extension offers one for the target and adds the engine's options to the options form, and thereviewEnginesetting makes an engine the default. Protocol clients passengineandengineParamsto thereviewintent. -
review:
/reviewstarts 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.
/reviewreads 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 likebashandcompact, so input sent while it summarizes the branch it leaves queues at once. (#585)
set_modeltakessource: "cycle"for a client's model-cycle control, which extensions see onmodel_select, and theconversation_infoquery reports whether the conversation's project is trusted (projectTrusted, local clients only). -
rpc: RPC clients'
!commands now go through extensions'user_bashhooks 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 asmodel,local,remote, orcli. -
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 aGitContextProviderPooltocreateAgentSessionServices. -
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/fastthrough 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./storeinstalls, 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
reviewwith targetprreviews withurl(theurlcompletions offer the current branch's pull request; local clients only), andset_settingsand thesettingsquery carrywarnings. -
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
steerandfollow_upwhile a compaction or a tree navigation runs: the input waits in the queue and is delivered once it ends. Abashintent no longer holds back the intents and queries sent after it on the same connection;bashintents still run one at a time. Theresourcesquery reports keys two extensions bind among its diagnostics. Extension command argument completions useExtensionCompletionItem(labelis 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/resumepicker 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'sctx.switchSession(). Messages sent during/compactwait in the conversation's queue without waiting for the compaction to answer./reloadshows the errors the reloaded setup reports, such as amodels.jsonthe host could not read. A/worktreesession is stored where the session it leaves is, and is bound to its worktree once the TUI moved to it.Protocol clients: a
compactintent runs in the same lane of its own asbash, so the intents and queries sent after it (input,withdraw_queued) no longer wait for the compaction.sessionsitems carry a local-onlysessionDir. An extension command'sctx.fork()sends the invoking local client aset_editor_textdirective with the text it forked before once the client shows the fork, andctx.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-servicekeeps starting afterbrew upgrade noderemoves the previous Homebrew Node version. (#563)
The service now records Homebrew'sopt/<formula>/bin/nodelink instead of the versioned Cellar path. Services installed earlier switch to it at the nextvolt update; to switch now, runvolt daemon install-service. - daemon:
volt updatenow 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; runvolt daemon install-serviceto start it sooner. - daemon:
volt updateno 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'spair_requestnow requiresworkspaceName.volt remote pairand/remotealready 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 addshares it later. A workspace can now be local to the host: paired devices never see or reach it unless their grant names it, andvolt daemon statusmarks 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 installandvolt store updatereview an extension's permissions asvolt installdoes, 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, involt installandvolt updatetoo, 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 installreviews the permissions of a package installed by a path relative to the current directory, andvolt update <source>reviews the package it updated however its source is spelled. (#585)
/storein 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, asvolt store updatedoes. 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 updatecannot restore a native addon now show normal file paths instead of paths prefixed with\\?\. - update:
volt updateno 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)