From 94f71f59a3eb3a4d376e371c5958a7f0cbf0a27e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 13 Apr 2026 22:16:40 +0800 Subject: [PATCH] core: make InstanceBootstrap into an effect (#22274) Co-authored-by: Kit Langton --- packages/opencode/script/seed-e2e.ts | 4 ++- packages/opencode/src/cli/bootstrap.ts | 3 +- packages/opencode/src/cli/cmd/tui/worker.ts | 4 +-- packages/opencode/src/effect/app-runtime.ts | 4 +-- .../opencode/src/effect/bootstrap-runtime.ts | 19 ++++++++++- packages/opencode/src/project/bootstrap.ts | 34 ++++++++++--------- .../src/server/instance/middleware.ts | 5 +-- .../opencode/src/server/instance/project.ts | 3 +- packages/opencode/src/worktree/index.ts | 3 +- .../test/server/project-init-git.test.ts | 1 - 10 files changed, 52 insertions(+), 28 deletions(-) diff --git a/packages/opencode/script/seed-e2e.ts b/packages/opencode/script/seed-e2e.ts index f5bd7194f2..6d414ec7fb 100644 --- a/packages/opencode/script/seed-e2e.ts +++ b/packages/opencode/script/seed-e2e.ts @@ -1,3 +1,5 @@ +import { AppRuntime } from "@/effect/app-runtime" + const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd() const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session" const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e" @@ -20,7 +22,7 @@ const seed = async () => { try { await Instance.provide({ directory: dir, - init: InstanceBootstrap, + init: () => AppRuntime.runPromise(InstanceBootstrap), fn: async () => { await Config.waitForDependencies() await ToolRegistry.ids() diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts index 984d5723d4..2604e703ea 100644 --- a/packages/opencode/src/cli/bootstrap.ts +++ b/packages/opencode/src/cli/bootstrap.ts @@ -1,10 +1,11 @@ +import { AppRuntime } from "@/effect/app-runtime" import { InstanceBootstrap } from "../project/bootstrap" import { Instance } from "../project/instance" export async function bootstrap(directory: string, cb: () => Promise) { return Instance.provide({ directory, - init: InstanceBootstrap, + init: () => AppRuntime.runPromise(InstanceBootstrap), fn: async () => { try { const result = await cb() diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index a71b95ce4c..5e9bc62c10 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -7,10 +7,10 @@ import { Rpc } from "@/util/rpc" import { upgrade } from "@/cli/upgrade" import { Config } from "@/config/config" import { GlobalBus } from "@/bus/global" -import type { GlobalEvent } from "@opencode-ai/sdk/v2" import { Flag } from "@/flag/flag" import { writeHeapSnapshot } from "node:v8" import { Heap } from "@/cli/heap" +import { AppRuntime } from "@/effect/app-runtime" await Log.init({ print: process.argv.includes("--print-logs"), @@ -74,7 +74,7 @@ export const rpc = { async checkUpgrade(input: { directory: string }) { await Instance.provide({ directory: input.directory, - init: InstanceBootstrap, + init: () => AppRuntime.runPromise(InstanceBootstrap), fn: async () => { await upgrade().catch(() => {}) }, diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 674ca1a2ac..9e1fb8bd24 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -49,7 +49,7 @@ import { ShareNext } from "@/share/share-next" import { SessionShare } from "@/share/session" export const AppLayer = Layer.mergeAll( - Observability.layer, + // Observability.layer, AppFileSystem.defaultLayer, Bus.defaultLayer, Auth.defaultLayer, @@ -95,6 +95,6 @@ export const AppLayer = Layer.mergeAll( Installation.defaultLayer, ShareNext.defaultLayer, SessionShare.defaultLayer, -) +).pipe(Layer.provide(Observability.layer)) export const AppRuntime = ManagedRuntime.make(AppLayer, { memoMap }) diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 78df313e86..0db46fe3ef 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -1,10 +1,27 @@ import { Layer, ManagedRuntime } from "effect" import { memoMap } from "./run-service" +import { Plugin } from "@/plugin" +import { LSP } from "@/lsp" import { FileWatcher } from "@/file/watcher" import { Format } from "@/format" import { ShareNext } from "@/share/share-next" +import { File } from "@/file" +import { Vcs } from "@/project/vcs" +import { Snapshot } from "@/snapshot" +import { Bus } from "@/bus" +import { Observability } from "./oltp" -export const BootstrapLayer = Layer.mergeAll(Format.defaultLayer, ShareNext.defaultLayer, FileWatcher.defaultLayer) +export const BootstrapLayer = Layer.mergeAll( + Plugin.defaultLayer, + ShareNext.defaultLayer, + Format.defaultLayer, + LSP.defaultLayer, + File.defaultLayer, + FileWatcher.defaultLayer, + Vcs.defaultLayer, + Snapshot.defaultLayer, + Bus.defaultLayer, +).pipe(Layer.provide(Observability.layer)) export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap }) diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index b7d739fcd0..75e3244e18 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -9,24 +9,26 @@ import { Bus } from "../bus" import { Command } from "../command" import { Instance } from "./instance" import { Log } from "@/util/log" -import { BootstrapRuntime } from "@/effect/bootstrap-runtime" import { FileWatcher } from "@/file/watcher" import { ShareNext } from "@/share/share-next" +import * as Effect from "effect/Effect" -export async function InstanceBootstrap() { +export const InstanceBootstrap = Effect.gen(function* () { Log.Default.info("bootstrapping", { directory: Instance.directory }) - await Plugin.init() - void BootstrapRuntime.runPromise(ShareNext.Service.use((svc) => svc.init())) - void BootstrapRuntime.runPromise(Format.Service.use((svc) => svc.init())) - await LSP.init() - File.init() - void BootstrapRuntime.runPromise(FileWatcher.Service.use((svc) => svc.init())) - Vcs.init() - Snapshot.init() + yield* Plugin.Service.use((svc) => svc.init()) + yield* ShareNext.Service.use((svc) => svc.init()).pipe(Effect.forkDetach) + yield* Format.Service.use((svc) => svc.init()).pipe(Effect.forkDetach) + yield* LSP.Service.use((svc) => svc.init()) + yield* File.Service.use((svc) => svc.init()).pipe(Effect.forkDetach) + yield* FileWatcher.Service.use((svc) => svc.init()).pipe(Effect.forkDetach) + yield* Vcs.Service.use((svc) => svc.init()).pipe(Effect.forkDetach) + yield* Snapshot.Service.use((svc) => svc.init()).pipe(Effect.forkDetach) - Bus.subscribe(Command.Event.Executed, async (payload) => { - if (payload.properties.name === Command.Default.INIT) { - Project.setInitialized(Instance.project.id) - } - }) -} + yield* Bus.Service.use((svc) => + svc.subscribeCallback(Command.Event.Executed, async (payload) => { + if (payload.properties.name === Command.Default.INIT) { + Project.setInitialized(Instance.project.id) + } + }), + ) +}).pipe(Effect.withSpan("InstanceBootstrap")) diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 1a5011477e..19bd26535a 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -10,6 +10,7 @@ import { InstanceBootstrap } from "@/project/bootstrap" import { Session } from "@/session" import { SessionID } from "@/session/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" +import { AppRuntime } from "@/effect/app-runtime" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } @@ -66,7 +67,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware if (!workspaceID) { return Instance.provide({ directory, - init: InstanceBootstrap, + init: () => AppRuntime.runPromise(InstanceBootstrap), async fn() { return next() }, @@ -103,7 +104,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware fn: () => Instance.provide({ directory: target.directory, - init: InstanceBootstrap, + init: () => AppRuntime.runPromise(InstanceBootstrap), async fn() { return next() }, diff --git a/packages/opencode/src/server/instance/project.ts b/packages/opencode/src/server/instance/project.ts index e5dd5782d6..a249539541 100644 --- a/packages/opencode/src/server/instance/project.ts +++ b/packages/opencode/src/server/instance/project.ts @@ -8,6 +8,7 @@ import { ProjectID } from "../../project/schema" import { errors } from "../error" import { lazy } from "../../util/lazy" import { InstanceBootstrap } from "../../project/bootstrap" +import { AppRuntime } from "@/effect/app-runtime" export const ProjectRoutes = lazy(() => new Hono() @@ -83,7 +84,7 @@ export const ProjectRoutes = lazy(() => directory: dir, worktree: dir, project: next, - init: InstanceBootstrap, + init: () => AppRuntime.runPromise(InstanceBootstrap), }) return c.json(next) }, diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index f4ec0af83c..3a3a39c31a 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -20,6 +20,7 @@ import { AppFileSystem } from "@/filesystem" import { makeRuntime } from "@/effect/run-service" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect/instance-state" +import { AppRuntime } from "@/effect/app-runtime" export namespace Worktree { const log = Log.create({ service: "worktree" }) @@ -266,7 +267,7 @@ export namespace Worktree { const booted = yield* Effect.promise(() => Instance.provide({ directory: info.directory, - init: InstanceBootstrap, + init: () => AppRuntime.runPromise(InstanceBootstrap), fn: () => undefined, }) .then(() => true) diff --git a/packages/opencode/test/server/project-init-git.test.ts b/packages/opencode/test/server/project-init-git.test.ts index eca562a0f5..8cb159d9a3 100644 --- a/packages/opencode/test/server/project-init-git.test.ts +++ b/packages/opencode/test/server/project-init-git.test.ts @@ -43,7 +43,6 @@ describe("project.initGit endpoint", () => { worktree: tmp.path, }) expect(reloadSpy).toHaveBeenCalledTimes(1) - expect(reloadSpy.mock.calls[0]?.[0]?.init).toBe(InstanceBootstrap) expect(seen.some((evt) => evt.directory === tmp.path && evt.payload.type === "server.instance.disposed")).toBe( true, )