Repository navigation
0.9.0-preview.7
Pre-releaseThe 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, so no key was printed; it now prints[REDACTED]while the dictionary holds any member and
empty while it holds none, so the value stays out if printing ever changes. The dictionary and
serialization are unchanged.
🐛 Fixes
- Disposing a standalone server on Linux and macOS no longer waits about 13 seconds when a
process the server started keeps its output open. The shutdown waits also waited for the
server's redirected output to end, so a server that exited at once still ran out the 3-second
grace and the 10-second forced-exit wait. Disposal now waits for the server's own exit and then
for its process group; only anOpenCodeServerOutputcollector waits for the output, for at most
one second. - A standalone server on Linux and macOS starts with every signal at its default disposition and
an empty signal mask, as upstream's launcher starts it. It used to inherit a signal its host
ignores (the .NET runtime ignoresSIGPIPE, and a host started undernohupignoresSIGINT
andSIGHUP). - Servers and background-service contenders started at the same time on macOS no longer receive
each other's pipe ends. macOS marks a new pipe close-on-exec only after creating it, so a spawn
on another thread in between could inherit a server's stdin lease and keep it open after the host
died. The SDK's spawns now take one lock around pipe creation and spawn, and on macOS the child
receives no descriptor beyond its own three. OpenCodeServer.DisposeAsyncno longer throws when the forced tree kill is incomplete. The
runtime raisesAggregateExceptionwhen a process of the tree refuses the kill. That exception
escaped disposal and skipped the rest of it: the bounded output drain, the release of the output
readers (on Windows, a reader blocked on a pipe that a descendant still held was never
cancelled), and the completion of theOpenCodeServerOutputcollector. A failedStartAsync
whose tree kill was incomplete threw thatAggregateExceptioninstead of
OpenCodeServerExceptionorOperationCanceledException. Disposal and a failed start now
continue past an incomplete kill and release everything.- A standalone server's stdout and stderr pipe handles are closed at disposal on Windows.
Processnever closes a redirected stream that was read synchronously, so the handles waited for
the finalizer; each output reader now closes its pipe as it ends, after a failed start too. - The
net472andnetstandard2.0Windows tree kill no longer leaves taskkill running. A
taskkill still running at its 10-second bound is now ended instead of left running.
📋 Important Notes
- On Windows a standalone server is still started and ended through
Process. The session
placement and theSIGTERM-then-SIGKILLladder above apply to Linux and macOS only. On
Windows, disposal closes the server's stdin, waits up toGracefulShutdownTimeout, and only then
kills the server's process tree; a server that exits inside the grace leaves its descendants
running. 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 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 still stale at
v2.0.23
(anomalyco/opencode#53105). It lacks
LocationNotFoundError, and with it the 404 that 95 operations declare or widen for a missing
location. This SDK's snapshot comes from upstream's own pinned generator, so a diff against that
committed file shows them here; they are real and the server answers them.