Skip to content

0.9.0-preview.7

Pre-release
Pre-release

Choose a tag to compare

@Blind-Striker Blind-Striker released this 06 Oct 13:25

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

  • WorktreeError is a tagged error. Upstream 2.0.23 declares it with _tag beside its name
    and data, so Tag reads _tag and a new required Name carries name. A body without _tag,
    from a server older than 2.0.23, no longer decodes to a typed error: Error is null and
    RawBody keeps the body.
  • _tag is the only error marker. Every error the 2.0.23 contract declares is tagged, so a
    name is 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: Error is null and RawBody keeps the body.
    • The public UnknownOpenCodeError(tag, payload) constructor throws ArgumentException for a
      payload whose only marker is name; the payload must carry the tag under _tag.
  • OpenCodeServer.EnsureAsync throws OpenCodeServerException on a service whose health
    protocol is incompatible, where it used to replace it.
    A registered daemon that answers
    /api/info with 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 with StopAsync to
    replace it.
  • OpenCodeServer.StartAsync refuses POSIX systems other than Linux and macOS, FreeBSD among
    them,
    with OpenCodeServerException before 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
    with ArgumentException, 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
    with npm 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 optional Provider (git by
    default); a provider without initialization answers 501 VcsInitNotSupportedError. The route
    helper is OpenCodeRoutes.Vcs.InitializeRepository.
  • A missing directory is a typed 404. Every location- and session-scoped route, and
    experimental.generate.text, session.diff, and session.permission.list by 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 its OpenCodeTransportException now names both causes:
    the PTY session or the requested location does not exist.
  • ServerInfo.Capabilities.PersistentPty says whether the server can run the
    persistent-terminal daemon: false on Windows, where opencode-pty ships no binaries. A server
    older than 2.0.23 leaves Capabilities null.

🔧 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 through DisposeAsync, 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 SIGTERM to the server's process group,
    and SIGKILL to whatever is left of the group after GracefulShutdownTimeout
    , as upstream's own
    launcher does. Behaviour changes:
    • The pinned server answers the SIGTERM by 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 ignores SIGCHLD on
      Linux, or runs as pid 1); one that exited with code 0 or on a signal leaves them alone, as
      before.
  • A failed StartAsync on 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 after SIGKILL, 1 for draining its output, and 5 for releasing the
    output readers. Behaviour change: it used to kill the server at once.
  • StartAsync on 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.WorkingDirectory works 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 blank WorkingDirectory counts as none.
  • A standalone server's relative command path on Linux and macOS is resolved against the
    caller's current directory
    , never against WorkingDirectory. 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=1 in 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.
  • EnsureAsync reports 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.
    StopAsync and the Ensure replacement end the service even when the sidecar cannot be removed.
    Behaviour change: StopAsync used to throw OpenCodeServerException when it could not remove
    the sidecar.
  • StopAsync escalates 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 IsAotCompatible on every modern target (net8.0 and later), not only
    net10.0, so trimming and native AOT analysis cover the net8.0 and net9.0 assets 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 of McpRemoteConfigOauth, ConfigInfoFormatter,
    ConfigInfoLsp, and ConfigLspEntry. Payload itself 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, and ConfigModelSettings keep upstream's provider
    options, apiKey among them, in AdditionalProperties. 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 an OpenCodeServerOutput collector 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 ignores SIGPIPE, and a host started under nohup ignores SIGINT
    and SIGHUP).
  • 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.DisposeAsync no longer throws when the forced tree kill is incomplete. The
    runtime raises AggregateException when 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 the OpenCodeServerOutput collector. A failed StartAsync
    whose tree kill was incomplete threw that AggregateException instead of
    OpenCodeServerException or OperationCanceledException. 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.
    Process never 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 net472 and netstandard2.0 Windows 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 the SIGTERM-then-SIGKILL ladder above apply to Linux and macOS only. On
    Windows, disposal closes the server's stdin, waits up to GracefulShutdownTimeout, and only then
    kills the server's process tree; a server that exits inside the grace leaves its descendants
    running.
  • net8.0 and net9.0 leave the packages, and net11.0 joins, 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.0 asset, 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.