fix(instance): run bootstrap from instance store (#25475)

This commit is contained in:
Kit Langton
2026-05-02 19:33:38 -04:00
committed by GitHub
parent 36007aecf4
commit f98053c34e
42 changed files with 540 additions and 249 deletions

View File

@@ -0,0 +1,37 @@
import { GlobalBus } from "@/bus/global"
import { InstanceStore } from "@/project/instance-store"
import * as Log from "@opencode-ai/core/util/log"
import { Effect } from "effect"
import { Event } from "./event"
const log = Log.create({ service: "server" })
export const emitGlobalDisposed = Effect.sync(() =>
GlobalBus.emit("event", {
directory: "global",
payload: {
type: Event.Disposed.type,
properties: {},
},
}),
)
export const disposeAllInstancesAndEmitGlobalDisposed = Effect.fn(
"Server.disposeAllInstancesAndEmitGlobalDisposed",
)(function* (options?: { swallowErrors?: boolean }) {
const store = yield* InstanceStore.Service
yield* Effect.gen(function* () {
yield* (options?.swallowErrors
? store.disposeAll().pipe(
Effect.catchCause((cause) =>
Effect.sync(() => {
log.warn("global disposal failed", { cause })
}),
),
)
: store.disposeAll())
yield* emitGlobalDisposed
}).pipe(Effect.uninterruptible)
})
export * as GlobalLifecycle from "./global-lifecycle"

View File

@@ -1,25 +1,23 @@
import { Hono, type Context } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import { streamSSE } from "hono/streaming"
import { Effect, Schema } from "effect"
import { Effect } from "effect"
import z from "zod"
import { BusEvent } from "@/bus/bus-event"
import { SyncEvent } from "@/sync"
import { GlobalBus } from "@/bus/global"
import { AppRuntime } from "@/effect/app-runtime"
import { AsyncQueue } from "@/util/queue"
import { InstanceStore } from "../../project/instance-store"
import { Installation } from "@/installation"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import * as Log from "@opencode-ai/core/util/log"
import { lazy } from "../../util/lazy"
import { Config } from "@/config/config"
import { errors } from "../error"
import { disposeAllInstancesAndEmitGlobalDisposed } from "../global-lifecycle"
const log = Log.create({ service: "server" })
export const GlobalDisposedEvent = BusEvent.define("global.disposed", Schema.Struct({}))
async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>) => () => void) {
return streamSSE(c, async (stream) => {
const q = new AsyncQueue<string | null>()
@@ -178,8 +176,13 @@ export const GlobalRoutes = lazy(() =>
validator("json", Config.Info.zod),
async (c) => {
const config = c.req.valid("json")
const next = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.updateGlobal(config)))
return c.json(next)
const result = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.updateGlobal(config)))
if (result.changed) {
void AppRuntime.runPromise(disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true })).catch(
() => undefined,
)
}
return c.json(result.info)
},
)
.post(
@@ -200,14 +203,7 @@ export const GlobalRoutes = lazy(() =>
},
}),
async (c) => {
await InstanceStore.disposeAllInstances()
GlobalBus.emit("event", {
directory: "global",
payload: {
type: GlobalDisposedEvent.type,
properties: {},
},
})
await AppRuntime.runPromise(disposeAllInstancesAndEmitGlobalDisposed())
return c.json(true)
},
)

View File

@@ -1,7 +1,8 @@
import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { Config } from "@/config/config"
import { InstanceState } from "@/effect/instance-state"
import { InstanceStore } from "@/project/instance-store"
import { Provider } from "@/provider/provider"
import { errors } from "../../error"
import { lazy } from "@/util/lazy"
@@ -55,7 +56,9 @@ export const ConfigRoutes = lazy(() =>
jsonRequest("ConfigRoutes.update", c, function* () {
const config = c.req.valid("json")
const cfg = yield* Config.Service
const store = yield* InstanceStore.Service
yield* cfg.update(config)
yield* store.dispose(yield* InstanceState.context)
return config
}),
)

View File

@@ -1,6 +1,7 @@
import { Config } from "@/config/config"
import { BusEvent } from "@/bus/bus-event"
import { SyncEvent } from "@/sync"
import "@/server/event"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { described } from "./metadata"

View File

@@ -16,7 +16,7 @@ export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (h
})
const update = Effect.fn("ConfigHttpApi.update")(function* (ctx) {
yield* configSvc.update(ctx.payload, { dispose: false })
yield* configSvc.update(ctx.payload)
yield* markInstanceForDisposal(yield* InstanceState.context)
return ctx.payload
})

View File

@@ -1,7 +1,8 @@
import { Config } from "@/config/config"
import { GlobalBus, type GlobalEvent as GlobalBusEvent } from "@/bus/global"
import { EffectBridge } from "@/effect/bridge"
import { Installation } from "@/installation"
import { InstanceStore } from "@/project/instance-store"
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import * as Log from "@opencode-ai/core/util/log"
import { Effect, Queue, Schema } from "effect"
@@ -68,7 +69,7 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl
Effect.gen(function* () {
const config = yield* Config.Service
const installation = yield* Installation.Service
const store = yield* InstanceStore.Service
const bridge = yield* EffectBridge.make()
const health = Effect.fn("GlobalHttpApi.health")(function* () {
return { healthy: true as const, version: InstallationVersion }
@@ -83,15 +84,13 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl
})
const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
return yield* config.updateGlobal(ctx.payload)
const result = yield* config.updateGlobal(ctx.payload)
if (result.changed) bridge.fork(disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true }))
return result.info
})
const dispose = Effect.fn("GlobalHttpApi.dispose")(function* () {
yield* store.disposeAll()
GlobalBus.emit("event", {
directory: "global",
payload: { type: "global.disposed", properties: {} },
})
yield* disposeAllInstancesAndEmitGlobalDisposed()
return true
})

