From 4296f1e2ba9c8f1e3ccd09842058200832814b7b Mon Sep 17 00:00:00 2001 From: rafi-the-dev Date: Thu, 12 Nov 2026 15:12:19 +0100 Subject: [PATCH 1/2] fix(server): wait for OpenCode skills before caching cold workspaces --- .../src/server/driver.test.ts | 69 +++++++++++++++++++ .../provider-opencode/src/server/driver.ts | 22 +++--- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index 6d74dc8dd2542..d5146db51444e 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -537,6 +537,75 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => { }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), ); + it.effect.each([false, true])( + "waits for OpenCode 2.2.7 plugin activation before caching skills (commands loaded: %s)", + (commandsLoaded) => + Effect.gen(function* () { + const activationRequested = yield* Deferred.make(); + const activated = yield* Deferred.make(); + let skillReads = 0; + let activationComplete = false; + const replay = replayOpenCodeServer( + { ...OPENCODE_2_RESPONSES, ...OPENCODE_2_WORKSPACE_RESPONSES }, + "secret", + ); + const http = HttpClient.make((request) => { + const path = new URL(request.url).pathname; + const directory = Option.getOrUndefined( + UrlParams.getFirst(request.urlParams, "location[directory]"), + ); + const json = (data: unknown) => + Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(data))); + if (path === "/api/info") + return json({ + ...JSON.parse(OPENCODE_2_RESPONSES["/api/info"].body), + version: "2.2.7", + }); + if (path === "/api/provider") + return directory === "/work" + ? Deferred.succeed(activationRequested, undefined).pipe( + Effect.andThen(Deferred.await(activated)), + Effect.andThen(json({ location: { directory }, data: [] })), + ) + : json({ location: { directory }, data: [] }); + if (path === "/api/model") return json({ location: { directory }, data: [] }); + if (path === "/api/debug/location") return json([{ directory: "/work" }]); + if (path === "/api/event") + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(new ReadableStream(), { + headers: { "content-type": "text/event-stream" }, + }), + ), + ); + if (path === "/api/skill") skillReads++; + if ( + (path === "/api/skill" || (path === "/api/command" && !commandsLoaded)) && + !activationComplete + ) + return json({ location: { directory }, data: [] }); + return replay.execute(request); + }); + const instance = yield* create( + { serverUrl: "http://127.0.0.1:4096", serverPassword: "secret" }, + http, + ); + const snapshot = yield* instance.snapshotForCwd!("/work").pipe(Effect.forkChild); + yield* Deferred.await(activationRequested); + yield* TestClock.adjust("1 second"); + activationComplete = true; + yield* Deferred.succeed(activated, undefined); + yield* TestClock.adjust("30 seconds"); + const { skills } = yield* Fiber.join(snapshot); + assert.include( + skills.map((skill) => skill.name), + "plum", + ); + assert.equal(skillReads, 1); + }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), + ); + it.effect.each(["probe", "error", "empty"] as const)( "recovers discovery without a manual refresh after %s", (initialCatalog) => diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index 2686de885b3f0..ed4f3840e5116 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -399,11 +399,23 @@ export const OpenCodeDriver: ProviderDriver< openCode2Server.withConnection(({ client }) => loadOpenCode2Catalog(client, host.paths.cwd)), ); const listOpenCode2Workspace = (cwd: string) => - openCode2Server.withConnection(({ client, events }) => + openCode2Server.withConnection(({ client, events, version }) => Effect.gen(function* () { // OpenCode names a location by its resolved path (no trailing slash or `..`). const directory = pathService.resolve(cwd); const location = { directory }; + const read = Effect.all( + { + skills: client.skill.list({ location }).pipe(Effect.map((list) => list.data)), + commands: client.command.list({ location }).pipe(Effect.map((list) => list.data)), + }, + { concurrency: "unbounded" }, + ); + if (compareSemverVersions(version, "2.2.7") >= 0) { + // provider.list awaits plugin activation; loaded locations and update events do not. + yield* client.provider.list({ location }).pipe(Effect.timeout("10 seconds")); + return yield* read; + } const scanned = yield* Deferred.make(); const pending = new Set(["command.updated", "skill.updated"]); const stream = yield* events.pipe(Effect.option); @@ -422,13 +434,7 @@ export const OpenCodeDriver: ProviderDriver< ); } return yield* loadOpenCode2Workspace( - Effect.all( - { - skills: client.skill.list({ location }).pipe(Effect.map((list) => list.data)), - commands: client.command.list({ location }).pipe(Effect.map((list) => list.data)), - }, - { concurrency: "unbounded" }, - ), + read, Deferred.await(scanned), // Without events no scan can be awaited, so trust the first read as before. stream._tag === "None" From 517405b285063e5ac35187d209fb01f64359f17e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 16:38:34 +0000 Subject: [PATCH 2/2] test(server): keep one OpenCode plugin activation regression case AI-assisted; verified with the tests and tools listed above. --- .../src/server/driver.test.ts | 125 +++++++++--------- 1 file changed, 60 insertions(+), 65 deletions(-) diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index d5146db51444e..0c6f1862bfcd4 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -537,73 +537,68 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => { }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), ); - it.effect.each([false, true])( - "waits for OpenCode 2.2.7 plugin activation before caching skills (commands loaded: %s)", - (commandsLoaded) => - Effect.gen(function* () { - const activationRequested = yield* Deferred.make(); - const activated = yield* Deferred.make(); - let skillReads = 0; - let activationComplete = false; - const replay = replayOpenCodeServer( - { ...OPENCODE_2_RESPONSES, ...OPENCODE_2_WORKSPACE_RESPONSES }, - "secret", + it.effect("waits for OpenCode 2.2.7 plugin activation before caching skills", () => + Effect.gen(function* () { + const activationRequested = yield* Deferred.make(); + const activated = yield* Deferred.make(); + let skillReads = 0; + let activationComplete = false; + const replay = replayOpenCodeServer( + { ...OPENCODE_2_RESPONSES, ...OPENCODE_2_WORKSPACE_RESPONSES }, + "secret", + ); + const http = HttpClient.make((request) => { + const path = new URL(request.url).pathname; + const directory = Option.getOrUndefined( + UrlParams.getFirst(request.urlParams, "location[directory]"), ); - const http = HttpClient.make((request) => { - const path = new URL(request.url).pathname; - const directory = Option.getOrUndefined( - UrlParams.getFirst(request.urlParams, "location[directory]"), + const json = (data: unknown) => + Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(data))); + if (path === "/api/info") + return json({ + ...JSON.parse(OPENCODE_2_RESPONSES["/api/info"].body), + version: "2.2.7", + }); + if (path === "/api/provider") + return directory === "/work" + ? Deferred.succeed(activationRequested, undefined).pipe( + Effect.andThen(Deferred.await(activated)), + Effect.andThen(json({ location: { directory }, data: [] })), + ) + : json({ location: { directory }, data: [] }); + if (path === "/api/model") return json({ location: { directory }, data: [] }); + if (path === "/api/debug/location") return json([{ directory: "/work" }]); + if (path === "/api/event") + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(new ReadableStream(), { + headers: { "content-type": "text/event-stream" }, + }), + ), ); - const json = (data: unknown) => - Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(data))); - if (path === "/api/info") - return json({ - ...JSON.parse(OPENCODE_2_RESPONSES["/api/info"].body), - version: "2.2.7", - }); - if (path === "/api/provider") - return directory === "/work" - ? Deferred.succeed(activationRequested, undefined).pipe( - Effect.andThen(Deferred.await(activated)), - Effect.andThen(json({ location: { directory }, data: [] })), - ) - : json({ location: { directory }, data: [] }); - if (path === "/api/model") return json({ location: { directory }, data: [] }); - if (path === "/api/debug/location") return json([{ directory: "/work" }]); - if (path === "/api/event") - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response(new ReadableStream(), { - headers: { "content-type": "text/event-stream" }, - }), - ), - ); - if (path === "/api/skill") skillReads++; - if ( - (path === "/api/skill" || (path === "/api/command" && !commandsLoaded)) && - !activationComplete - ) - return json({ location: { directory }, data: [] }); - return replay.execute(request); - }); - const instance = yield* create( - { serverUrl: "http://127.0.0.1:4096", serverPassword: "secret" }, - http, - ); - const snapshot = yield* instance.snapshotForCwd!("/work").pipe(Effect.forkChild); - yield* Deferred.await(activationRequested); - yield* TestClock.adjust("1 second"); - activationComplete = true; - yield* Deferred.succeed(activated, undefined); - yield* TestClock.adjust("30 seconds"); - const { skills } = yield* Fiber.join(snapshot); - assert.include( - skills.map((skill) => skill.name), - "plum", - ); - assert.equal(skillReads, 1); - }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), + if (path === "/api/skill") skillReads++; + if (path === "/api/skill" && !activationComplete) + return json({ location: { directory }, data: [] }); + return replay.execute(request); + }); + const instance = yield* create( + { serverUrl: "http://127.0.0.1:4096", serverPassword: "secret" }, + http, + ); + const snapshot = yield* instance.snapshotForCwd!("/work").pipe(Effect.forkChild); + yield* Deferred.await(activationRequested); + yield* TestClock.adjust("1 second"); + activationComplete = true; + yield* Deferred.succeed(activated, undefined); + yield* TestClock.adjust("30 seconds"); + const { skills } = yield* Fiber.join(snapshot); + assert.include( + skills.map((skill) => skill.name), + "plum", + ); + assert.equal(skillReads, 1); + }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), ); it.effect.each(["probe", "error", "empty"] as const)(