From b0ca5520a1a03f6227efa0441cd185995ac82e68 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 2 Jul 2026 16:45:09 -0400 Subject: [PATCH] feat(server): add lazy simulation layer replacements (#35024) --- packages/server/src/routes.ts | 33 +++++++++++++++++-------- packages/server/src/simulation/index.ts | 14 +++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 packages/server/src/simulation/index.ts diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index 4d5fe537d75..0cb76573deb 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -16,7 +16,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 { Layer, Option } from "effect" +import { Effect, Layer, Option } from "effect" import { Api } from "./api" import { ServerAuth } from "./auth" import { handlers } from "./handlers" @@ -58,15 +58,24 @@ function makeRoutes( sdkPlugins?: SdkPlugins.Store, ) { const pluginRuntimeCell = PluginRuntime.makeCell() - const serviceLayer = AppNodeBuilder.build( - applicationServices, - [ - [SessionExecution.node, SessionExecutionLocal.node], - [PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)], - [PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)], - ...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []), - ], - ) + 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] : []), + ] + // Simulation replacements are loaded via dynamic import so the simulation + // module is never eagerly loaded. Layer.unwrap defers both the import and + // the app-node build to layer-build time; when simulation is off the branch + // is byte-for-byte identical to a plain AppNodeBuilder.build call. + const serviceLayer = simulationEnabled() + ? Layer.unwrap( + Effect.gen(function* () { + const { simulationReplacements } = yield* Effect.promise(() => import("./simulation")) + return AppNodeBuilder.build(applicationServices, [...replacements, ...simulationReplacements]) + }), + ) + : AppNodeBuilder.build(applicationServices, replacements) return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe( Layer.provide(handlers), @@ -79,6 +88,10 @@ function makeRoutes( ) } +function simulationEnabled() { + return !!process.env.OPENCODE_SIMULATION +} + export const routes = createRoutes() export const webHandler = () => diff --git a/packages/server/src/simulation/index.ts b/packages/server/src/simulation/index.ts new file mode 100644 index 00000000000..3f499a5bf81 --- /dev/null +++ b/packages/server/src/simulation/index.ts @@ -0,0 +1,14 @@ +import { LayerNode } from "@opencode-ai/core/effect/layer-node" + +/** + * Layer replacements applied when the server is built in simulation mode. + * + * Empty for now; simulation-mode implementations will populate this with + * replacement nodes/layers that swap real services for simulated ones (e.g. + * a fake filesystem). The server merges these into the app node build when + * `OPENCODE_SIMULATION` is enabled, via a dynamic import so this module is + * never loaded eagerly. + */ +export const simulationReplacements: LayerNode.Replacements = [] + +export * as Simulation from "./index"