refactor(instance): remove legacy runtime fallback (#27757)
This commit is contained in:
@@ -43,16 +43,25 @@ import { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "../provider/schema"
|
||||
import { Agent as AgentModule } from "../agent/agent"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { InstanceRuntime } from "@/project/instance-runtime"
|
||||
import { Installation } from "@/installation"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { Config } from "@/config/config"
|
||||
import { ConfigMCP } from "@/config/mcp"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { Result, Schema } from "effect"
|
||||
import { Effect, Result, Schema } from "effect"
|
||||
import { LoadAPIKeyError } from "ai"
|
||||
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { applyPatch } from "diff"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
|
||||
const defaultAgentInfo = async (directory: string) => {
|
||||
const ctx = await InstanceRuntime.load({ directory })
|
||||
return AppRuntime.runPromise(
|
||||
AgentModule.Service.use((svc) => svc.defaultInfo()).pipe(Effect.provideService(InstanceRef, ctx)),
|
||||
)
|
||||
}
|
||||
import { ShellID } from "@/tool/shell/id"
|
||||
|
||||
type ModeOption = { id: string; name: string; description?: string }
|
||||
@@ -1094,7 +1103,7 @@ export class Agent implements ACPAgent {
|
||||
|
||||
const currentModeId = await (async () => {
|
||||
if (!availableModes.length) return undefined
|
||||
const defaultAgent = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultInfo()))
|
||||
const defaultAgent = await defaultAgentInfo(directory)
|
||||
const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgent.name)?.id ?? availableModes[0].id
|
||||
this.sessionManager.setMode(sessionId, resolvedModeId)
|
||||
return resolvedModeId
|
||||
@@ -1328,8 +1337,7 @@ export class Agent implements ACPAgent {
|
||||
if (!current) {
|
||||
this.sessionManager.setModel(session.id, model)
|
||||
}
|
||||
const agent =
|
||||
session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultInfo()))).name
|
||||
const agent = session.modeId ?? (await defaultAgentInfo(directory)).name
|
||||
|
||||
const parts: Array<
|
||||
| { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Effect, Schema } from "effect"
|
||||
import { AppRuntime, type AppServices } from "@/effect/app-runtime"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { cmd, type WithDoubleDash } from "./cmd/cmd"
|
||||
|
||||
/**
|
||||
@@ -83,19 +82,11 @@ export const effectCmd = <Args, A>(opts: EffectCmdOpts<Args, A>) =>
|
||||
return
|
||||
}
|
||||
const directory = opts.directory?.(args) ?? process.cwd()
|
||||
// Two-phase: load ctx, then run body inside Instance.current ALS.
|
||||
// Effect's InstanceRef is provided via fiber context, but that context is
|
||||
// lost across `await` inside `Effect.promise(async () => ...)` callbacks
|
||||
// — when handlers re-enter Effect via `AppRuntime.runPromise(svc.method())`
|
||||
// there, attach() falls back to Instance.current ALS, which Node preserves
|
||||
// across awaits. Matches the pre-effectCmd `bootstrap()` behavior.
|
||||
const { store, ctx } = await AppRuntime.runPromise(
|
||||
InstanceStore.Service.use((store) => store.load({ directory }).pipe(Effect.map((ctx) => ({ store, ctx })))),
|
||||
)
|
||||
try {
|
||||
await Instance.restore(ctx, () =>
|
||||
AppRuntime.runPromise(opts.handler(args).pipe(Effect.provideService(InstanceRef, ctx))),
|
||||
)
|
||||
await AppRuntime.runPromise(opts.handler(args).pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
} finally {
|
||||
await AppRuntime.runPromise(store.dispose(ctx))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { WorkspaceContext } from "../workspace-context"
|
||||
import { type WorkspaceAdapter, WorkspaceInfo } from "../types"
|
||||
import { type WorkspaceAdapter, type WorkspaceAdapterContext, WorkspaceInfo } from "../types"
|
||||
|
||||
const WorktreeConfig = Schema.Struct({
|
||||
name: WorkspaceInfo.fields.name,
|
||||
@@ -11,26 +10,31 @@ const WorktreeConfig = Schema.Struct({
|
||||
const decodeWorktreeConfig = Schema.decodeUnknownSync(WorktreeConfig)
|
||||
|
||||
async function loadWorktree() {
|
||||
const [{ AppRuntime }, { Instance }, { Worktree }] = await Promise.all([
|
||||
const [{ AppRuntime }, { Worktree }] = await Promise.all([
|
||||
import("@/effect/app-runtime"),
|
||||
import("@/project/instance"),
|
||||
import("@/worktree"),
|
||||
])
|
||||
return { AppRuntime, Instance, Worktree }
|
||||
return { AppRuntime, Worktree }
|
||||
}
|
||||
|
||||
function requireInstance(context: WorkspaceAdapterContext | undefined) {
|
||||
if (!context?.instance) throw new Error("Worktree adapter requires an instance context")
|
||||
return context.instance
|
||||
}
|
||||
|
||||
const provideContext = <A, E, R>(effect: Effect.Effect<A, E, R>, context: WorkspaceAdapterContext | undefined) =>
|
||||
effect.pipe(
|
||||
Effect.provideService(InstanceRef, requireInstance(context)),
|
||||
Effect.provideService(WorkspaceRef, context?.workspaceID),
|
||||
)
|
||||
|
||||
export const WorktreeAdapter: WorkspaceAdapter = {
|
||||
name: "Worktree",
|
||||
description: "Create a git worktree",
|
||||
async configure(info) {
|
||||
const { AppRuntime, Instance, Worktree } = await loadWorktree()
|
||||
const ctx = Instance.current
|
||||
const workspaceID = WorkspaceContext.workspaceID
|
||||
async configure(info, context) {
|
||||
const { AppRuntime, Worktree } = await loadWorktree()
|
||||
const next = await AppRuntime.runPromise(
|
||||
Worktree.Service.use((svc) => svc.makeWorktreeInfo({ detached: true })).pipe(
|
||||
Effect.provideService(InstanceRef, ctx),
|
||||
Effect.provideService(WorkspaceRef, workspaceID),
|
||||
),
|
||||
provideContext(Worktree.Service.use((svc) => svc.makeWorktreeInfo({ detached: true })), context),
|
||||
)
|
||||
return {
|
||||
...info,
|
||||
@@ -38,32 +42,27 @@ export const WorktreeAdapter: WorkspaceAdapter = {
|
||||
directory: next.directory,
|
||||
}
|
||||
},
|
||||
async create(info) {
|
||||
const { AppRuntime, Instance, Worktree } = await loadWorktree()
|
||||
const ctx = Instance.current
|
||||
const workspaceID = WorkspaceContext.workspaceID
|
||||
async create(info, _env, _from, context) {
|
||||
const { AppRuntime, Worktree } = await loadWorktree()
|
||||
const config = decodeWorktreeConfig(info)
|
||||
await AppRuntime.runPromise(
|
||||
Worktree.Service.use((svc) =>
|
||||
svc.createFromInfo({
|
||||
name: config.name,
|
||||
directory: config.directory,
|
||||
...(config.branch ? { branch: config.branch } : {}),
|
||||
}),
|
||||
).pipe(Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, workspaceID)),
|
||||
provideContext(
|
||||
Worktree.Service.use((svc) =>
|
||||
svc.createFromInfo({
|
||||
name: config.name,
|
||||
directory: config.directory,
|
||||
...(config.branch ? { branch: config.branch } : {}),
|
||||
}),
|
||||
),
|
||||
context,
|
||||
),
|
||||
)
|
||||
},
|
||||
async list() {
|
||||
const { AppRuntime, Instance, Worktree } = await loadWorktree()
|
||||
const ctx = Instance.current
|
||||
const workspaceID = WorkspaceContext.workspaceID
|
||||
async list(context) {
|
||||
const { AppRuntime, Worktree } = await loadWorktree()
|
||||
const ctx = requireInstance(context)
|
||||
return (
|
||||
await AppRuntime.runPromise(
|
||||
Worktree.Service.use((svc) => svc.list()).pipe(
|
||||
Effect.provideService(InstanceRef, ctx),
|
||||
Effect.provideService(WorkspaceRef, workspaceID),
|
||||
),
|
||||
)
|
||||
await AppRuntime.runPromise(provideContext(Worktree.Service.use((svc) => svc.list()), context))
|
||||
).map((info) => ({
|
||||
type: "worktree",
|
||||
name: info.name,
|
||||
@@ -72,16 +71,11 @@ export const WorktreeAdapter: WorkspaceAdapter = {
|
||||
projectID: ctx.project.id,
|
||||
}))
|
||||
},
|
||||
async remove(info) {
|
||||
const { AppRuntime, Instance, Worktree } = await loadWorktree()
|
||||
const ctx = Instance.current
|
||||
const workspaceID = WorkspaceContext.workspaceID
|
||||
async remove(info, context) {
|
||||
const { AppRuntime, Worktree } = await loadWorktree()
|
||||
const config = decodeWorktreeConfig(info)
|
||||
await AppRuntime.runPromise(
|
||||
Worktree.Service.use((svc) => svc.remove({ directory: config.directory })).pipe(
|
||||
Effect.provideService(InstanceRef, ctx),
|
||||
Effect.provideService(WorkspaceRef, workspaceID),
|
||||
),
|
||||
provideContext(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })), context),
|
||||
)
|
||||
},
|
||||
target(info) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Schema, Struct } from "effect"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import type { DeepMutable } from "@opencode-ai/core/schema"
|
||||
|
||||
@@ -37,12 +38,22 @@ export type Target =
|
||||
headers?: HeadersInit
|
||||
}
|
||||
|
||||
export type WorkspaceAdapterContext = {
|
||||
readonly instance?: InstanceContext
|
||||
readonly workspaceID?: WorkspaceID
|
||||
}
|
||||
|
||||
export type WorkspaceAdapter = {
|
||||
name: string
|
||||
description: string
|
||||
configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
|
||||
create(info: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo): Promise<void>
|
||||
list?(): WorkspaceListedInfo[] | Promise<WorkspaceListedInfo[]>
|
||||
remove(info: WorkspaceInfo): Promise<void>
|
||||
target(info: WorkspaceInfo): Target | Promise<Target>
|
||||
configure(info: WorkspaceInfo, context?: WorkspaceAdapterContext): WorkspaceInfo | Promise<WorkspaceInfo>
|
||||
create(
|
||||
info: WorkspaceInfo,
|
||||
env: Record<string, string | undefined>,
|
||||
from?: WorkspaceInfo,
|
||||
context?: WorkspaceAdapterContext,
|
||||
): Promise<void>
|
||||
list?(context?: WorkspaceAdapterContext): WorkspaceListedInfo[] | Promise<WorkspaceListedInfo[]>
|
||||
remove(info: WorkspaceInfo, context?: WorkspaceAdapterContext): Promise<void>
|
||||
target(info: WorkspaceInfo, context?: WorkspaceAdapterContext): Target | Promise<Target>
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ import { SessionID } from "@/session/schema"
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
import { errorData } from "@/util/error"
|
||||
import { waitEvent } from "./util"
|
||||
import { WorkspaceContext } from "./workspace-context"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
@@ -196,6 +196,50 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
|
||||
const adapterContext = Effect.gen(function* () {
|
||||
return {
|
||||
instance: yield* InstanceRef,
|
||||
workspaceID: yield* WorkspaceRef,
|
||||
}
|
||||
})
|
||||
|
||||
const adapterTarget = (workspace: Info) =>
|
||||
Effect.gen(function* () {
|
||||
const adapter = getAdapter(workspace.projectID, workspace.type)
|
||||
const context = yield* adapterContext
|
||||
return yield* EffectBridge.fromPromise(() => adapter.target(workspace, context))
|
||||
})
|
||||
|
||||
const adapterConfigure = (adapter: ReturnType<typeof getAdapter>, info: WorkspaceInfo) =>
|
||||
Effect.gen(function* () {
|
||||
const context = yield* adapterContext
|
||||
return yield* EffectBridge.fromPromise(() => adapter.configure(info, context))
|
||||
})
|
||||
|
||||
const adapterCreate = (
|
||||
adapter: ReturnType<typeof getAdapter>,
|
||||
info: WorkspaceInfo,
|
||||
env: Record<string, string | undefined>,
|
||||
from?: WorkspaceInfo,
|
||||
) =>
|
||||
Effect.gen(function* () {
|
||||
const context = yield* adapterContext
|
||||
return yield* EffectBridge.fromPromise(() => adapter.create(info, env, from, context))
|
||||
})
|
||||
|
||||
const adapterList = (adapter: ReturnType<typeof getAdapter>) =>
|
||||
Effect.gen(function* () {
|
||||
const context = yield* adapterContext
|
||||
return yield* EffectBridge.fromPromise(() => Promise.resolve(adapter.list?.(context) ?? []))
|
||||
})
|
||||
|
||||
const adapterRemove = (info: Info, type: string) =>
|
||||
Effect.gen(function* () {
|
||||
const adapter = getAdapter(info.projectID, type)
|
||||
const context = yield* adapterContext
|
||||
return yield* EffectBridge.fromPromise(() => adapter.remove(info, context))
|
||||
})
|
||||
|
||||
const connectSSE = Effect.fn("Workspace.connectSSE")(function* (
|
||||
url: URL | string,
|
||||
headers: HeadersInit | undefined,
|
||||
@@ -281,8 +325,7 @@ export const layer = Layer.effect(
|
||||
const workspace = yield* get(input.workspaceID)
|
||||
if (!workspace) return input.fallback
|
||||
|
||||
const adapter = getAdapter(workspace.projectID, workspace.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(workspace))
|
||||
const target = yield* adapterTarget(workspace)
|
||||
|
||||
if (target.type === "local") {
|
||||
const store = yield* InstanceStore.Service
|
||||
@@ -375,35 +418,27 @@ export const layer = Layer.effect(
|
||||
events: events.length,
|
||||
})
|
||||
|
||||
yield* Effect.promise(async () => {
|
||||
await WorkspaceContext.provide({
|
||||
workspaceID: space.id,
|
||||
async fn() {
|
||||
await Effect.runPromise(
|
||||
Effect.forEach(
|
||||
events,
|
||||
(event) =>
|
||||
sync.replay(
|
||||
{
|
||||
id: event.id,
|
||||
aggregateID: event.aggregate_id,
|
||||
seq: event.seq,
|
||||
type: event.type,
|
||||
data: event.data,
|
||||
},
|
||||
{ publish: true },
|
||||
),
|
||||
{ discard: true },
|
||||
),
|
||||
yield* Effect.forEach(
|
||||
events,
|
||||
(event) =>
|
||||
sync
|
||||
.replay(
|
||||
{
|
||||
id: event.id,
|
||||
aggregateID: event.aggregate_id,
|
||||
seq: event.seq,
|
||||
type: event.type,
|
||||
data: event.data,
|
||||
},
|
||||
{ publish: true },
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
.pipe(Effect.provideService(WorkspaceRef, space.id)),
|
||||
{ discard: true },
|
||||
)
|
||||
})
|
||||
|
||||
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
|
||||
const adapter = getAdapter(space.projectID, space.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||
const target = yield* adapterTarget(space)
|
||||
|
||||
if (target.type === "local") return
|
||||
|
||||
@@ -486,8 +521,7 @@ export const layer = Layer.effect(
|
||||
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
|
||||
if (!flags.experimentalWorkspaces) return
|
||||
|
||||
const adapter = getAdapter(space.projectID, space.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(space)).pipe(
|
||||
const target = yield* adapterTarget(space).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
setStatus(space.id, "error")
|
||||
@@ -538,15 +572,13 @@ export const layer = Layer.effect(
|
||||
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
||||
const id = WorkspaceID.ascending(input.id)
|
||||
const adapter = getAdapter(input.projectID, input.type)
|
||||
const config = yield* EffectBridge.fromPromise(() =>
|
||||
adapter.configure({
|
||||
...input,
|
||||
id,
|
||||
name: Slug.create(),
|
||||
directory: null,
|
||||
extra: input.extra ?? null,
|
||||
}),
|
||||
)
|
||||
const config = yield* adapterConfigure(adapter, {
|
||||
...input,
|
||||
id,
|
||||
name: Slug.create(),
|
||||
directory: null,
|
||||
extra: input.extra ?? null,
|
||||
})
|
||||
|
||||
const info: Info = {
|
||||
id,
|
||||
@@ -583,7 +615,7 @@ export const layer = Layer.effect(
|
||||
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||
}
|
||||
|
||||
yield* EffectBridge.fromPromise(() => adapter.create(config, env))
|
||||
yield* adapterCreate(adapter, config, env)
|
||||
yield* Effect.all(
|
||||
[
|
||||
waitEvent({
|
||||
@@ -622,8 +654,7 @@ export const layer = Layer.effect(
|
||||
if (current?.workspaceID) {
|
||||
const previous = yield* get(current.workspaceID)
|
||||
if (previous) {
|
||||
const adapter = getAdapter(previous.projectID, previous.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(previous))
|
||||
const target = yield* adapterTarget(previous)
|
||||
|
||||
if (target.type === "remote") {
|
||||
yield* syncHistory(previous, target.url, target.headers).pipe(
|
||||
@@ -701,8 +732,7 @@ export const layer = Layer.effect(
|
||||
workspaceID,
|
||||
})
|
||||
|
||||
const adapter = getAdapter(space.projectID, space.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||
const target = yield* adapterTarget(space)
|
||||
|
||||
if (target.type === "local") {
|
||||
yield* sync.run(Session.Event.Updated, {
|
||||
@@ -856,7 +886,7 @@ export const layer = Layer.effect(
|
||||
registeredAdapters(project.id),
|
||||
([type, adapter]) =>
|
||||
adapter.list
|
||||
? EffectBridge.fromPromise(() => Promise.resolve(adapter.list?.() ?? [])).pipe(
|
||||
? adapterList(adapter).pipe(
|
||||
Effect.catchCause((error) =>
|
||||
Effect.sync(() => {
|
||||
log.warn("workspace adapter list failed", { type, error })
|
||||
@@ -937,8 +967,7 @@ export const layer = Layer.effect(
|
||||
const info = fromRow(row)
|
||||
yield* Effect.catchCause(
|
||||
Effect.gen(function* () {
|
||||
const adapter = getAdapter(info.projectID, row.type)
|
||||
yield* EffectBridge.fromPromise(() => adapter.remove(info))
|
||||
yield* adapterRemove(info, row.type)
|
||||
}),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Context, Effect, Exit, Fiber } from "effect"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { Instance } from "@/project/instance"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||
import { attachWith } from "./run-service"
|
||||
|
||||
@@ -14,27 +11,14 @@ export interface Shape {
|
||||
readonly bind: <Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) => (...args: Args) => Result
|
||||
}
|
||||
|
||||
function restore<R>(instance: InstanceContext | undefined, workspace: WorkspaceID | undefined, fn: () => R): R {
|
||||
if (instance && workspace !== undefined) {
|
||||
return WorkspaceContext.restore(workspace, () => Instance.restore(instance, fn))
|
||||
}
|
||||
if (instance) return Instance.restore(instance, fn)
|
||||
function restoreWorkspace<R>(workspace: WorkspaceID | undefined, fn: () => R): R {
|
||||
if (workspace !== undefined) return WorkspaceContext.restore(workspace, fn)
|
||||
return fn()
|
||||
}
|
||||
|
||||
function captureSync() {
|
||||
const fiber = Fiber.getCurrent()
|
||||
const value = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
|
||||
const instance =
|
||||
value ??
|
||||
(() => {
|
||||
try {
|
||||
return Instance.current
|
||||
} catch (err) {
|
||||
if (!(err instanceof LocalContext.NotFound)) throw err
|
||||
}
|
||||
})()
|
||||
const instance = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
|
||||
const workspace =
|
||||
(fiber ? Context.getReferenceUnsafe(fiber.context, WorkspaceRef) : undefined) ?? WorkspaceContext.workspaceID
|
||||
return { instance, workspace }
|
||||
@@ -42,47 +26,43 @@ function captureSync() {
|
||||
|
||||
export const bind = <Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) => {
|
||||
const captured = captureSync()
|
||||
return (...args: Args) => restore(captured.instance, captured.workspace, () => fn(...args))
|
||||
return (...args: Args) =>
|
||||
restoreWorkspace(captured.workspace, () =>
|
||||
Effect.runSync(attachWith(Effect.sync(() => fn(...args)), captured)),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge from Effect into a Promise-returning JS callback while installing
|
||||
* legacy `Instance.context` and `WorkspaceContext` AsyncLocalStorage for
|
||||
* the duration of the callback. Effect's `InstanceRef`/`WorkspaceRef` do
|
||||
* not propagate across async/await boundaries inside `Effect.promise(() =>
|
||||
* async fn)` callbacks that re-enter Effect via `AppRuntime.runPromise`,
|
||||
* but Node's AsyncLocalStorage does. Use this whenever an Effect crosses
|
||||
* into JS that may itself spawn new Effect runtimes (workspace adapters,
|
||||
* legacy plugins, etc.).
|
||||
* Bridge from Effect into a Promise-returning JS callback while preserving
|
||||
* `WorkspaceContext` AsyncLocalStorage for callback code that still reads it.
|
||||
* `InstanceRef` is captured for effects run through the returned bridge APIs;
|
||||
* plain JS callbacks that need it should receive the ref explicitly.
|
||||
*
|
||||
* Mirrors `Effect.promise` but restores legacy ALS first.
|
||||
* Mirrors `Effect.promise` but restores workspace ALS first.
|
||||
*/
|
||||
export const fromPromise = <T>(fn: () => Promise<T> | T): Effect.Effect<T> =>
|
||||
Effect.gen(function* () {
|
||||
const instance = yield* InstanceRef
|
||||
const workspace = yield* WorkspaceRef
|
||||
return yield* Effect.promise(() => Promise.resolve(restore(instance, workspace, () => fn())))
|
||||
return yield* Effect.promise(() => Promise.resolve(restoreWorkspace(workspace, () => fn())))
|
||||
})
|
||||
|
||||
export function make(): Effect.Effect<Shape> {
|
||||
return Effect.gen(function* () {
|
||||
const ctx = yield* Effect.context()
|
||||
const value = yield* InstanceRef
|
||||
const captured = captureSync()
|
||||
const instance = value ?? captured.instance
|
||||
const instance = (yield* InstanceRef) ?? captured.instance
|
||||
const workspace = (yield* WorkspaceRef) ?? captured.workspace
|
||||
const attach = <A, E, R>(effect: Effect.Effect<A, E, R>) => attachWith(effect, { instance, workspace })
|
||||
const wrap = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
attach(effect).pipe(Effect.provide(ctx)) as Effect.Effect<A, E, never>
|
||||
attachWith(effect.pipe(Effect.provide(ctx)) as Effect.Effect<A, E, never>, { instance, workspace })
|
||||
|
||||
return {
|
||||
promise: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
restore(instance, workspace, () => Effect.runPromise(wrap(effect))),
|
||||
restoreWorkspace(workspace, () => Effect.runPromise(wrap(effect))),
|
||||
fork: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
restore(instance, workspace, () => Effect.runFork(wrap(effect))),
|
||||
restoreWorkspace(workspace, () => Effect.runFork(wrap(effect))),
|
||||
run: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
Effect.callback<A, E>((resume) => {
|
||||
restore(instance, workspace, () =>
|
||||
restoreWorkspace(workspace, () =>
|
||||
Effect.runPromiseExit(wrap(effect)).then((exit) =>
|
||||
resume(Exit.isSuccess(exit) ? Effect.succeed(exit.value) : Effect.failCause(exit.cause)),
|
||||
),
|
||||
@@ -91,7 +71,7 @@ export function make(): Effect.Effect<Shape> {
|
||||
bind:
|
||||
<Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) =>
|
||||
(...args: Args) =>
|
||||
restore(instance, workspace, () => fn(...args)),
|
||||
restoreWorkspace(workspace, () => Effect.runSync(wrap(Effect.sync(() => fn(...args))))),
|
||||
} satisfies Shape
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Effect, Fiber, ScopedCache, Scope, Context } from "effect"
|
||||
import { Effect, ScopedCache, Scope } from "effect"
|
||||
import * as EffectLogger from "@opencode-ai/core/effect/logger"
|
||||
import { Instance } from "@/project/instance"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||
import { registerDisposer } from "./instance-registry"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
@@ -14,20 +12,10 @@ export interface InstanceState<A, E = never, R = never> {
|
||||
readonly cache: ScopedCache.ScopedCache<string, A, E, R>
|
||||
}
|
||||
|
||||
export const bind = <F extends (...args: any[]) => any>(fn: F): F => {
|
||||
try {
|
||||
return Instance.bind(fn)
|
||||
} catch (err) {
|
||||
if (!(err instanceof LocalContext.NotFound)) throw err
|
||||
}
|
||||
const fiber = Fiber.getCurrent()
|
||||
const ctx = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
|
||||
if (!ctx) return fn
|
||||
return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F
|
||||
}
|
||||
|
||||
export const context = Effect.gen(function* () {
|
||||
return (yield* InstanceRef) ?? Instance.current
|
||||
const ctx = yield* InstanceRef
|
||||
if (!ctx) return yield* Effect.die(new Error("InstanceRef not provided"))
|
||||
return ctx
|
||||
})
|
||||
|
||||
export const workspaceID = Effect.gen(function* () {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Effect, Fiber, Layer, ManagedRuntime } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||
import * as Observability from "@opencode-ai/core/effect/observability"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
@@ -25,17 +23,9 @@ export function attachWith<A, E, R>(effect: Effect.Effect<A, E, R>, refs: Refs):
|
||||
|
||||
export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
|
||||
const workspace = WorkspaceContext.workspaceID
|
||||
const instance = (() => {
|
||||
try {
|
||||
return Instance.current
|
||||
} catch (err) {
|
||||
if (!(err instanceof LocalContext.NotFound)) throw err
|
||||
}
|
||||
})()
|
||||
if (instance && workspace !== undefined) return attachWith(effect, { instance, workspace })
|
||||
const fiber = Fiber.getCurrent()
|
||||
return attachWith(effect, {
|
||||
instance: instance ?? (fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined),
|
||||
instance: fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined,
|
||||
workspace: workspace ?? (fiber ? Context.getReferenceUnsafe(fiber.context, WorkspaceRef) : undefined),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Process } from "@/util/process"
|
||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import type * as LSPServer from "./server"
|
||||
import { withTimeout } from "../util/timeout"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { context, type InstanceContext } from "@/project/instance-context"
|
||||
|
||||
const DIAGNOSTICS_DEBOUNCE_MS = 150
|
||||
const DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS = 5_000
|
||||
@@ -25,6 +28,7 @@ const FILE_CHANGE_CHANGED = 2
|
||||
const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2
|
||||
|
||||
const log = Log.create({ service: "lsp.client" })
|
||||
const busRuntime = makeRuntime(Bus.Service, Bus.layer)
|
||||
|
||||
export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
|
||||
|
||||
@@ -134,9 +138,16 @@ function shouldSeedDiagnosticsOnFirstPush(serverID: string) {
|
||||
return serverID === "typescript"
|
||||
}
|
||||
|
||||
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string; directory: string }) {
|
||||
export async function create(input: {
|
||||
serverID: string
|
||||
server: LSPServer.Handle
|
||||
root: string
|
||||
directory: string
|
||||
instance?: InstanceContext
|
||||
}) {
|
||||
const logger = log.clone().tag("serverID", input.serverID)
|
||||
logger.info("starting client")
|
||||
const instance = input.instance ?? context.use()
|
||||
|
||||
const connection = createMessageConnection(
|
||||
new StreamMessageReader(input.server.process.stdout as any),
|
||||
@@ -162,7 +173,11 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
|
||||
dedupeDiagnostics([...(pushDiagnostics.get(filePath) ?? []), ...(pullDiagnostics.get(filePath) ?? [])])
|
||||
const updatePushDiagnostics = (filePath: string, next: Diagnostic[]) => {
|
||||
pushDiagnostics.set(filePath, next)
|
||||
Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
|
||||
void busRuntime.runPromise((svc) =>
|
||||
svc.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID }).pipe(
|
||||
Effect.provideService(InstanceRef, instance),
|
||||
),
|
||||
)
|
||||
}
|
||||
const updatePullDiagnostics = (filePath: string, next: Diagnostic[]) => {
|
||||
pullDiagnostics.set(filePath, next)
|
||||
@@ -510,10 +525,14 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
|
||||
}
|
||||
|
||||
timeoutTimer = setTimeout(() => finish(false), request.timeout)
|
||||
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
|
||||
if (event.properties.path !== request.path || event.properties.serverID !== input.serverID) return
|
||||
schedule()
|
||||
})
|
||||
unsub = busRuntime.runSync((svc) =>
|
||||
svc
|
||||
.subscribeCallback(Event.Diagnostics, (event) => {
|
||||
if (event.properties.path !== request.path || event.properties.serverID !== input.serverID) return
|
||||
schedule()
|
||||
})
|
||||
.pipe(Effect.provideService(InstanceRef, instance)),
|
||||
)
|
||||
schedule()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -237,6 +237,7 @@ export const layer = Layer.effect(
|
||||
server: handle,
|
||||
root,
|
||||
directory: ctx.directory,
|
||||
instance: ctx,
|
||||
}).catch(async (err) => {
|
||||
s.broken.add(key)
|
||||
await Process.stop(handle.process)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { context, type InstanceContext } from "./instance-context"
|
||||
|
||||
export type { InstanceContext } from "./instance-context"
|
||||
|
||||
export const Instance = {
|
||||
get current() {
|
||||
return context.use()
|
||||
},
|
||||
get directory() {
|
||||
return context.use().directory
|
||||
},
|
||||
get worktree() {
|
||||
return context.use().worktree
|
||||
},
|
||||
get project() {
|
||||
return context.use().project
|
||||
},
|
||||
|
||||
/**
|
||||
* Captures the current instance ALS context and returns a wrapper that
|
||||
* restores it when called. Use this for callbacks that fire outside the
|
||||
* instance async context (native addons, event emitters, timers, etc.).
|
||||
*/
|
||||
bind<F extends (...args: any[]) => any>(fn: F): F {
|
||||
const ctx = context.use()
|
||||
return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F
|
||||
},
|
||||
/**
|
||||
* Run a synchronous function within the given instance context ALS.
|
||||
* Use this to bridge from Effect (where InstanceRef carries context)
|
||||
* back to sync code that reads Instance.directory from ALS.
|
||||
*/
|
||||
restore<R>(ctx: InstanceContext, fn: () => R): R {
|
||||
return context.provide(ctx, fn)
|
||||
},
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { context } from "./instance-context"
|
||||
import type { InstanceContext } from "./instance-context"
|
||||
import { InstanceStore } from "./instance-store"
|
||||
|
||||
export async function provide<R>(input: { directory: string; fn: () => R }): Promise<R> {
|
||||
export async function provide<R>(input: { directory: string; fn: (ctx: InstanceContext) => R }): Promise<R> {
|
||||
const ctx = await AppRuntime.runPromise(
|
||||
InstanceStore.Service.use((store) => store.load({ directory: input.directory })),
|
||||
)
|
||||
return context.provide(ctx, () => input.fn())
|
||||
return input.fn(ctx)
|
||||
}
|
||||
|
||||
export * as WithInstance from "./with-instance"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
||||
@@ -26,9 +26,10 @@ function provideInstanceContext<E>(
|
||||
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
|
||||
return Effect.gen(function* () {
|
||||
const route = yield* WorkspaceRouteContext
|
||||
return yield* store.provide(
|
||||
{ directory: decode(route.directory) },
|
||||
effect.pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
|
||||
const ctx = yield* store.load({ directory: decode(route.directory) })
|
||||
return yield* effect.pipe(
|
||||
Effect.provideService(InstanceRef, ctx),
|
||||
Effect.provideService(WorkspaceRef, route.workspaceID),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -609,10 +609,10 @@ export const ShellTool = Tool.define(
|
||||
parameters: prompt.parameters,
|
||||
execute: (params: Parameters, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const executeInstance = yield* InstanceState.context
|
||||
const instanceCtx = yield* InstanceState.context
|
||||
const cwd = params.workdir
|
||||
? yield* resolvePath(params.workdir, executeInstance.directory, shell)
|
||||
: executeInstance.directory
|
||||
? yield* resolvePath(params.workdir, instanceCtx.directory, shell)
|
||||
: instanceCtx.directory
|
||||
if (params.timeout !== undefined && params.timeout < 0) {
|
||||
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
||||
}
|
||||
@@ -623,8 +623,8 @@ export const ShellTool = Tool.define(
|
||||
const tree = yield* Effect.acquireRelease(parse(params.command, ps), (tree) =>
|
||||
Effect.sync(() => tree.delete()),
|
||||
)
|
||||
const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance)
|
||||
if (!containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
|
||||
const scan = yield* collect(tree.rootNode, cwd, ps, shell, instanceCtx)
|
||||
if (!containsPath(cwd, instanceCtx)) scan.dirs.add(cwd)
|
||||
yield* ask(ctx, scan)
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user