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

@ -2,13 +2,24 @@ import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
import { Effect, Option, RcMap } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
import { requestRef } from "../location"
export const DebugHandler = HttpApiBuilder.group(Api, "server.debug", (handlers) =>
handlers.handle(
"debug.location",
Effect.fn(function* () {
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
return Array.from(yield* RcMap.keys(locations.rcMap))
}),
),
handlers
.handle(
"debug.location",
Effect.fn(function* () {
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
return Array.from(yield* RcMap.keys(locations.rcMap))
}),
)
.handle(
"debug.location.evict",
Effect.fn(function* (ctx) {
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
// Resolve through requestRef so the key matches the shape the location
// middleware cached the services under.
yield* locations.invalidate(requestRef(ctx.request))
}),
),
)

View file

@ -1,14 +1,9 @@
export * as ServerProcess from "./process"
import { NodeHttpClient, NodeHttpServer } from "@effect/platform-node"
import { Credential } from "@opencode-ai/core/credential"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { Project } from "@opencode-ai/core/project"
import { HealthGroup } from "@opencode-ai/protocol/groups/health"
import { Context, Effect, Layer, Option } from "effect"
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpClient, HttpClientRequest, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
import { createServer } from "node:http"
import { ServerAuth } from "./auth"
@ -53,9 +48,11 @@ function listen(options: Options) {
function bind(hostname: string, port: number, password: string) {
const server = createServer()
return Layer.build(
HttpRouter.serve(createRoutes(password), { disableListenLog: true }).pipe(
createRoutes(password).pipe(
Layer.flatMap((context) =>
HttpServer.serve(Context.get(context, HttpRouter.HttpRouter).asHttpEffect(), HttpMiddleware.logger),
),
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
),
).pipe(
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),

View file

@ -19,7 +19,7 @@ import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Effect, Layer, Option } from "effect"
import { Context, Effect, Layer, Option } from "effect"
import { Api } from "./api"
import { ServerAuth } from "./auth"
import { handlers } from "./handlers"
@ -40,6 +40,7 @@ const applicationServices = LayerNode.group([
Project.node,
SessionV2.node,
PluginRuntime.providerNode,
SdkPlugins.node,
PermissionSaved.node,
PtyTicket.node,
Credential.node,
@ -55,26 +56,22 @@ export function createRoutes(password?: string) {
)
}
export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store) {
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), sdkPlugins)
export function createEmbeddedRoutes() {
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }))
}
function makeRoutes<AuthError, AuthServices>(
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
sdkPlugins?: SdkPlugins.Store,
) {
function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>) {
const pluginRuntimeCell = PluginRuntime.makeCell()
const replacements: LayerNode.Replacements = [
[SessionExecution.node, SessionExecutionLocal.node],
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []),
]
const serviceLayer = simulateEnabled()
? Layer.unwrap(
Effect.gen(function* () {
const { simulationReplacements, startDriveServer } = yield* Effect.promise(() =>
import("@opencode-ai/simulation/backend"),
const { simulationReplacements, startDriveServer } = yield* Effect.promise(
() => import("@opencode-ai/simulation/backend"),
)
if (driveEnabled()) startDriveServer()
return AppNodeBuilder.build(applicationServices, [
@ -85,16 +82,24 @@ function makeRoutes<AuthError, AuthServices>(
)
: AppNodeBuilder.build(applicationServices, replacements)
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers.pipe(Layer.provide(serviceLayer))),
Layer.provide(formLocationLayer),
Layer.provide(sessionLocationLayer),
Layer.provide(layer),
Layer.provide(authorizationLayer),
Layer.provide(schemaErrorLayer),
Layer.provide(auth),
Layer.provide(serviceLayer),
Layer.provide(Observability.layer),
return serviceLayer.pipe(
Layer.flatMap((context) => {
const services = Layer.succeedContext(context)
const requestServices = Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service)(context))
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers.pipe(Layer.provide(services))),
Layer.provide(formLocationLayer),
Layer.provide(sessionLocationLayer),
Layer.provide(layer),
Layer.provide(authorizationLayer),
Layer.provide(schemaErrorLayer),
Layer.provide(auth),
Layer.provide(Observability.layer),
HttpRouter.provideRequest(requestServices),
Layer.provideMerge(services),
Layer.provideMerge(HttpRouter.layer),
)
}),
)
}
@ -108,5 +113,4 @@ function driveEnabled() {
export const routes = createRoutes()
export const webHandler = () =>
HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)))
export const webHandler = () => HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)))