Repository navigation
Releases: opencode-dotnet/opencode-sdk-dotnet
Release list
0.9.0-preview.13
The SDK speaks opencode 2.0.32. The pinned server moves from 2.0.29 to 2.0.32. The upgrade
brings an experimental voice session, typed reasoning and temperature capabilities on models, and
a background service whose saved env wins after startup. On Linux and macOS, StopAsync now
returns only once the stopped daemon has been reaped. IntelliSense reaches the members that
implement a documented interface. The guides gain a recipe for answering permission asks and
question forms, plus the notes that isolating and cleaning up a background service needs.
✨ Added
- The SDK speaks opencode 2.0.32.
ExperimentalClient.CreateVoiceSessionAsyncopens an
experimental voice session over WebRTC or WebSocket.ConfigModelCapabilities.Reasoningtells a
model that does not reason (ConfigModelReasoningUnsupported) from one that does
(ConfigModelReasoningSupported, with its toggle, efforts andConfigModelReasoningBudget);
ConfigModelCapabilities.TemperatureandModelCapabilities.Temperaturesay whether a model
takes a temperature, andConfigModel.ReleaseDatecarries its release date.
🔧 Changes
- A spawned background service's saved
envwins after startup.
OpenCodeServerEnsureOptions.Environmententries still win over the channel's service-config
envat spawn, but a 2.0.32 service re-applies its savedenvwhen it starts, so a key set in
both takes the saved value for everything the service reads after startup.
🐛 Fixes
- Stopping a background service waits for its child process to be reaped. On Linux and macOS,
OpenCodeServer.StopAsyncno longer returns while the daemon it stopped is still an unreaped
zombie of the calling process. - IntelliSense reaches implementing members and restores declared halves. Properties that
implement a documented interface member (durable envelopes'SeqandVersion, the MCP
timeouts andCodemode, inbox and compactionCreatedtimes) show that member's documentation,
and 26 summaries that preview.12 dropped as partly implementation-only return with the part
upstream declares. SDK types and runtime behavior are unchanged.
📚 Documentation
- Isolating a background service needs separate state and config roots. The connection-modes
guide now says to giveXDG_STATE_HOMEandXDG_CONFIG_HOMEdifferent directories: the
registration and the CLI's service configuration are both namedservice.json, so one folder for
both makes them the same file, andEnsureAsyncthen fails now and then on Windows withEPERM. - A stopped service's files unlock a moment after
StopAsyncon Windows. The connection-modes
guide now says that the daemon'sopencode.db-shmand-walstay locked for a few tens of
milliseconds after the call returns, so deleting an isolated data directory right away can fail. - The streaming guide answers permission asks and question forms. One recipe covers explicit
tool-action rules, one-time approval or rejection, and field-keyed text answers with nullable
union-member handling; the requests guide links to it from session permissions.
0.9.0-preview.12
Streams and automatic traversals take per-call options, and the public surface sheds its last
loose names before 1.0. SubscribeAsync, ReadLogAsync (formerly GetLogAsync),
EnumerateMessagesAsync and EnumerateSessionsAsync accept OpenCodeRequestOptions, so a
per-call Location reaches every connection attempt and page request, and
EventStreamIdleTimeout sets the event bus's idle window. Query parameters take the types the
server reads (bool?, int?, long?), generated enums work in switch and patterns through
their Values constants, assistant content has one union type, PTY sizes share PtySize, and
OpenCodeRoutes is internal. Cancellation reports your own token at every public async door,
disposing the root client interrupts calls and streams with ObjectDisposedException, readiness
timeouts keep an outcome recorded before them, IntelliSense states only what the API declares, and
on .NET Framework and netstandard2.0 response bodies up to 16 MiB are pooled.
💥 Breaking changes
-
Streams and automatic cursor traversals take per-call options before cancellation.
These four virtual method signatures change; recompile consumers and update overrides.
Name a token ascancellationToken: tokenwhen omitting the new options argument.
Calls that omit options keep their behavior. Streams and traversals refuseNoThrow;
the event bus also refuses a per-callNetworkTimeout, includingTimeout.InfiniteTimeSpan.Door Was Now EventsClient.SubscribeAsync(CancellationToken cancellationToken = default)(OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)SessionClient.ReadLogAsync(SessionLogRequest? request = null, CancellationToken cancellationToken = default)(SessionLogRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)SessionClient.EnumerateMessagesAsync(SessionMessageListRequest? request = null, CancellationToken cancellationToken = default)(SessionMessageListRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default)SessionsClient.EnumerateSessionsAsync(SessionListRequest? request = null, CancellationToken cancellationToken = default)(SessionListRequest? request = null, OpenCodeRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) -
Invalid per-call options name
requestOptionsin exceptions. An undefinedErrorBehavior
on a one-shot call now reportsArgumentOutOfRangeException.ParamNameas"requestOptions"
rather than"options", and the new stream and traversal refusals name"requestOptions"too. -
IOpenCodeStreamFailureCauseexposesTag. Every cause in
OpenCodeStreamFailureException.Causesexposes its wire tag without a cast, including an
unknown cause. Custom test implementations must supply the property.IStreamFailureCause
inherits it rather than redeclaring it; concrete causes and their wire representation are unchanged. -
Instruction-entry DTO names retain
Put.
ExperimentalSessionInstructionsEntryRequestis now
ExperimentalSessionInstructionsEntryPutRequest, and
ExperimentalSessionInstructionsEntryResponseis now
ExperimentalSessionInstructionsEntryPutResponse.
ExperimentalClient.SetSessionInstructionsEntryAsynckeeps its method name. -
The instruction-entry list method names its subject in the plural.
ExperimentalClient.ListSessionInstructionsEntryAsyncis now
ListSessionInstructionsEntriesAsync, and
ExperimentalSessionInstructionsEntryListResponse.SessionInstructionsEntryis now
SessionInstructionsEntries. -
The session log uses stream-reading terminology.
SessionClient.GetLogAsyncis nowReadLogAsync; its request and stream item types and
its behavior are unchanged. -
OpenCodeRoutesis no longer public. Call the typed clients to build and send request
paths instead of using route constants or builders directly. Request paths on the wire are
unchanged. -
Assistant content has one union type. Message content and content-updated events use
ISessionMessageAssistantContent; the encoded duplicate and its unknown carrier are removed.
Existing text, reasoning, and tool records implement the surviving interface. The wire is unchanged.Surface Was Now SessionMessageContentUpdatedData.ContentitemISessionMessageAssistantContentEncodedISessionMessageAssistantContentUnknown assistant-content record UnknownSessionMessageAssistantContentEncodedUnknownSessionMessageAssistantContent -
PTY requests share
PtySize. Replace the three old size names withPtySize.
ItsColsandRowsmembers remain requiredlongvalues. The persistent requests remain experimental.
Thepty.updatebody now writescolsbeforerows, which the server does not observe.Property Was Now PtyUpdateRequest.SizePtyUpdateSize?PtySize?PersistentPtyCreateRequest.SizePersistentPtyCreateInputSize?PtySize?PersistentPtyUpdateRequest.SizePersistentPtyUpdateInputSizePtySize -
Boolean query parameters use
bool?.QueryBooleanis removed. Passtrueorfalse,
notQueryBoolean.TrueorQueryBoolean.False; leave the member null to omit it.
The query text remains lowercasetrueorfalse.Property Was Now SessionLogRequest.FollowQueryBoolean?bool?SessionInterruptRequest.ResumeQueryBoolean?bool?ProjectListRequest.ArchivedQueryBoolean?bool?ExperimentalSessionExportRequest.SanitizeQueryBoolean?bool? -
Numeric query parameters are numbers. The API description declares them as strings, but the
server reads each one as a number and upstream's own client takes a number, so the request
records now do too. PassLimit = 50, notLimit = "50". The value still goes out as query text,
and the server still enforces its range.Property Was Now ListRequest.Limit(SessionListRequest,SessionMessageListRequest)string?int?FsFindRequest.Limitstring?int?VcsBranchListRequest.Limitstring?int?ShellOutputRequest.Limitstring?int?ShellOutputRequest.Cursorstring?long?PersistentPtyReadRequest.Linesstring?int?SessionLogRequest.Afterstring?long?SessionDiffRequest.Contextstring?int?VcsDiffRequest.Contextstring?int?ExperimentalSessionStatsRequest.From,.To(milliseconds since the Unix epoch)string?long? -
OpenCodeApiExceptionandOpenCodeServerExceptionare sealed. They can no longer be used
as base classes; their constructors and error data are unchanged. -
SessionParentFilteris a record with value equality. Filters for the same parent compare
equal and have the same hash code, soSessionListRequestrecords built with equal filters
compare equal too.ToString()identifiesRootOnlyorOfand includes the parent id forOf.
✨ Added
-
Per-call
Locationapplies to streams and automatic cursor traversals. Pass
OpenCodeRequestOptionsto override the ambient directory on every stream connection attempt
or every page request. The server still ignores the header on routes that resolve a session's
location or need no location; it does not filter the global event bus. A per-call
NetworkTimeoutbounds only opening the session log, and bounds each page request separately
on a traversal, not the whole walk. -
OpenCodeClientOptions.EventStreamIdleTimeoutsets the event bus's idle window, as
upstream's own client allows. A connection that receives nothing for that long is replaced; the
default stays 45 seconds. Keep it above the server's 15-second keepalive. Any window above zero
and at mostint.MaxValuemilliseconds is accepted, but notTimeout.InfiniteTimeSpan: a bus
that never times out would hang on a half-open connection. The 2-second connect bound and the
1-second delay before a new attempt stay fixed. -
Enum values work in
switchand in patterns. Every generated enum carries its pinned wire
spellings asconst stringmembers of a nestedValuesclass, and aValueproperty that
returns a value's spelling. Match one against the other in acaselabel or a property pattern;
a value a newer server sends matches none of them:switch (mode.Value) { case VcsMode.Values.Committed: /* … */ break; } if (rule is { Effect.Value: PermissionEffect.Values.Allow }) { /* … */ }
Nothing else changes: the static properties,
==,IsKnownand the wire format stay as they
were.
🔧 Changes
- On .NET Framework and netstandard2.0, response bodies between 1 and 16 MiB are pooled instead
of allocated on every call. The SDK then typically keeps one buffer per size class it has used,
at most about 60 MiB after large responses; bodies above 16 MiB still allocate.
🐛 Fixes
- IntelliSense documents declared units, defaults and family roles. Documentation inferred only
from server implementation is removed; SDK types and runtime behavior are unchanged. - Generated XML summaries use grammatical prose. Articles and brand spellings are corrected,
digits stay attached to their words, and members namedValueread "Gets the value." - A background-service start failure no longer carries part of the PTY handoff ticket. The
contender's stderr tail redacts the ticket even where the 8 KiB cut or the end of the pipe splits
it, so no fragment of it reachesOpenCodeServerExceptionmessages. - **Caller cancellation pres...
0.9.0-preview.11
The event bus reconnects the way upstream's own client does, and the calls that answer only when
their work ends now wait for it. EventsClient.SubscribeAsync replaces a dropped, failed or
silent connection one second later, starts every connection with EventServerConnected, and gives
up after one minute in which every attempt failed, so a wrong address or password surfaces after a
minute rather than at once. NetworkTimeout sets how long a call may go without network progress,
and WaitForSessionAsync, RunShellCommandAsync, RunCommandAsync and GenerateTextAsync wait
with no window. The other breaking changes: generated string enums are extensible value types, the
upstream-experimental surface is [Experimental] and needs an opt-in, and OAuth, shellId and
savedPermissionId are spelled as they should be. Events that belong to a session implement
ISessionScopedEvent, the generated XML documentation says what each record is, the guides carry
the fixes from the preview.10 evaluation, and on Windows the background service's sidecar
publishes even while another process reads it.
💥 Breaking changes
-
EventsClient.SubscribeAsyncreconnects, as upstream's client does.- A connection that drops, fails, or receives nothing for 45 seconds is replaced one second
later. Every failure counts: an HTTP error status, a malformed frame, a failure frame and the
server ending the stream included. - Each connection has 2 seconds to answer and deliver its first event, so a server consistently
slower than that fails every attempt. - Each connection starts with
EventServerConnected, and the SDK fails one that does not. A
second one means the events in between were missed. - The stream no longer ends on its own. Your token, disposing the client, or one minute in which
every attempt failed ends it; the minute throws the last failure. - What changes for you: a wrong address or a refused password surfaces after one minute, not at
once, and code that expected the stream to end when the server went away now sees a new
EventServerConnectedinstead.OpenCodeClientOptions.NetworkTimeoutno longer reaches the
bus.
- A connection that drops, fails, or receives nothing for 45 seconds is replaced one second
-
The upstream-experimental surface is marked
[Experimental]. Every member that calls an
operation upstream serves under/api/experimental/, and every type only those operations use,
now reports a diagnostic when you use it. It is a compile error until you opt in with
#pragma warning disableor<NoWarn>for its id:OPENCODE001:client.Experimental(ExperimentalClient);OPENCODE002: the persistent-PTY family (client.PersistentPtys, its handle, connect door,
session, options and frames);OPENCODE003: the Session family's experimental operations, todaySessionClient.GetLogAsync.
Nothing on the wire changes. See Experimental APIs. Waiting for a
turn on the event bus, as the getting-started guide shows, needs no opt-in. -
Every generated name spells
OAuthas the brand does. Names built from operation ids and wire
members saidOauth, beside the schema-derivedCredentialOAuthandMcpOAuthConfig. They are now
BeginOAuthConnectionAsync,CancelOAuthConnectionAsync,CompleteOAuthConnectionAsync,
GetOAuthStatusAsync, theIntegrationOAuth*request and response types with their
OAuthConnectandOAuthStatusmembers,McpRemoteConfigOAuth(with itsKind), the
OpenCodeRoutesbuilders and templates, and theOAuthvalue of
ConnectionCredentialInfoMethod. Rename the uses; nothing on the wire changes. -
The shell and saved-permission ids have descriptive parameter names.
ShellsClient.GetShellClient, and theOpenCodeRoutes.Shellsbuilders, takeshellId.PermissionsClient.RemoveSavedAsyncandOpenCodeRoutes.Permissions.RemoveSavedtake
savedPermissionId.- Before, both were a bare
id. Only a call that named the argument (id: …) changes.
-
Generated string enums are extensible value types (ADR-0035).
-
Every enum the API defines, such as
VcsMode,PermissionEffect,SessionInboxDeliveryand
AgentInfoMode, is now areadonly struct, not a C#enum. -
Previously a server newer than the SDK broke reading whenever it sent a value the SDK did not
know, on a response or on an event stream frame. That value now reads as it was sent, and it is
written back unchanged. -
The pinned values are static properties with the same names, so
VcsMode.Committedstill
compiles. -
IsKnownreports whether this SDK's pin defines a value.new VcsMode("…"), the implicit
conversion fromstring, andFromStringcreate a value from any spelling. -
Equality is ordinal.
-
ToString()now returns the wire spelling (running), not the C# member name (Running).
Logs and string comparisons built on it change. -
A
defaultvalue has no spelling. A request body or query refuses it. -
What breaks: enum values used as constant patterns, and code that relied on
enumbehaviour
(numeric casts,Enum.GetValues,[Flags]). Compare with==instead:// Before switch (mode) { case VcsMode.Committed: /* … */ break; } if (rule is { Effect: PermissionEffect.Allow }) { /* … */ } // After if (mode == VcsMode.Committed) { /* … */ } if (rule.Effect == PermissionEffect.Allow) { /* … */ }
-
✨ Added
-
ISessionScopedEventroutes an event by its session. Every event whose payload names the
session it happened in (58 at the pin: thesession.*events,permission.asked/replied,
persistent-pty.added/removedandtui.session.select) implements it, so
e is ISessionScopedEvent { SessionId: var id }replaces a switch over every type. The member is
implemented explicitly, so the events gain no public member and serialize as before. The form
events are not session-scoped: a form's session id can be"global". -
NetworkTimeoutsets how long a call may go without network progress.
OpenCodeClientOptions.NetworkTimeoutsets it for the client and
OpenCodeRequestOptions.NetworkTimeoutfor one call; the default stays 100 seconds.
WaitForSessionAsync,RunShellCommandAsync,RunCommandAsyncand bothGenerateTextAsync
methods (onSessionClientandExperimentalClient) answer only when their work ends, so they
now wait with no window and the call's cancellation token is their deadline; before, each failed
after 100 seconds without headers, and a shell command kept running on the server. A client-wide
value does not reach them; a per-call value does.OpenCodeServer.CreateClientcarries the
client value over. See Requests.
🔧 Changes
- Generated XML documentation says what each record is. The pinned API describes almost none of
its schemas, so summaries now come from the wire: an event record names its event type, its
payload names the event it belongs to, an operation's body, response and query records name the
operation with its method and route, and an error record names its tag. The types builders found
hardest carry hand-written summaries from upstream's behaviour: assistant messages and their
finish, error and retry; text deltas and their end; the prompt and create requests; the execution
events; and a PTY's command and arguments, including which shell starts when the command is
absent. - The package README links open the docs of the release they came with. On NuGet.org, the
links to the guides point at the commit the package was built from, not atmaster. - The hosting internals join the flat
OpenCode.Sdk.Internalnamespace. The public API is
unchanged, byte for byte; only internal type names differ, as a stack trace or reflection shows
them.
🐛 Fixes
- An event stream no longer ends on an enum value the SDK does not know. Before, a frame with
such a value, such as a newreasononsession.execution.interrupted, failed to materialize and
ended the subscription withOpenCodeTransportException. The frame now reads as its typed event,
with the value kept (IsKnownis false). One-shot responses read the same way. See the Breaking
entry on extensible enums. - On Windows, publishing the background service's sidecar no longer fails while another process
reads it. The sidecar was replaced with a rename that Windows refuses with access denied while a
reader holds the file open. It is now replaced with a POSIX-semantics rename, which succeeds even
then, and falls back to the previous rename where the file system does not support it.
📚 Documentation
- The streaming guide streams an answer as it arrives. The recipe follows upstream's own client:
- wait for
EventServerConnectedbefore you prompt; - prompt with your own
msg_id, and match it toSessionInboxDelivered'sInboxId; - append
SessionTextDeltatext, and replace it withSessionTextEnded's finalText; - stop on the session's execution event.
- wait for
- The requests guide names the session-create location.
SessionCreateRequest.Locationis a
bodyLocationPublicRef, not theLocationSelectorthat headers and queries use. The guide also
shows that a collection expression cannot fill anOptionalcollection member (CS9174). - The terminals guide documents the two normal-PTY gaps. It says which shell a PTY gets without a
Command: the configuredshell, then$SHELL, then a per-OS fallback. It also shows how to
resize a normal PTY, throughPtyClient.UpdateAsyncwithSize. - The connection guide keeps a launched server around a host, and fixes the 401.
- A launched server stays in
await usingaround the host'sRunAsync. The co...
- A launched server stays in
0.9.0-preview.10
The packages target netstandard2.0, net472, net10.0, and net11.0: net8.0 and net9.0
leave ahead of their end of support, and net11.0 joins on its go-live release candidate. A
project on .NET 8 or 9 still installs the packages through the netstandard2.0 asset, now with a
build warning. The pin moves to upstream release tag v2.0.29, which adds provider compatibility
defaults, request-body encoding and top-level config policies. OpenCodeServer.Exited reports how
a started server ended, and on Windows a server's exit is observed on the operating system's own
wait thread, so a busy host no longer reports a crashed start as a closed stdout. The breaking
changes come first: besides the target frameworks, OpenCodeServerOptions refuses an out-of-range
timeout when it is set, IOpenCodeClientOptions is removed,
OpenCodeStreamFailureException.Cause is now Causes, and PersistentPtyReplayBounds takes
camelCase constructor parameters.
💥 Breaking changes
- The packages target
netstandard2.0,net472,net10.0, andnet11.0.net8.0and
net9.0leave ahead of their end of support on 2026-11-10, andnet11.0joins on its go-live
release candidate.0.9.0-preview.9is the last version withnet8.0andnet9.0assets. A
net8.0ornet9.0project still installs the packages through thenetstandard2.0asset, now
with a build warning, because that runtime is not one the packages are tested on; set
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>to silence it, or move
the project tonet10.0or later. A project that treats warnings as errors fails its build on it
until it does one or the other. OpenCodeServerOptionsrefuses an out-of-range timeout when it is set.ReadinessTimeout
takes a value above zero and at mostint.MaxValuemilliseconds, or
Timeout.InfiniteTimeSpanto wait until the start's cancellation token fires;
GracefulShutdownTimeouttakes zero toint.MaxValuemilliseconds. A value outside that range
throwsArgumentOutOfRangeExceptionfrom the setter. A zero or negative readiness timeout and a
negative grace were refused byOpenCodeServer.StartAsyncbefore; a value past the ceiling was
accepted and failed only after the server had been spawned.IOpenCodeClientOptionsis removed. No public API accepted it, and every option added
later would have broken its implementers, becausenet472andnetstandard2.0have no default
interface members. Configure clients throughOpenCodeClientOptions, which a client still
snapshots at construction. Code that declared a variable or parameter as
IOpenCodeClientOptionsusesOpenCodeClientOptionsinstead.OpenCodeStreamFailureException.Causeis nowCauses. It is a list of causes, so the name
is now plural. The constructor parameter is nowcausesas well. Readfailure.Causeswhere
you readfailure.Cause, and passcauses:where you named the argument.PersistentPtyReplayBoundstakes camelCase constructor parameters. They are
requestedOffset,availableOffset,endOffsetandtruncated, like the other frame types'
constructors. The properties keep their names, and positional construction,withand
deconstruction are unchanged. Only a call that named an argument (RequestedOffset: 0) changes.
✨ Added
OpenCodeServer.Exitedreports how a started server ended. The task completes with an
OpenCodeServerExit. It holds an exit code (on Windows the full 32-bit code), a terminating
signal by name and number, orUnknownwhen the exit status could not be read, for example
because something else reaped the server. It completes whether the server ended on its own or
disposal ended it, once its output has ended or one second after the exit, as upstream's
exiteddoes. It never faults. On a handle from
DiscoverAsyncorEnsureAsyncit throwsInvalidOperationException, because that handle owns
no process.- The accepted snapshot moved to upstream release tag
v2.0.29
(042d0832f5f3a73eaa039a58a7d565776f6d4290), published as@opencode/cli@2.0.29; install it
withnpm install -g @opencode/cli@2.0.29. It adds and changes no operation, so all 142 remain
callable. - Provider compatibility defaults.
ProviderInfo.Compatibilityand
ConfigProvider.Compatibilitycarry aProviderCompatibility, the defaults a provider's models
inherit:ReasoningField,RequireReasoning,MaxTokensField,RequireFinishReason,
RequireAssistantAfterTool,SupportsPromptCacheKey, andCacheFormat. - Prompt-cache marker format.
ModelCompatibility.CacheFormatand
ProviderCompatibility.CacheFormatname the marker syntax the endpoint accepts, as the new
ModelCacheFormatenum (Anthropic,Alibaba). - Request-body encoding.
ProviderSettings.Encoding,ConfigProviderSettings.Encoding,
ModelSettings.Encoding, andConfigModelSettings.Encodingname the compression upstream applies
to the provider request body, as the newProviderEncodingenum (Zstd,Gzip,None). - Top-level config policies.
ConfigInfo.Policiescarries the ordered policies that control
access to providers, tools, and integrations, as the newConfigInfoPolicies(Action,
Resource,Effect) with the newConfigInfoPoliciesAction(ProviderUse,ToolUse,
IntegrationUse) andConfigInfoPoliciesEffect(Allow,Deny) enums. Upstream deprecates
experimental.policies: a 2.0.29 server reports those policies here, and
ConfigInfoExperimental.Policiesstaysnull.
🔧 Changes
-
The packages depend on the .NET 10.0.13 servicing builds of their dependencies.
OpenCodeDotNet.SdkrequiresSystem.Text.Json,Microsoft.Bcl.AsyncInterfaces,
Microsoft.Bcl.Memory,Microsoft.Bcl.TimeProvider,System.Collections.Immutable, and
System.Threading.Channels10.0.13 onnetstandard2.0andnet472, up from 10.0.11; its
net10.0andnet11.0assets declare no dependency.OpenCodeDotNet.Sdk.Extensionsrequires
Microsoft.Extensions.DependencyInjection.Abstractionsand
Microsoft.Extensions.Options.ConfigurationExtensions10.0.13 on every target, up from 10.0.10.
No security advisory covers the versions they replace. A project that references one of these
packages directly at a lower version must raise it, or restore reports a package downgrade
(NU1605, an error by default). -
A server that does not report readiness in time fails with the timeout as the inner
exception. TheOpenCodeServerExceptionthatOpenCodeServer.StartAsyncthrows then carries
the readiness bound'sOperationCanceledExceptionas itsInnerException, which wasnull. -
A settings
encodingmember is read as the typedEncoding. OnProviderSettings,
ConfigProviderSettings,ModelSettings, andConfigModelSettingsit is no longer in
AdditionalProperties, where0.9.0-preview.9kept it as an undeclared member. -
The source is laid out in feature folders, and the internal namespaces are flattened into
OpenCode.Sdk.Internal. The public API is unchanged, byte for byte. Only the names of internal
types differ, as a stack trace or reflection shows them.
🐛 Fixes
- On a busy Windows host, a server's exit is no longer reported late. The launcher learned of a
Windows server's exit through a callback queued to the .NET thread pool, so under load it could
arrive seconds after the server had exited. A server that exited before readiness then failed
its start as one that closed its stdout, without its exit code;OpenCodeServer.Exited
completed late; and ending a server that had already exited could still end the descendants it
left running. The exit is now observed on the operating system's own wait thread, as libuv
observes it, so each of these reads the exit as soon as the kernel reports it. OpenCodeServer.EnsureAsyncstarted through a.cmdor.batshim on Windows opens no
console window. With a batch shim such as npm'sopencode.cmdas the command, the background
service took a new console of its own, with a visible terminal window that took the focus and
stayed open for the service's life, and its startup output went to that window instead of to
the SDK. The shim now starts with a console that has no window, which the service inherits, so
its window stays hidden and its startup output reaches the SDK again. A service started from an
.exestill starts detached, with no console.- On Unix under Mono, a failed
chmodof an owner-only file fails its write. The
netstandard2.0andnet472assets set mode0600on the files discovery andEnsureAsync
write through achmodchild process after the create. That process now receives a quoted path,
and a nonzero exit removes the file and fails the create, as the modern targets' atomic
owner-only create does. Behaviour change: such a file was kept at the process umask before.
📋 Important Notes
- This release is built on the .NET 11 release-candidate SDK. A preview package may build on a
go-live release candidate (ADR-0002); the first non-preview package waits for the GA SDK. The
net11.0asset needs the .NET 11 runtime, and anet10.0app keeps using thenet10.0asset. - From
1.0.0, a minor version may move the protocol pin and change the generated surface the
way upstream changed it, each change listed under protocol changes; only a major version breaks
the hand-written design (ADR-0034).
0.9.0-preview.9
The pin moves to upstream release tag v2.0.27, and all 142 operations it exposes are
callable — 140 as generated HTTP calls, and the two terminal WebSocket connections through
hand-written transports. Upstream 2.0.27 moves to Effect 4.0.1, which renames the stream failure
frame, and project.list takes an archived filter, which makes its route helper a method; the
two breaking changes come first. A project records when it was archived, an idle message carries
the error of a failed turn, fs.list answers a missing directory with a typed
FileNotFoundError, and the background-service config reader validates remoteAccepted as the
CLI does. Discovery reads a refused connection as no service even when its bound expires at the
same moment. net8.0 and net9.0 still leave in an upcoming preview; see the important notes.
💥 Breaking changes
- A mid-stream failure is recognized under upstream 2.0.27's frame name. Upstream 2.0.27 moves
to Effect 4.0.1, which renames the reserved failure frame fromeffect/httpapi/stream/failureto
effect/http-api/stream/failure. Against a 2.0.27 server a failure onEventsClientor the
session log surfaces asOpenCodeStreamFailureExceptionwith its typed causes; against an older
server it surfaces asOpenCodeTransportExceptionnaming the undeclared frame. On
0.9.0-preview.8it was the other way round. OpenCodeRoutes.Projects.ListProjectsis now a method.project.listtakes an optional
archivedquery, so the route helper isListProjects(ProjectListRequest? request = null)and
the template constant isListProjectsTemplate.ProjectsClient.ListProjectsAsyncgains a
leading optionalProjectListRequest? requestparameter: a call that passesrequestOptions
positionally moves it one place, and the change needs a recompile.
✨ Added
- The accepted snapshot moved to upstream release tag
v2.0.27
(1f4d07a4a519185955ae8ab2ea3607805509e386), published as@opencode/cli@2.0.27; install it
withnpm install -g @opencode/cli@2.0.27. - Archived projects. Upstream 2.0.27 archives a project whose known directories are all gone
and unarchives it when it is resolved again.ProjectTime.Archivedcarries when it was
archived, andproject.listleaves archived projects out unless
ProjectListRequest.ArchivedisQueryBoolean.True. SessionMessageIdle.Errorcarries theSessionStructuredErrorof a turn that went idle with
a failed outcome.
🔧 Changes
- The
netstandard2.0andnet472assets depend onMicrosoft.Bcl.TimeProvider. The request
progress window and the standalone launcher's output drain now run onTimeProvider, which
net8.0and later carry inbox. Behaviour does not change: both still run on the system clock. - The background-service config reader validates
remoteAcceptedas upstream 2.0.27 decodes
it. The CLI records an accepted remote access as the literaltrue; a document whose
remoteAcceptedis anything else is no config at all, so itsenvoverlay and the legacy
migration it gates do not apply, as in the CLI. - A 2.0.27 server that stops or fails while a terminal is connected closes it with 1001 or
1011, which reads as an abnormal close. When the server stops, or a handler fails, before it
queues its own close on apty.connector persistent-terminal connection, it now closes with
1001 or 1011, whichPtySessionandPersistentPtySessionreport as an abnormal close
(OpenCodeTransportException), as upstream's own terminal clients do. A 2.0.26 server sent a
close frame with no status there, which read as a normal end. FileSystemClient.ListEntriesAsyncreports a missing directory as the typedFileNotFoundError,
which upstream 2.0.27 now answers, and declares onfs.list's 404 besideLocationNotFoundError;
an older server answered an undeclared server error.
🐛 Fixes
OpenCodeServer.DiscoverAsyncandEnsureAsyncread a refused connection as no service even
when the probe's two-second bound expires at the same moment. On a loaded machine
the bound could expire after the runtime had already seen the daemon's port refuse the connect,
and the probe then reported a timeout for a daemon that was gone. A refused connection now decides
whatever the bound says; any other failure still counts as a timeout exactly when the bound
expired. Behaviour change: fewer spurious timeouts under load, so Ensure less often counts a dead
daemon toward the three timeouts that end a registered service, and starts a contender after the
spawn delay instead.
📋 Important Notes
net8.0andnet9.0leave the packages, andnet11.0joins, in an upcoming preview, ahead
of their end of support on 2026-11-10; this release still ships them. The packages will then
targetnetstandard2.0;net472;net10.0;net11.0. An app on .NET 8 or 9 still installs through the
netstandard2.0asset, but that asset is a compatibility asset, not a supported runtime: support
and testing cover the targeted runtimes only, and .NET 5–7 are not supported. Plan the move to
.NET 10.- Upstream's committed OpenAPI document matches its own generator at
v2.0.27. The stale
copy that0.9.0-preview.8noted is current again, so a diff against it no longer shows
operations or errors this SDK has and that document lacks.
0.9.0-preview.8
The pin moves to upstream release tag v2.0.26, and all 142 operations it exposes are
callable — 140 as generated HTTP calls, and the two terminal WebSocket connections through
hand-written transports. An integration can connect through an external credential source, and
a 2.0.26 server's external credentials now decode; the experimental policy action permission is
renamed tool.use, the one breaking change, which comes first. On Windows a standalone server now
runs as upstream's launcher runs it: created suspended in a kill-on-close job, so it ends with its
owner however the owner ends; given its three standard handles and nothing else; read through
overlapped pipes; and ended at once with taskkill /T /F. net8.0 and net9.0 still leave in an
upcoming preview; see the important notes.
💥 Breaking changes
ConfigInfoExperimentalPoliciesAction.Permissionis nowToolUse. Upstream 2.0.26 renames
the experimental policy actionpermissiontotool.useand addsintegration.use, which gates
MCP servers and skills, so the enum's members areProviderUse,ToolUse, andIntegrationUse.
A 2.0.26 server drops apermissionpolicy entry when it loads its config; against an older
server whose config still has one, reading the config fails to decode.
✨ Added
- The accepted snapshot moved to upstream release tag
v2.0.26
(9b4ec5714d481559990db0a816d5dec19541a814), published as@opencode/cli@2.0.26; install it
withnpm install -g @opencode/cli@2.0.26. - An integration can be connected through an external credential source.
IntegrationClient.ConnectWithExternalCredentialsAsync(request)(integration.connect.external)
runs an external authentication method, named byMethodIdwith an optional formAnswerand
Label, and stores a reference to its credential source. The integration lists such a method
asIntegrationExternalMethod, and the stored credential reads asCredentialExternal
(MethodId,Metadata). The route helper is
OpenCodeRoutes.Integrations.ConnectWithExternalCredentials. Behaviour change: on
0.9.0-preview.7a 2.0.26 server'sexternalmethod surfaced asUnknownIntegrationMethod, and
itsexternalcredential value asUnknownCredentialValue. ConnectionCredentialInfoMethod.Externalnames a credential stored through an external
method. Before, a server that returned one failed the decode of its connection.ModelCompatibility.SupportsThinkingBlockBindingandSupportsEffortUpdatescarry the two
compatibility flags upstream 2.0.26 adds.
🔧 Changes
- On Windows a standalone server is placed in a job that ends it with its owner, however the owner
ends.StartAsynccreates the server suspended, places it in a process-wide job that ends it
when the host process exits, and only then lets it run, as upstream's launcher places its server
in a job. Behaviour change: a server that ignores the end-of-stream of its stdin no longer
outlives an owner that crashed or was killed before disposing it. Descendants the server starts
detached stay outside the job and keep running; the other children of a server built on Bun or
Node end with it, because its runtime places them in a job of its own under the launcher's. The
job also ends a server that crashes on an unhandled exception at once, without a Windows Error
Reporting dialog, as upstream's job does, and any window the server opens starts hidden. A server
the kernel refuses to place in the job withERROR_ACCESS_DENIED, as under a host job that cannot
nest, runs outside it, as upstream's does, and then ends with a crashed owner only through its
stdin lease. Any other refusal, a job that cannot be created, and a server that cannot be resumed
fail the start withOpenCodeServerExceptionand leave nothing running. - With the default npm install, pass the path of
opencode.exeinCommandfor that guarantee.
The bareopencoderesolves to npm'sopencode.cmdshim, which runs throughcmd.exe: the job
then holdscmd.exe, and the real server,cmd.exe's child, ends with a crashed owner only
through its stdin lease, as upstream's own launcher of a.cmdfile arranges it. - Disposing a standalone server on Windows ends the server and its whole process tree at once
withtaskkill /T /F, as upstream does, instead of closing its stdin and waiting. Behaviour
changes:- The server no longer runs its own graceful shutdown on disposal: it exits with code 1, where
the stdin end-of-stream used to end it with code 0. Stdin now closes last, as on Linux and
macOS, so anOutputcollector no longer receives the lines a server writes when its stdin
ends. - Every live descendant of the server is ended at every close, detached ones included; the
descendants used to survive a normal close. - Disposal takes the
taskkillrun, about 300 ms, where a server that left on its stdin's
end-of-stream used to take about 10 ms. GracefulShutdownTimeoutnow starts with that first kill and bounds it: when the server is
still running at its end, the same kill runs once more, andTerminateProcessends the server
with code 1 when a kill fails. A disposal takes at most the grace plus 26 seconds, the output
drain and the release of the output readers included.- On
net8.0and later the forced end istaskkill, exit code 1, where it was the runtime's tree
kill, exit code -1. - A server that already exited is not tree-killed, and a failed start ends the server the same
way, with the configured grace, so a canceled or timed-outStartAsynccan take up to the
grace plus 26 seconds to throw. - On
net8.0and later the descendants of a server that exits before readiness are no longer
ended: the runtime's tree kill used to end them, buttaskkillreaches no descendant of an
exited root, so it does not run.
- The server no longer runs its own graceful shutdown on disposal: it exits with code 1, where
- On Windows the server no longer inherits the host's own inheritable handles. It receives its
three standard handles and nothing else, so a parent process that captures the host's output
sees it end when the host exits, even while a server it started still runs. Behaviour change:
the server also no longer receives the handles the host inherited from its own ancestors. - On Windows the server's output is read without a dedicated thread, on every target. Its
stdout and stderr are overlapped pipes whose reads complete on the I/O completion port, on .NET
Framework too; the two reader threads each server used to hold are gone. StartAsyncon Windows fails at once when the server closes its stdout before readiness and
keeps running, as on Linux and macOS, where it used to wait for the readiness timeout. A
readiness line that arrives together with the exit now always starts the server, where the exit
could win before. A server that exits before readiness while a process it started holds its
stdout fails the start after a drain of at most one second instead of two.- A negative Windows exit code is reported with its hexadecimal form. A server that crashes
before readiness is reported asexited with code -1073741819 (0xC0000005), where the message
used to give only the decimal value. - A whitespace-only
WorkingDirectorycounts as none on Windows too. The server then starts in
the host's directory, as on Linux and macOS, where the blank value used to be passed to the
process start as the server's directory. OPENCODE_PRINT_LOGS=1in the host's environment hands the server the host's own stderr on
Windows too, as upstream's launcher does; no stderr is then collected or quoted in a startup
failure. Diagnostics-mode behaviour, as upstream's: the server then also shares the host's
console, so a Ctrl+C in that console reaches the server, even when the host handles it.- A NUL in a command entry or an environment entry is refused with
ArgumentExceptionon Windows
too, before anything is spawned, and a spawn that fails in aWorkingDirectorynames the
directory. StopAsyncand the Ensure replacement act on the service registered when they stop it. Like
upstream's client, they read the registration again before preparing or clearing the terminal
handoff, and end the service that file names then. Behaviour change: a service that registered
whileStopAsyncwas asking the old one to shut its terminals down is the one stopped, where it
used to be left running.- The service config's
remotekey is read as upstream reads it. Remote access is set only
through the CLI (opencode service set remote true, oropencode pair --remote); the SDK reads
the key and never writes it. While the service's tunnel is attached, its remote URL is listed in
ServerInfo.Urls. Behaviour change: aremotethat is neither an object holding a string
routenor the legacy boolean makes the whole document no config, as upstream decides, so its
envoverlay no longer applies, and a document with a legacy boolean still reads but is no
longer migrated to a named channel's file;0.9.0-preview.7ignoredremotein both cases. - A PTY connect refused with HTTP 401 or 403 names the credential or the origin. Upstream
2.0.26 checks the request's origin on every connect, before it looks the PTY up. Behaviour
change: theOpenCodeTransportExceptionmessage now ends "the request's credential or origin
was rejected", as the persistent PTY's message already did, and a refused origin is reported as
such even for a PTY that does not exist. FileSystem.ReadFileAsyncwith a path that leaves the location answers the declared 404 with a
typedFileNotFoundError. Upstream 2.0.26 refuses such a path, and one it cannot decode, as a
missing file. Behaviour change: the status is 404, not an undeclared 5xx, andErroris a
FileNotFoundError.- **A server bound to every interface lists it...
0.9.0-preview.7
The pin moves to upstream release tag v2.0.23, and all 141 operations it exposes are
callable — 139 as generated HTTP calls, and the two terminal WebSocket connections through
hand-written transports. A repository can now be initialized, a location whose directory does
not exist answers a typed 404, and the server says whether it can run persistent terminals.
2.0.23 tags every error, so WorktreeError changes shape and an error body marked only by name
no longer decodes, and EnsureAsync no longer replaces a service whose health protocol is
incompatible; the breaking changes come first. On Linux and macOS a standalone server now runs in
a session of its own and is ended as upstream's launcher ends it, a SIGTERM to its process group
before the SIGKILL; Windows keeps its earlier path. The background-service doors follow the
2.0.23 client, the generated ToString() masks every configured header and environment map, the
open provider options, and unrecognized union values that can carry a secret, and both packages
declare IsAotCompatible on net8.0 and net9.0 too. net8.0 and net9.0 still leave in an
upcoming preview; see the important notes.
💥 Breaking changes
WorktreeErroris a tagged error. Upstream 2.0.23 declares it with_tagbeside itsname
anddata, soTagreads_tagand a new requiredNamecarriesname. A body without_tag,
from a server older than 2.0.23, no longer decodes to a typed error:Erroris null and
RawBodykeeps the body._tagis the only error marker. Every error the 2.0.23 contract declares is tagged, so a
nameis no longer read as an error's marker:- Any error body whose only marker is
name, not just a worktree failure, no longer decodes to
UnknownOpenCodeError:Erroris null andRawBodykeeps the body. - The public
UnknownOpenCodeError(tag, payload)constructor throwsArgumentExceptionfor a
payload whose only marker isname; the payload must carry the tag under_tag.
- Any error body whose only marker is
OpenCodeServer.EnsureAsyncthrowsOpenCodeServerExceptionon a service whose health
protocol is incompatible, where it used to replace it. A registered daemon that answers
/api/infowith an authenticated 404 is left running and the call throws, unless the call's
version requirement is unmet, which still replaces it. Stop it explicitly withStopAsyncto
replace it.OpenCodeServer.StartAsyncrefuses POSIX systems other than Linux and macOS, FreeBSD among
them, withOpenCodeServerExceptionbefore anything is spawned; they used to start through
Process. On Linux and macOS a NUL in a command entry or an environment entry is now refused
withArgumentException, because the C string it is passed as would end there.
✨ Added
- The accepted snapshot moved to upstream release tag
v2.0.23
(0fd7e2829449b052abf0078666669302923d77af), published as@opencode/cli@2.0.23; install it
withnpm install -g @opencode/cli@2.0.23. - A repository can be initialized in a project that has none.
client.Vcs.InitializeRepositoryAsync(request)(vcs.init) takes an optionalProvider(git by
default); a provider without initialization answers 501VcsInitNotSupportedError. The route
helper isOpenCodeRoutes.Vcs.InitializeRepository. - A missing directory is a typed 404. Every location- and session-scoped route, and
experimental.generate.text,session.diff, andsession.permission.listby name, now declare
LocationNotFoundError(Location,Message) for a location whose directory does not exist;
before 2.0.23 the server failed such a request instead of answering it. A PTY connect refused
with HTTP 404 carries no error body, so itsOpenCodeTransportExceptionnow names both causes:
the PTY session or the requested location does not exist. ServerInfo.Capabilities.PersistentPtysays whether the server can run the
persistent-terminal daemon:falseon Windows, whereopencode-ptyships no binaries. A server
older than 2.0.23 leavesCapabilitiesnull.
🔧 Changes
- On Linux and macOS a standalone server runs in a session of its own, so a Ctrl+C or a hangup
sent to the host's terminal no longer reaches it. Behaviour change: the server used to stay in
the host's process group, so a Ctrl+C the host itself handled still ended the server. It now
ends throughDisposeAsync, or, when the host dies without disposing, on the end-of-stream of
its stdin lease. - Disposing a standalone server on Linux and macOS sends
SIGTERMto the server's process group,
andSIGKILLto whatever is left of the group afterGracefulShutdownTimeout, as upstream's own
launcher does. Behaviour changes:- The pinned server answers the
SIGTERMby exiting with code 130, where the stdin end-of-stream
used to end it with code 0. Stdin now closes last. - Children the server started in its own process group end with it. Descendants that moved into a
session of their own (the persistent-terminal daemon, PTY shells) are left to the server, as
upstream leaves them; the forced tree kill used to end them too. - A server that already exited on its own with a non-zero code has the survivors of its group
ended, and so does one whose exit status nobody could read (the host ignoresSIGCHLDon
Linux, or runs as pid 1); one that exited with code 0 or on a signal leaves them alone, as
before.
- The pinned server answers the
- A failed
StartAsyncon Linux and macOS ends the server the same way, with the configured
grace, so a canceled or timed-out start can take up to that grace plus 16 seconds to throw: at
most 10 for the server's exit afterSIGKILL, 1 for draining its output, and 5 for releasing the
output readers. Behaviour change: it used to kill the server at once. StartAsyncon Linux and macOS sees the server's own exit without waiting for its output, and
fails at once when the server closes its stdout before readiness. A server that exits before
readiness while a process it started still holds its stdout now fails the start within about a
second instead of at the readiness timeout. Behaviour change in the message: a server ended by a
signal before readiness reads "terminated on signal N", where it read "exited with code 128+N".OpenCodeServerOptions.WorkingDirectoryworks on Linux with a C library older than glibc 2.29
(RHEL 8, Debian 10, Ubuntu 18.04): the server starts there through/usr/bin/env -C, which keeps
it the same process, and a directory that does not exist fails the start as an exit with code
125 whose stderr names it. A blankWorkingDirectorycounts as none.- A standalone server's relative command path on Linux and macOS is resolved against the
caller's current directory, never againstWorkingDirectory. Behaviour change:Process
tried it against the application's own directory first, so a relative path that named a file
there started that file even when the current directory held another. OPENCODE_PRINT_LOGS=1in the host's environment hands the server the host's own stderr on
Linux and macOS, as upstream's launcher does; no stderr is then collected or quoted in a startup
failure.EnsureAsyncreports the first contender failure without recruiting replacements. While a
failed contender's error stands, no further contender starts; the call throws it once no contender
is live, and at the 120-second bound it throws that failure rather than the timeout. A recovery or
replacement that ends the registered service clears it and releases the contenders it made moot.
Behaviour change: a call that a recruited replacement contender used to rescue now throws the
first contender's failure.- Persistent-terminal handoff and the sidecar clear are best-effort. Preparing the handoff for
a replacement never fails it: a failed or unusable ticket shuts the daemon's terminals down and
publishes no ticket, and a sidecar that cannot be written leaves the replacement without one.
StopAsyncand the Ensure replacement end the service even when the sidecar cannot be removed.
Behaviour change:StopAsyncused to throwOpenCodeServerExceptionwhen it could not remove
the sidecar. StopAsyncescalates to the hard kill by the signalled process alone. When the process
survives the request to stop, the hard kill follows it even if its registration disappeared or
another service registered meanwhile, as upstream's client does; a successor's registration is
still never removed. Behaviour change: the stop used to end without the hard kill when the
registration changed between the two signals.- Both packages declare
IsAotCompatibleon every modern target (net8.0and later), not only
net10.0, so trimming and native AOT analysis cover thenet8.0andnet9.0assets too.
🔒 Security
- An unrecognized union value no longer prints a secret. When a union's known shapes carry a
masked member at any depth, its unknown arm — what a newer server's value decodes to — prints its
preserved payload as[REDACTED]and keeps its marker:UnknownCredentialValue,UnknownMcp,
UnknownConfigEntry, and the unknown arm ofMcpRemoteConfigOauth,ConfigInfoFormatter,
ConfigInfoLsp, andConfigLspEntry.Payloaditself and serialization are unchanged. - Every user-configured header and environment map is masked in
ToString(), now including
ConfigProvider.Headers,ConfigModel.Headers,ConfigModelVariants.Headers,
ConfigAgentRequest.Headers,ConfigFormatterEntry.Environment,ConfigLspServer.Env, and
SessionEnvironmentRequest.Variables. - The open provider options are masked in
ToString().ProviderSettings,
ConfigProviderSettings,ModelSettings, andConfigModelSettingskeep upstream's provider
options,apiKeyamong them, inAdditionalProperties. A record printed that dictionary's type
name, ...
0.9.0-preview.6
The pin moves to upstream release tag v2.0.22, and all 140 operations it exposes are
callable — 138 as generated HTTP calls, and the two terminal WebSocket connections through
hand-written transports. Stored credentials can
now be listed and created, a session can be created as a linked child of another, a cancelled form
can tell the asker why, and a connection reports when it needs signing in again. The refresh
changes three shapes, so the breaking changes come first. Discovery, Ensure, and Stop now read the
CLI's service files exactly as the CLI does. net8.0 and net9.0 leave the packages in an
upcoming preview; see the important notes.
💥 Breaking changes
CancelFormAsynctakes an optionalSessionFormCancelRequestbeforerequestOptions. A call
that passedrequestOptionspositionally names it now (requestOptions: options). The method is
virtual, so overrides and mock setups move to the new signature, and an assembly compiled against
0.9.0-preview.5must be rebuilt. The route helperOpenCodeRoutes.Sessions.CancelFormgained
the same optionalrequest.ProviderSettings.ChunkTimeoutandConfigProviderSettings.ChunkTimeoutare number-or-false
unions, notdouble?. Upstream accepts a number of milliseconds orfalse(no timeout), so a
provider configured withfalsenow decodes. ReadKind, thenNumberorBoolean; a value
neither arm claims is kept asUnknown.ChunkTimeout = 30000becomes
ChunkTimeout = ProviderSettingsChunkTimeout.FromNumber(30000)(ConfigProviderSettingsChunkTimeout
on the config side), andFromBoolean(false)turns the timeout off; upstream refusestrue.ConfigModel.Capabilitiesis aConfigModelCapabilities, not the catalog's
ModelCapabilities. It is the shape upstream's config accepts, and every member is optional:
Toolsisbool?, andInputandOutputmay be null.ModelInfo.Capabilitiesis unchanged.IConnectionInfohas aStatusmember. A type outside the SDK that implements the interface,
such as a test fake, addsConnectionStatus? Status; code that only reads connections is
unaffected.
✨ Added
- The accepted snapshot moved to upstream release tag
v2.0.22
(527f0b931d1f9b3ebd34e106c51b31ce5db5b075), published as@opencode/cli@2.0.22; install it
withnpm install -g @opencode/cli@2.0.22. - Stored credentials can be listed and created.
client.Credentials.ListCredentialsAsync()
(credential.list) returns everyCredentialEntry(Id,IntegrationId,Label,Active,
Value), andCreateCredentialAsync(request)(credential.create) stores one and returns it;
anIdthat already exists answers with the declared 409. They join activate, update, and
remove.Valueis aCredentialKeyor aCredentialOAuth. Upstream returns secrets in clear
text, so the API key (CredentialKey.Key) and the OAuth tokens (CredentialOAuth.Access,
Refresh) are masked inToString(). SessionCreateRequest.ParentIdcreates a linked child session at its parent's location; a
missing parent answers with the declared 404SessionNotFoundError.- A cancelled form can tell the asker why.
CancelFormAsync(formId, new SessionFormCancelRequest { Message = "..." })sends it, andFormStateCancelled.Messagecarries it back. HeaderTimeoutonProviderSettingsandConfigProviderSettingsbounds the wait for a
provider's response headers, as the same number-of-milliseconds-or-falseunion.- A connection reports when it needs attention.
IConnectionInfo.Statusis a
ConnectionStatuswith theStatus, aMessage, and, when set, theUrlto sign in again at. SessionStructuredError.Responsecarries the failed provider response'sBody.
🐛 Fixes
OpenCodeServer.DiscoverAsync,EnsureAsync, andStopAsyncdecode the CLI's service files
exactly as the CLI does. A whole-valued number such as8080.0is an integer for the service
config'sportand for the pid in the registration and the info probe. Before, such a config was
ignored, soEnsureAsynclost itsenvoverlay, and such a registration was not found. A service
config whosedisabledmember is not a boolean is no config at all, so itsenvoverlay and the
legacy config copy do not apply, as in the CLI.disabled: trueitself does not stop
EnsureAsync: only CLI commands read it, never the CLI's service operations.
📋 Important Notes
net8.0andnet9.0leave the packages, andnet11.0joins, in an upcoming preview, ahead of
their end of support on 2026-11-10. The packages will then target
netstandard2.0;net472;net10.0;net11.0. An app on .NET 8 or 9 still installs through the
netstandard2.0asset, but that asset is a compatibility asset, not a supported runtime: support
and testing cover the targeted runtimes only, and .NET 5–7 are not supported. Plan the move to
.NET 10.- Upstream's committed OpenAPI document is stale at
v2.0.22
(anomalyco/opencode#53105). This SDK's
snapshot comes from upstream's own pinned generator, so a diff against that committed file shows
four more operations here; they are real and the server serves them.
0.9.0-preview.5
The packages are now OpenCodeDotNet.Sdk and OpenCodeDotNet.Sdk.Extensions, and the
repository lives in the opencode-dotnet organization. The API is unchanged: only the
PackageReference changes. The release also fixes thread-pool starvation from standalone servers
on Windows.
💥 Breaking changes
- The packages are now
OpenCodeDotNet.SdkandOpenCodeDotNet.Sdk.Extensions. Change the
PackageReferencefromOpenCodeAI.SdkandOpenCodeAI.Sdk.Extensions; the earlier ids receive
no further versions. Assemblies and namespaces are stillOpenCode.Sdk, so no source changes. - The repository moved to
opencode-dotnet/opencode-sdk-dotnet.
GitHub redirects the old links. The nightly feed moved with it to
https://nuget.pkg.github.com/opencode-dotnet/index.json; the earlier feed keeps its existing
OpenCodeAI.*nightlies and receives no new ones.
🐛 Fixes
- A standalone server no longer holds two thread-pool threads on Windows.
Processcreates
the redirected stdout and stderr pipes synchronous there before .NET 11, and its event readers
blocked a pool thread on each for the server's whole life; with a few servers in one process the
pool starved, and unrelated async work stalled for seconds. The launcher now reads both streams
on dedicated threads on Windows, and disposal ends them: it waits for end-of-stream inside the
drain bound and cancels a read still blocked, so no reader outlives the server handle. That also
keeps the last lines of an exited child from missing theOpenCodeServerOutputsnapshot under
load. Linux and macOS are unchanged.
0.9.0-preview.4
Every operation upstream exposes is now callable: 100% coverage of the protocol surface. At
upstream release tag v2.0.18, all 138 operations in the pinned document are usable — 136
generated and the two terminal WebSocket transports — and none is declined. 0.9.0-preview.3
generated 130 and declined 4. config.get, experimental.migration.v1.status, fs.read, and
experimental.fs.write are now generated, and the refresh to v2.0.18 adds pairing.
✨ Added
- The accepted snapshot moved to upstream release tag
v2.0.18
(cd9a14a6b688d4021bee381dfd39d2cef9c0f862), published as@opencode/cli@2.0.18; install it
withnpm install -g @opencode/cli@2.0.18. It adds two operations and moves none, so all 138
are usable: 136 generated and the two WebSocket transports. - Pairing.
client.Server.CreatePairingCodeAsync()(server.pair) issues a short-lived,
single-use code;client.Server.RedeemPairingCodeAsync(code)(server.connect) exchanges it,
with no other credential, for a session token the server accepts anywhere the password is, so
another client connects withPassword = tokenwithout ever holding the server password. The
token is masked inPairingSession.ToString(), and so is the code inPairingCode.ToString(). ShellInfo.Signalnames the signal that ended a killed command.- The configuration can be read back.
client.Config.GetConfigAsync()(config.get) returns
the configuration documents and discovery sources for the location, from lowest to highest
priority: aConfigDocumentcarries itsConfigInfo, aConfigDirectorythe directory it came
from.Experimental.UpdateConfigAsyncwrites the global file and the server reloads it
asynchronously, publishingconfig.updatedonce the loaded configuration changes, so read after
that event. client.Experimental.GetMigrationV1StatusAsync()(experimental.migration.v1.status)
returns the V1 session-history migration status:ExperimentalMigrationV1StatusIdle
(RequiredorCompleted),…Runningwith its progress, or…Error.- Files can be written.
client.Experimental.WriteFileAsync(new ExperimentalFsWriteRequest { Path = … }, stream)
(experimental.fs.write) sends the stream as the raw body and returns the resolved absolute path
asFile.Path. The SDK reads the stream once from its current position to its end and never
disposes or rewinds it; a slow upload stays alive while it keeps moving. Upstream does not confine
the target to the location, so a relativePathresolves against it and an absolute one does not. - Files can be read.
client.FileSystem.ReadFileAsync(new FsReadRequest { Path = … })
(fs.read) returns the file's bytes asContent(ReadOnlyMemory<byte>) and the MIME type the
server declared asContentType.Pathis relative to the location and may be nested; the SDK
escapes each segment as upstream's own client does and refuses.and..segments. A missing
file is the declared 404FileNotFoundError; a path that leaves the location is refused by the
server. - Unions of objects without a discriminator are typed. A structural union carrier whose object
members share the JSON object token now selects a member the way upstream's decoder does: the
first member in declaration order whose required keys are present and whose literal-constrained
properties hold admitted values, then that member's full parse. A value no member claims is kept
as the carrier's rawUnknown. The configuration's reference entries (ConfigReferenceEntry),
language-server entries (ConfigLspEntry), and the migration status use it. - Operation members name their OpenAPI operation. Each generated operation method, each
Enumerate*Asynccompanion, and each hand-written PTY and persistent-PTY door carries an XML
<remarks>with its operation id, HTTP method, and route, for example
Operation session.permission.create: POST /api/session/{sessionID}/permission.
🔧 Changes
- A 401 now carries the typed
UnauthorizedError. From 2.0.18 the server answers a missing or
refused credential with the error body its document declares, where it used to send an empty
one, soErrorisUnauthorizedErrorinstead ofnull. Nothing changed in the SDK; the errors
guide now says so.
🐛 Fixes
- A logged response no longer prints the raw error body. Every response envelope's
ToString()printedRawBody, so logging a failed response wrote whatever the server echoed into
its error body, request data included. The printed form now keepsStatus,IsError, and the
typedError;RawBodyitself is unchanged and still carries the body. OpenCodeServer.EnsureAsyncno longer holds a thread-pool thread per contender on Windows.
The contender's standard error was an anonymous pipe, which Windows reads synchronously, so every
contender Ensure started kept one pool thread blocked for as long as its standard error stayed
open — for the elected service, its whole life — and ten concurrent callers stalled the pool for
seconds. The pipe's read end is now overlapped, as in .NET 11'sProcessand libuv.
📚 Documentation
- The README is a front page. It opens with the status and a capability table that maps each
area of the API to itsclient.*entry point and guide page. The quick start opens with a table
for choosing a connection mode, and the known issues keep only what a consumer meets. Every link
is absolute, so the page also works on NuGet.org. - The connection guide shows how to pair another client (
CreatePairingCodeAsync, then
RedeemPairingCodeAsync, thenPassword = token), and it lists the launcher'sOutputoption. - A CLI-started server's session log is the marker alone, followed as well as replayed. The
streaming guide and the README's known issues said liveFollow = Truedelivery was unaffected
by persistence; it is not. The server's live tail re-reads the same persisted store as a replay,
so against a server the distributedopencodeCLI started,GetLogAsyncdelivers one
EventLogSyncedand no durable event either way. Both now say so and point live session activity
toclient.Events.SubscribeAsync, filtered by session id. Observed on@opencode/cli@2.0.15. - A refused worktree removal can be partial. The errors guide said a 400
WorktreeErrorremoved
nothing. WithForceRequiredfalse, git can already have deleted the worktree's files and its
git metadata while the directory and the inventory row remain, and a retry then answers
Worktree directory unavailablewithForceRequirednull. The guide names the cause observed on
Windows at the pin — the server keeps the worktree's location alive, and that location's local MCP
servers run inside the worktree — and shows how to prevent it (Debug.EvictLocationAsyncbefore
the removal) and how to recover (evict, delete the directory,RefreshWorktreesAsync). - An empty VCS summary is explained. Getting started says that
Vcs.GetVcsAsyncfor a
directory the server has not served before can answer with anullProviderandnullbranch
values until that location's plugins settle, that the caller waits for a non-nullProvider,
and that anullBranch.Currentwith a provider set is a detached HEAD. - Getting started shows a prompt answered in a console app:
PromptAsync, then
Experimental.WaitForSessionAsyncunder a caller-owned deadline, thenEnumerateMessagesAsync. - Getting started has a .NET Framework section. A
net472project needs
<LangVersion>10.0</LangVersion>— the SDK's public surface needs C# 9 and the console template's
implicit usings C# 10 — and no extra package. - The requests guide explains the members the schema does not: session permission rules
(an update replaces the whole ruleset; create, list, and reply; how upstream evaluates), a
worktree'sDirectoryas the parent andNameas the child, and a shellTimeoutin
milliseconds with0or unset meaning none. - The README no longer states a test count by hand; the test badges carry it. A tools test now
fails when the README's guide table and the pages underdocs/guidediffer.