Normalize instance lifecycle wiring (#25501)

This commit is contained in:
Kit Langton
2026-05-02 20:39:20 -04:00
committed by GitHub
parent a6464062b7
commit 7d91d3b1ed
71 changed files with 852 additions and 936 deletions

View File

@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import z from "zod"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { Project } from "@/project/project"
import { Session as SessionNs } from "@/session/session"
import * as Log from "@opencode-ai/core/util/log"
@@ -28,11 +29,11 @@ describe("session.listGlobal", () => {
await using first = await tmpdir({ git: true })
await using second = await tmpdir({ git: true })
const firstSession = await Instance.provide({
const firstSession = await WithInstance.provide({
directory: first.path,
fn: async () => svc.create({ title: "first-session" }),
})
const secondSession = await Instance.provide({
const secondSession = await WithInstance.provide({
directory: second.path,
fn: async () => svc.create({ title: "second-session" }),
})
@@ -58,12 +59,12 @@ describe("session.listGlobal", () => {
test("excludes archived sessions by default", async () => {
await using tmp = await tmpdir({ git: true })
const archived = await Instance.provide({
const archived = await WithInstance.provide({
directory: tmp.path,
fn: async () => svc.create({ title: "archived-session" }),
})
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => svc.setArchived({ sessionID: archived.id, time: Date.now() }),
})
@@ -82,12 +83,12 @@ describe("session.listGlobal", () => {
test("supports cursor pagination", async () => {
await using tmp = await tmpdir({ git: true })
const first = await Instance.provide({
const first = await WithInstance.provide({
directory: tmp.path,
fn: async () => svc.create({ title: "page-one" }),
})
await new Promise((resolve) => setTimeout(resolve, 5))
const second = await Instance.provide({
const second = await WithInstance.provide({
directory: tmp.path,
fn: async () => svc.create({ title: "page-two" }),
})

View File

@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { Server } from "../../src/server/server"
import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental"
import { Session } from "@/session/session"
@@ -126,12 +127,12 @@ describe("experimental HttpApi", () => {
test("serves global session list through Hono bridge", async () => {
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const first = await Instance.provide({
const first = await WithInstance.provide({
directory: tmp.path,
fn: async () => createSession({ title: "page-one" }),
})
await new Promise((resolve) => setTimeout(resolve, 5))
const second = await Instance.provide({
const second = await WithInstance.provide({
directory: tmp.path,
fn: async () => createSession({ title: "page-two" }),
})

View File

@@ -10,8 +10,8 @@ import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { Instance } from "../../src/project/instance"
import { InstanceLayer } from "../../src/project/instance-layer"
import { Project } from "../../src/project/project"
import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle"
import { instanceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/instance-context"
@@ -41,7 +41,7 @@ const it = testEffect(
testStateLayer,
NodeHttpServer.layerTest,
NodeServices.layer,
InstanceRuntime.layer,
InstanceLayer.layer,
Project.defaultLayer,
Workspace.defaultLayer,
),

View File

@@ -5,6 +5,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
import { McpPaths } from "../../src/server/routes/instance/httpapi/groups/mcp"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { Server } from "../../src/server/server"
import * as Log from "@opencode-ai/core/util/log"
@@ -59,7 +60,7 @@ function withMcpProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>)
)
yield* Effect.addFinalizer(() =>
Effect.promise(() =>
Instance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
WithInstance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
).pipe(Effect.ignore),
)

View File

@@ -3,6 +3,7 @@ import { Effect, FileSystem, Layer, Path } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { Server } from "../../src/server/server"
import * as Log from "@opencode-ai/core/util/log"
@@ -91,7 +92,7 @@ function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E,
yield* writeProviderAuthPlugin(dir)
yield* Effect.addFinalizer(() =>
Effect.promise(() =>
Instance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
WithInstance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
).pipe(Effect.ignore),
)

View File

@@ -5,6 +5,7 @@ import { HttpRouter } from "effect/unstable/http"
import { Flag } from "@opencode-ai/core/flag/flag"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
import { Server } from "../../src/server/server"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
@@ -226,7 +227,7 @@ function seedMessage(directory: string, sessionID: string) {
const id = SessionID.make(sessionID)
return call(
async () =>
await Instance.provide({
await WithInstance.provide({
directory,
fn: () =>
Effect.runPromise(

View File

@@ -9,6 +9,7 @@ import { Workspace } from "../../src/control-plane/workspace"
import { PermissionID } from "../../src/permission/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { Project } from "../../src/project/project"
import { Server } from "../../src/server/server"
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
@@ -44,7 +45,7 @@ function pathFor(path: string, params: Record<string, string>) {
function createSession(directory: string, input?: Session.CreateInput) {
return Effect.promise(
async () =>
await Instance.provide({
await WithInstance.provide({
directory,
fn: () => runSession(Session.Service.use((svc) => svc.create(input))),
}),
@@ -54,7 +55,7 @@ function createSession(directory: string, input?: Session.CreateInput) {
function createTextMessage(directory: string, sessionID: SessionID, text: string) {
return Effect.promise(
async () =>
await Instance.provide({
await WithInstance.provide({
directory,
fn: () =>
runSession(

View File

@@ -2,6 +2,7 @@ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
import { Effect } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { Server } from "../../src/server/server"
import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync"
import { Session } from "@/session/session"
@@ -38,7 +39,7 @@ describe("sync HttpApi", () => {
const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
const info = spyOn(Log.create({ service: "server.sync" }), "info")
const session = await Instance.provide({
const session = await WithInstance.provide({
directory: tmp.path,
fn: async () => runSession(Session.Service.use((svc) => svc.create({ title: "sync" }))),
})

View File

@@ -1,6 +1,7 @@
import { afterEach, describe, expect, mock, test } from "bun:test"
import { Effect } from "effect"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { Server } from "../../src/server/server"
import { Session as SessionNs } from "@/session/session"
import type { SessionID } from "../../src/session/schema"
@@ -31,7 +32,7 @@ afterEach(async () => {
describe("session action routes", () => {
test("abort route returns success", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const session = await svc.create({})

View File

@@ -1,6 +1,7 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { Session as SessionNs } from "@/session/session"
import * as Log from "@opencode-ai/core/util/log"
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
@@ -40,20 +41,20 @@ describe("session.list", () => {
await mkdir(path.join(tmp.path, "packages", "opencode"), { recursive: true })
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const root = await svc.create({ title: "root" })
const parent = await Instance.provide({
const parent = await WithInstance.provide({
directory: path.join(tmp.path, "packages"),
fn: async () => svc.create({ title: "parent" }),
})
const current = await Instance.provide({
const current = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "opencode"),
fn: async () => svc.create({ title: "current" }),
})
const sibling = await Instance.provide({
const sibling = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "app"),
fn: async () => svc.create({ title: "sibling" }),
})
@@ -73,20 +74,20 @@ describe("session.list", () => {
await mkdir(path.join(tmp.path, "packages", "opencode"), { recursive: true })
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const root = await svc.create({ title: "root" })
const parent = await Instance.provide({
const parent = await WithInstance.provide({
directory: path.join(tmp.path, "packages"),
fn: async () => svc.create({ title: "parent" }),
})
const current = await Instance.provide({
const current = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "opencode"),
fn: async () => svc.create({ title: "current" }),
})
const sibling = await Instance.provide({
const sibling = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "app"),
fn: async () => svc.create({ title: "sibling" }),
})
@@ -106,22 +107,22 @@ describe("session.list", () => {
await mkdir(path.join(tmp.path, "packages", "opencode", "src", "deep"), { recursive: true })
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const parent = await Instance.provide({
const parent = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "opencode"),
fn: async () => svc.create({ title: "parent" }),
})
const current = await Instance.provide({
const current = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "opencode", "src"),
fn: async () => svc.create({ title: "current" }),
})
const deeper = await Instance.provide({
const deeper = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "opencode", "src", "deep"),
fn: async () => svc.create({ title: "deeper" }),
})
const sibling = await Instance.provide({
const sibling = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "app"),
fn: async () => svc.create({ title: "sibling" }),
})
@@ -146,14 +147,14 @@ describe("session.list", () => {
await mkdir(path.join(tmp.path, "packages", "opencode", "src"), { recursive: true })
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const current = await Instance.provide({
const current = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "opencode", "src"),
fn: async () => svc.create({ title: "legacy-current" }),
})
const sibling = await Instance.provide({
const sibling = await WithInstance.provide({
directory: path.join(tmp.path, "packages", "app"),
fn: async () => svc.create({ title: "legacy-sibling" }),
})
@@ -175,7 +176,7 @@ describe("session.list", () => {
test("filters root sessions", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const root = await svc.create({ title: "root-session" })
@@ -192,7 +193,7 @@ describe("session.list", () => {
test("filters by start time", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
await svc.create({ title: "new-session" })
@@ -206,7 +207,7 @@ describe("session.list", () => {
test("filters by search term", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
await svc.create({ title: "unique-search-term-abc" })
@@ -223,7 +224,7 @@ describe("session.list", () => {
test("respects limit parameter", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
await svc.create({ title: "session-1" })

View File

@@ -1,6 +1,7 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { Server } from "../../src/server/server"
import { Session as SessionNs } from "@/session/session"
import { MessageV2 } from "../../src/session/message-v2"
@@ -76,7 +77,7 @@ describe("session messages endpoint", () => {
test("returns cursor headers for older pages", async () => {
await using tmp = await tmpdir({ git: true })
await withoutWatcher(() =>
Instance.provide({
WithInstance.provide({
directory: tmp.path,
fn: async () => {
const session = await svc.create({})
@@ -105,7 +106,7 @@ describe("session messages endpoint", () => {
test("keeps full-history responses when limit is omitted", async () => {
await using tmp = await tmpdir({ git: true })
await withoutWatcher(() =>
Instance.provide({
WithInstance.provide({
directory: tmp.path,
fn: async () => {
const session = await svc.create({})
@@ -126,7 +127,7 @@ describe("session messages endpoint", () => {
test("rejects invalid cursors and missing sessions", async () => {
await using tmp = await tmpdir({ git: true })
await withoutWatcher(() =>
Instance.provide({
WithInstance.provide({
directory: tmp.path,
fn: async () => {
const session = await svc.create({})
@@ -147,7 +148,7 @@ describe("session messages endpoint", () => {
test("does not truncate large legacy limit requests", async () => {
await using tmp = await tmpdir({ git: true })
await withoutWatcher(() =>
Instance.provide({
WithInstance.provide({
directory: tmp.path,
fn: async () => {
const session = await svc.create({})

View File

@@ -4,6 +4,7 @@ import { Session as SessionNs } from "@/session/session"
import type { SessionID } from "../../src/session/schema"
import * as Log from "@opencode-ai/core/util/log"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { Server } from "../../src/server/server"
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
@@ -30,7 +31,7 @@ afterEach(async () => {
describe("tui.selectSession endpoint", () => {
test("should return 200 when called with valid session", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
// #given
@@ -56,7 +57,7 @@ describe("tui.selectSession endpoint", () => {
test("should return 404 when session does not exist", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
// #given
@@ -78,7 +79,7 @@ describe("tui.selectSession endpoint", () => {
test("should return 400 when session ID format is invalid", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
// #given