View File

@@ -1,5 +1,4 @@
import { WorkspaceRef } from "@/effect/instance-ref"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
@@ -24,12 +23,11 @@ function decode(input: string): string {
function provideInstanceContext<E>(
effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
store: InstanceStore.Interface,
bootstrap: InstanceBootstrap.Interface,
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
return Effect.gen(function* () {
const route = yield* WorkspaceRouteContext
return yield* store.provide(
{ directory: decode(route.directory), init: bootstrap.run },
{ directory: decode(route.directory) },
effect.pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
)
})
@@ -39,15 +37,13 @@ export const instanceContextLayer = Layer.effect(
InstanceContextMiddleware,
Effect.gen(function* () {
const store = yield* InstanceStore.Service
const bootstrap = yield* InstanceBootstrap.Service
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store, bootstrap))
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store))
}),
)
export const instanceRouterMiddleware = HttpRouter.middleware()(
Effect.gen(function* () {
const store = yield* InstanceStore.Service
const bootstrap = yield* InstanceBootstrap.Service
return (effect) => provideInstanceContext(effect, store, bootstrap)
return (effect) => provideInstanceContext(effect, store)
}),
)

View File

@@ -18,8 +18,7 @@ import { LSP } from "@/lsp/lsp"
import { MCP } from "@/mcp"
import { Permission } from "@/permission"
import { Installation } from "@/installation"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { InstanceRuntime } from "@/project/instance-runtime"
import { Plugin } from "@/plugin"
import { Project } from "@/project/project"
import { ProviderAuth } from "@/provider/auth"
@@ -153,8 +152,7 @@ export function createRoutes(corsOptions?: CorsOptions) {
Format.defaultLayer,
LSP.defaultLayer,
Installation.defaultLayer,
InstanceBootstrap.defaultLayer,
InstanceStore.defaultLayer,
InstanceRuntime.layer,
MCP.defaultLayer,
ModelsDev.defaultLayer,
Permission.defaultLayer,

View File

@@ -6,7 +6,7 @@ import z from "zod"
import { Format } from "@/format"
import { TuiRoutes } from "./tui"
import { Instance } from "@/project/instance"
import { InstanceStore } from "@/project/instance-store"
import { InstanceRuntime } from "@/project/instance-runtime"
import { Vcs } from "@/project/vcs"
import { Agent } from "@/agent/agent"
import { Skill } from "@/skill"
@@ -25,7 +25,6 @@ import { ExperimentalRoutes } from "./experimental"
import { ProviderRoutes } from "./provider"
import { EventRoutes } from "./event"
import { SyncRoutes } from "./sync"
import { InstanceMiddleware } from "./middleware"
import { jsonRequest } from "./trace"
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
@@ -63,7 +62,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
},
}),
async (c) => {
await InstanceStore.disposeInstance(Instance.current)
await InstanceRuntime.disposeInstance(Instance.current)
return c.json(true)
},
)

View File

@@ -1,6 +1,5 @@
import type { MiddlewareHandler } from "hono"
import { Instance } from "@/project/instance"
import { getBootstrapRunEffect } from "@/effect/app-runtime"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { WorkspaceContext } from "@/control-plane/workspace-context"
import { WorkspaceID } from "@/control-plane/schema"
@@ -23,7 +22,6 @@ export function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler
async fn() {
return Instance.provide({
directory,
init: await getBootstrapRunEffect(),
async fn() {
return next()
},

View File

@@ -2,13 +2,12 @@ import { Hono } from "hono"
import { describeRoute, validator } from "hono-openapi"
import { resolver } from "hono-openapi"
import { Instance } from "@/project/instance"
import { InstanceStore } from "@/project/instance-store"
import { InstanceRuntime } from "@/project/instance-runtime"
import { Project } from "@/project/project"
import z from "zod"
import { ProjectID } from "@/project/schema"
import { errors } from "../../error"
import { lazy } from "@/util/lazy"
import { getBootstrapRunEffect } from "@/effect/app-runtime"
import { jsonRequest, runRequest } from "./trace"
export const ProjectRoutes = lazy(() =>
@@ -82,12 +81,7 @@ export const ProjectRoutes = lazy(() =>
Project.Service.use((svc) => svc.initGit({ directory: dir, project: prev })),
)
if (next.id === prev.id && next.vcs === prev.vcs && next.worktree === prev.worktree) return c.json(next)
await InstanceStore.reloadInstance({
directory: dir,
worktree: dir,
project: next,
init: await getBootstrapRunEffect(),
})
await InstanceRuntime.reloadInstance({ directory: dir, worktree: dir, project: next })
return c.json(next)
},
)

View File

@@ -5,7 +5,7 @@ import { WorkspaceID } from "@/control-plane/schema"
import { WorkspaceContext } from "@/control-plane/workspace-context"
import { Workspace } from "@/control-plane/workspace"
import { Flag } from "@opencode-ai/core/flag/flag"
import { getBootstrapRunEffect, AppRuntime } from "@/effect/app-runtime"
import { AppRuntime } from "@/effect/app-runtime"
import { Instance } from "@/project/instance"
import { Session } from "@/session/session"
import { SessionID } from "@/session/schema"
@@ -94,13 +94,11 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
const target = await adapter.target(workspace)
if (target.type === "local") {
const init = await getBootstrapRunEffect()
return WorkspaceContext.provide({
workspaceID: WorkspaceID.make(workspaceID),
fn: () =>
Instance.provide({
directory: target.directory,
init,
async fn() {
return next()
},