From e82624b7f9149ca1525b8b4e21e59799c6efdba8 Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Thu, 11 Jun 2026 02:54:27 -0600 Subject: [PATCH 1/3] fix: make client-options example genuinely demonstrate the preconfigured client Step 2 handed its preconfigured client to the process-wide singleton client manager, which step 1 had already initialized, so the client was ignored and step 2's requests flowed through step 1's client while the demo reported success. Pass an isolated manager via createInstance() instead, echo each request's x-demo-source header so the output proves which client served it, drop the spurious await on the synchronous v2 createOpencodeClient(), and allow overriding the model via OPENCODE_MODEL. --- examples/client-options.ts | 40 ++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/examples/client-options.ts b/examples/client-options.ts index 8b06715..3b91f66 100644 --- a/examples/client-options.ts +++ b/examples/client-options.ts @@ -1,14 +1,36 @@ import { generateText } from "ai"; -import { createOpencode } from "../dist/index.js"; +import { createOpencode, OpencodeClientManager } from "../dist/index.js"; import { createOpencodeClient } from "@opencode-ai/sdk/v2"; -const MODEL = "openai/gpt-5.3-codex-spark"; +const MODEL = process.env.OPENCODE_MODEL ?? "openai/gpt-5.3-codex-spark"; const BASE_URL = "http://127.0.0.1:4096"; function formatError(error: unknown): string { return error instanceof Error ? error.message : String(error); } +// Wraps fetch to echo each outgoing request's x-demo-source header, so the +// output proves which client a request actually flowed through. +function echoingFetch(): typeof fetch { + return (input, init) => { + const headers = + input instanceof Request ? input.headers : new Headers(init?.headers); + const url = + input instanceof Request + ? input.url + : input instanceof URL + ? input.href + : String(input); + const method = + (input instanceof Request ? input.method : init?.method) ?? "GET"; + const source = headers.get("x-demo-source") ?? "(none)"; + console.log( + ` [http] ${method} ${new URL(url).pathname} x-demo-source: ${source}`, + ); + return fetch(input, init); + }; +} + async function runStep(title: string, fn: () => Promise) { console.log(title); try { @@ -30,6 +52,7 @@ async function main() { headers: { "x-demo-source": "client-options-example", }, + fetch: echoingFetch(), credentials: "include", throwOnError: true, }, @@ -43,16 +66,25 @@ async function main() { console.log(` Response: ${text}`); }); - const preconfiguredClient = await createOpencodeClient({ + // createOpencodeClient() is synchronous in @opencode-ai/sdk/v2. + const preconfiguredClient = createOpencodeClient({ baseUrl: BASE_URL, headers: { "x-demo-source": "preconfigured-client-example", }, + fetch: echoingFetch(), throwOnError: true, }); + // createOpencode({ client }) hands the client to the process-wide singleton + // client manager. Step 1 already initialized that singleton, so the client + // would be ignored (with a warning) and requests would keep flowing through + // step 1's client. An isolated manager guarantees this provider really uses + // the preconfigured client. const providerWithPreconfiguredClient = createOpencode({ - client: preconfiguredClient, + clientManager: OpencodeClientManager.createInstance({ + client: preconfiguredClient, + }), }); await runStep("2) Provider with preconfigured SDK client", async () => { From 07c78fe29a1d5bc9f0c5217e91dc7db2598972c4 Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Thu, 11 Jun 2026 02:54:36 -0600 Subject: [PATCH 2/3] fix: recover singleton client manager after dispose dispose() set isDisposed but left the static singleton pointing at the dead instance, so every later createOpencode() without an explicit clientManager received the disposed manager and threw "Client manager has been disposed" on first use. Release the singleton slot in dispose() so getInstance() builds a fresh manager, and update the already-initialized warnings to recommend the escape hatches that actually work (createInstance() with the clientManager setting, or resetInstance()). --- src/opencode-client-manager.test.ts | 21 +++++++++++++++++++++ src/opencode-client-manager.ts | 10 ++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/opencode-client-manager.test.ts b/src/opencode-client-manager.test.ts index 1b2eb26..b9b65b3 100644 --- a/src/opencode-client-manager.test.ts +++ b/src/opencode-client-manager.test.ts @@ -507,6 +507,27 @@ describe("opencode-client-manager", () => { // Attempting to get client should throw await expect(instance.getClient()).rejects.toThrow("disposed"); }); + + it("should allow a fresh singleton to be created after dispose", async () => { + const clientA = { + session: { create: vi.fn(), prompt: vi.fn(), abort: vi.fn() }, + event: { subscribe: vi.fn() }, + } as unknown as OpencodeClient; + + const clientB = { + session: { create: vi.fn(), prompt: vi.fn(), abort: vi.fn() }, + event: { subscribe: vi.fn() }, + } as unknown as OpencodeClient; + + const first = OpencodeClientManager.getInstance({ client: clientA }); + expect(await first.getClient()).toBe(clientA); + + await first.dispose(); + + const second = OpencodeClientManager.getInstance({ client: clientB }); + expect(second).not.toBe(first); + expect(await second.getClient()).toBe(clientB); + }); }); describe("createClientManager", () => { diff --git a/src/opencode-client-manager.ts b/src/opencode-client-manager.ts index 11d511f..25f3751 100644 --- a/src/opencode-client-manager.ts +++ b/src/opencode-client-manager.ts @@ -133,13 +133,13 @@ export class OpencodeClientManager { filteredOptions.client !== this.client ) { this.logger.warn( - "Client manager already initialized; provided preconfigured client was ignored because a client is already active. New options from createOpencode() are ignored after initialization. Use separate client manager instances or call dispose() first.", + "Client manager already initialized; provided preconfigured client was ignored because a client is already active — requests will keep flowing through the existing client. To use this client, pass an isolated manager via createOpencode({ clientManager: OpencodeClientManager.createInstance({ client }) }), or call OpencodeClientManager.resetInstance() before creating the provider.", ); return; } this.logger.warn( - "Client manager already initialized; new options from createOpencode() were ignored. Use separate client manager instances or call dispose() first.", + "Client manager already initialized; new options from createOpencode() were ignored. Use OpencodeClientManager.createInstance() with the clientManager provider setting for isolated configuration, or call OpencodeClientManager.resetInstance() first.", ); } @@ -324,6 +324,12 @@ export class OpencodeClientManager { this.isDisposed = true; this.unregisterCleanupHandlers(); + // Release the singleton slot so a later getInstance() builds a fresh + // manager instead of returning this disposed one. + if (OpencodeClientManager.instance === this) { + OpencodeClientManager.instance = null; + } + if (this.server) { this.logger.debug?.("Stopping managed OpenCode server"); try { From 869c8995565a9b89a95c4f3d2bee4a2988cb6dfb Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Thu, 11 Jun 2026 02:54:44 -0600 Subject: [PATCH 3/3] chore: release 3.0.5 --- CHANGELOG.md | 11 +++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9049c6..24aaf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.5] - 2026-06-11 + +### Fixed + +- **Singleton client manager recovery after dispose** - `OpencodeClientManager.dispose()` now releases the singleton slot when the disposed manager is the singleton, so a later `createOpencode()` builds a fresh client manager. Previously the singleton kept pointing at the disposed instance, and every subsequent provider in the same process failed with `Client manager has been disposed`. +- **client-options example** - Step 2 of `examples/client-options.ts` now genuinely demonstrates the preconfigured-client pattern by passing an isolated manager via `clientManager: OpencodeClientManager.createInstance({ client })`. Previously the preconfigured client was handed to the singleton that step 1 had already initialized, so it was ignored and step 2's requests flowed through step 1's client while the demo reported success. The example now also echoes each outgoing request's `x-demo-source` header so the output proves which client served it, drops a spurious `await` on the synchronous v2 `createOpencodeClient()`, and allows overriding the example model via `OPENCODE_MODEL`. + +### Changed + +- **Clearer stale-options warnings** - The client manager's "already initialized" warnings now point at the escape hatches that actually work (`OpencodeClientManager.createInstance()` with the `clientManager` provider setting, or `OpencodeClientManager.resetInstance()`) instead of the previous generic advice. + ## [3.0.4] - 2026-06-11 ### Fixed diff --git a/package-lock.json b/package-lock.json index 94e938d..cae8914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai-sdk-provider-opencode-sdk", - "version": "3.0.4", + "version": "3.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-sdk-provider-opencode-sdk", - "version": "3.0.4", + "version": "3.0.5", "license": "MIT", "dependencies": { "@ai-sdk/provider": "^3.0.8", diff --git a/package.json b/package.json index 8e4d138..abc3994 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ai-sdk-provider-opencode-sdk", - "version": "3.0.4", + "version": "3.0.5", "description": "AI SDK v6 provider for OpenCode via @opencode-ai/sdk", "keywords": [ "ai-sdk",