From 14eacb40192be0b14e7f8d6273c3a735a5bdd255 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 15 Apr 2026 21:07:58 +0800 Subject: [PATCH 1/6] core: move plugin intialisation to config layer override --- packages/opencode/src/effect/app-runtime.ts | 19 ++++++++++++++++++- packages/opencode/src/project/bootstrap.ts | 6 ------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index eae52d6366db..17be3ac634d2 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -47,6 +47,23 @@ import { Installation } from "@/installation" import { ShareNext } from "@/share" import { SessionShare } from "@/share" import { Npm } from "@opencode-ai/shared/npm" +import * as Effect from "effect/Effect" + +// Adjusts the default Config layer to ensure that plugins are always initialised before +// any other layers read the current config +const PluginPriorityConfigLayer = Layer.unwrap( + Effect.gen(function* () { + const configSvc = yield* Config.Service + const pluginSvc = yield* Plugin.Service + + return Layer.succeed(Config.Service, { + ...configSvc, + get: () => Effect.andThen(pluginSvc.init(), configSvc.get), + getGlobal: () => Effect.andThen(pluginSvc.init(), configSvc.getGlobal), + getConsoleState: () => Effect.andThen(pluginSvc.init(), configSvc.getConsoleState), + }) + }), +).pipe(Layer.provideMerge(Layer.merge(Plugin.defaultLayer, Config.defaultLayer))) export const AppLayer = Layer.mergeAll( Npm.defaultLayer, @@ -54,7 +71,7 @@ export const AppLayer = Layer.mergeAll( Bus.defaultLayer, Auth.defaultLayer, Account.defaultLayer, - Config.defaultLayer, + PluginPriorityConfigLayer, Git.defaultLayer, Ripgrep.defaultLayer, File.defaultLayer, diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index a7c071a9f80b..012b10ef8695 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -1,4 +1,3 @@ -import { Plugin } from "../plugin" import { Format } from "../format" import { LSP } from "../lsp" import { File } from "../file" @@ -12,14 +11,9 @@ import { Log } from "@/util" import { FileWatcher } from "@/file/watcher" import { ShareNext } from "@/share" import * as Effect from "effect/Effect" -import { Config } from "@/config" export const InstanceBootstrap = Effect.gen(function* () { Log.Default.info("bootstrapping", { directory: Instance.directory }) - // everything depends on config so eager load it for nice traces - yield* Config.Service.use((svc) => svc.get()) - // Plugin can mutate config so it has to be initialized before anything else. - yield* Plugin.Service.use((svc) => svc.init()) yield* Effect.all( [ LSP.Service, From e287569f82c3935415db4c18c2bda915280a63ef Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 15 Apr 2026 21:19:59 +0800 Subject: [PATCH 2/6] rename layer --- packages/opencode/src/effect/app-runtime.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 17be3ac634d2..b96af2fa5a99 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -51,7 +51,7 @@ import * as Effect from "effect/Effect" // Adjusts the default Config layer to ensure that plugins are always initialised before // any other layers read the current config -const PluginPriorityConfigLayer = Layer.unwrap( +const ConfigWithPluginPriority = Layer.unwrap( Effect.gen(function* () { const configSvc = yield* Config.Service const pluginSvc = yield* Plugin.Service @@ -71,7 +71,7 @@ export const AppLayer = Layer.mergeAll( Bus.defaultLayer, Auth.defaultLayer, Account.defaultLayer, - PluginPriorityConfigLayer, + ConfigWithPluginPriority, Git.defaultLayer, Ripgrep.defaultLayer, File.defaultLayer, From dc6d39551c51e7d514725f0161e3158d130b256b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 16 Apr 2026 06:36:34 +0800 Subject: [PATCH 3/6] address feedback --- packages/opencode/src/effect/app-runtime.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index b96af2fa5a99..d5e76cb5b7c0 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -51,19 +51,20 @@ import * as Effect from "effect/Effect" // Adjusts the default Config layer to ensure that plugins are always initialised before // any other layers read the current config -const ConfigWithPluginPriority = Layer.unwrap( +const ConfigWithPluginPriority = Layer.effect( + Config.Service, Effect.gen(function* () { - const configSvc = yield* Config.Service - const pluginSvc = yield* Plugin.Service + const config = yield* Config.Service + const plugin = yield* Plugin.Service - return Layer.succeed(Config.Service, { - ...configSvc, - get: () => Effect.andThen(pluginSvc.init(), configSvc.get), - getGlobal: () => Effect.andThen(pluginSvc.init(), configSvc.getGlobal), - getConsoleState: () => Effect.andThen(pluginSvc.init(), configSvc.getConsoleState), - }) + return { + ...config, + get: () => Effect.andThen(plugin.init(), config.get), + getGlobal: () => Effect.andThen(plugin.init(), config.getGlobal), + getConsoleState: () => Effect.andThen(plugin.init(), config.getConsoleState), + } }), -).pipe(Layer.provideMerge(Layer.merge(Plugin.defaultLayer, Config.defaultLayer))) +).pipe(Layer.provide(Layer.merge(Plugin.defaultLayer, Config.defaultLayer))) export const AppLayer = Layer.mergeAll( Npm.defaultLayer, From 031766efa030800caee79d1ea745e9ab95689555 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 16 Apr 2026 10:54:55 +0800 Subject: [PATCH 4/6] fix tui --- packages/opencode/src/cli/cmd/tui/thread.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 96ceb905c5ff..f1b94a2ce988 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -8,6 +8,7 @@ import { UI } from "@/cli/ui" import { Log } from "@/util" import { errorMessage } from "@/util/error" import { withTimeout } from "@/util/timeout" +import { Instance } from "@/project/instance" import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network" import { Filesystem } from "@/util" import type { GlobalEvent } from "@opencode-ai/sdk/v2" @@ -178,7 +179,11 @@ export const TuiThreadCommand = cmd({ const prompt = await input(args.prompt) const config = await TuiConfig.get() - const network = resolveNetworkOptionsNoConfig(args) + const network = await Instance.provide({ + directory: cwd, + fn: () => resolveNetworkOptionsNoConfig(args), + }) + const external = process.argv.includes("--port") || process.argv.includes("--hostname") || From b1db69fdf76000312068b9322fa0fc6ff93014f2 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 16 Apr 2026 15:05:40 +0800 Subject: [PATCH 5/6] fix other commands --- packages/opencode/src/cli/cmd/serve.ts | 4 +++- packages/opencode/src/cli/cmd/web.ts | 3 ++- packages/opencode/src/project/bootstrap.ts | 22 ++++++++++++++-------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index d5eee75dd18e..ea2f717f7cc3 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -2,6 +2,7 @@ import { Server } from "../../server/server" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "../../flag/flag" +import { bootstrap } from "../bootstrap" export const ServeCommand = cmd({ command: "serve", @@ -11,7 +12,8 @@ export const ServeCommand = cmd({ if (!Flag.OPENCODE_SERVER_PASSWORD) { console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } - const opts = await resolveNetworkOptions(args) + + const opts = await bootstrap(process.cwd(), () => resolveNetworkOptions(args)) const server = await Server.listen(opts) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 9dd8796d6e94..30a4641fef2d 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -5,6 +5,7 @@ import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "../../flag/flag" import open from "open" import { networkInterfaces } from "os" +import { bootstrap } from "../bootstrap" function getNetworkIPs() { const nets = networkInterfaces() @@ -36,7 +37,7 @@ export const WebCommand = cmd({ if (!Flag.OPENCODE_SERVER_PASSWORD) { UI.println(UI.Style.TEXT_WARNING_BOLD + "! OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } - const opts = await resolveNetworkOptions(args) + const opts = await bootstrap(process.cwd(), () => resolveNetworkOptions(args)) const server = await Server.listen(opts) UI.empty() UI.println(UI.logo(" ")) diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 012b10ef8695..de13f162f7cd 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -7,23 +7,29 @@ import * as Vcs from "./vcs" import { Bus } from "../bus" import { Command } from "../command" import { Instance } from "./instance" +import { Plugin } from "../plugin" import { Log } from "@/util" import { FileWatcher } from "@/file/watcher" import { ShareNext } from "@/share" import * as Effect from "effect/Effect" +import { Config } from "@/config" export const InstanceBootstrap = Effect.gen(function* () { Log.Default.info("bootstrapping", { directory: Instance.directory }) yield* Effect.all( [ - LSP.Service, - ShareNext.Service, - Format.Service, - File.Service, - FileWatcher.Service, - Vcs.Service, - Snapshot.Service, - ].map((s) => Effect.forkDetach(s.use((i) => i.init()))), + Config.Service.use((i) => i.get()), + ...[ + Plugin.Service, + LSP.Service, + ShareNext.Service, + Format.Service, + File.Service, + FileWatcher.Service, + Vcs.Service, + Snapshot.Service, + ].map((s) => s.use((i) => i.init())), + ].map((e) => Effect.forkDetach(e)), ).pipe(Effect.withSpan("InstanceBootstrap.init")) yield* Bus.Service.use((svc) => From f280e7e69ce7ffb046af331afb3433822063460a Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:08:42 +1000 Subject: [PATCH 6/6] fix: defer MessageV2.Assistant.shape access to break circular dep in compiled binary --- packages/opencode/src/session/session.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 5168b80b563d..ba144da9f030 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -246,7 +246,8 @@ export const Event = { "session.error", z.object({ sessionID: SessionID.zod.optional(), - error: MessageV2.Assistant.shape.error, + // z.lazy defers access to break circular dep: session → message-v2 → provider → plugin → session + error: z.lazy(() => MessageV2.Assistant.shape.error), }), ), }