fix(core): preserve SDK plugins across location eviction (#35725)

This commit is contained in:
Kit Langton 2026-07-07 11:43:40 -04:00 committed by GitHub
commit 8deb0e5780
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 209 additions and 105 deletions

View file

@ -1,47 +1,23 @@
import { OpenCode } from "@opencode-ai/client/effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { Project } from "@opencode-ai/core/project"
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
import { Context, Effect, Layer, Scope } from "effect"
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
import { Context, Effect, Layer, ManagedRuntime } from "effect"
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
export const create = Effect.fn("OpenCode.create")(function* () {
const scope = yield* Scope.Scope
const memoMap = yield* Layer.makeMemoMap
const sdkPlugins = SdkPlugins.makeStore()
const context = yield* Layer.buildWithMemoMap(
AppNodeBuilder.build(LayerNode.group([EventV2.node, PermissionSaved.node, Project.node, SdkPlugins.node]), [
[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)],
]),
memoMap,
scope,
const runtime = yield* Effect.acquireRelease(
Effect.sync(() => ManagedRuntime.make(createEmbeddedRoutes().pipe(Layer.provide(HttpServer.layerServices)))),
(runtime) => runtime.disposeEffect,
)
const context = yield* runtime.contextEffect
const plugins = Context.get(context, SdkPlugins.Service)
const permissions = Context.get(context, PermissionSaved.Service)
const project = Context.get(context, Project.Service)
const web = yield* Effect.acquireRelease(
Effect.sync(() =>
HttpRouter.toWebHandler(
createEmbeddedRoutes(sdkPlugins).pipe(
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
HttpRouter.provideRequest(Layer.succeed(Project.Service, project)),
Layer.provide(HttpServer.layerServices),
),
{ disableLogger: true, memoMap },
),
),
(web) => Effect.promise(web.dispose),
)
const fetch = Object.assign((input: RequestInfo | URL, init?: RequestInit) => web.handler(new Request(input, init)), {
const router = Context.get(context, HttpRouter.HttpRouter)
const handler = HttpEffect.toWebHandler(router.asHttpEffect())
const fetch = Object.assign((input: RequestInfo | URL, init?: RequestInit) => handler(new Request(input, init)), {
preconnect: () => undefined,
}) satisfies typeof globalThis.fetch
const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }).pipe(
Effect.provide(FetchHttpClient.layer),
Effect.provideService(FetchHttpClient.Fetch, fetch),
Effect.provide(FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetch)), Layer.fresh)),
)
return {
...client,

View file

@ -105,6 +105,50 @@ it.live(
25_000,
)
it.live(
"preserves SDK plugins across Location eviction",
() =>
withEmbedded("opencode-embedded-plugin-eviction-", (fixture) =>
Effect.gen(function* () {
const opencode = yield* fixture.sdk.OpenCode.create()
const ref = location(fixture)
const connected = yield* Latch.make(false)
const booted = yield* Deferred.make<void>()
// The rebooted Location commits its second plugin generation.
const recommitted = yield* Deferred.make<void>()
const generations = yield* Ref.make(0)
const id = `evicted-sdk-${crypto.randomUUID()}`
yield* opencode.events.subscribe().pipe(
Stream.runForEach((event) => {
if (event.type === "server.connected") return connected.open
if (event.type !== "plugin.updated" || event.location?.directory !== fixture.directory) return Effect.void
return Ref.updateAndGet(generations, (total) => total + 1).pipe(
Effect.flatMap((total) => {
if (total === 1) return Deferred.succeed(booted, undefined)
if (total === 2) return Deferred.succeed(recommitted, undefined)
return Effect.void
}),
Effect.asVoid,
)
}),
Effect.forkScoped,
)
yield* connected.await
yield* opencode.plugin({ id, effect: () => Effect.void })
yield* opencode.plugin.list({ location: ref })
yield* Deferred.await(booted).pipe(Effect.timeout("5 seconds"))
yield* opencode.debug.evictLocation({ location: ref })
yield* opencode.plugin.list({ location: ref })
yield* Deferred.await(recommitted).pipe(Effect.timeout("5 seconds"))
expect((yield* opencode.plugin.list({ location: ref })).data.map((plugin) => String(plugin.id))).toContain(id)
}),
),
15_000,
)
it.live(
"keeps SDK plugin registration isolated between embedded hosts",
() =>