diff --git a/packages/opencode/src/effect/bridge.ts b/packages/opencode/src/effect/bridge.ts index e987c901316f..820a7009b8cb 100644 --- a/packages/opencode/src/effect/bridge.ts +++ b/packages/opencode/src/effect/bridge.ts @@ -1,4 +1,4 @@ -import { Effect, Exit, Fiber } from "effect" +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" @@ -11,6 +11,7 @@ export interface Shape { readonly promise: (effect: Effect.Effect) => Promise readonly fork: (effect: Effect.Effect) => Fiber.Fiber readonly run: (effect: Effect.Effect) => Effect.Effect + readonly bind: (fn: (...args: Args) => Result) => (...args: Args) => Result } function restore(instance: InstanceContext | undefined, workspace: WorkspaceID | undefined, fn: () => R): R { @@ -22,6 +23,28 @@ function restore(instance: InstanceContext | undefined, workspace: WorkspaceI 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 workspace = (fiber ? Context.getReferenceUnsafe(fiber.context, WorkspaceRef) : undefined) ?? + WorkspaceContext.workspaceID + return { instance, workspace } +} + +export const bind = (fn: (...args: Args) => Result) => { + const captured = captureSync() + return (...args: Args) => restore(captured.instance, captured.workspace, () => fn(...args)) +} + /** * Bridge from Effect into a Promise-returning JS callback while installing * legacy `Instance.context` and `WorkspaceContext` AsyncLocalStorage for @@ -45,16 +68,9 @@ export function make(): Effect.Effect { return Effect.gen(function* () { const ctx = yield* Effect.context() const value = yield* InstanceRef - const instance = - value ?? - (() => { - try { - return Instance.current - } catch (err) { - if (!(err instanceof LocalContext.NotFound)) throw err - } - })() - const workspace = (yield* WorkspaceRef) ?? WorkspaceContext.workspaceID + const captured = captureSync() + const instance = value ?? captured.instance + const workspace = (yield* WorkspaceRef) ?? captured.workspace const attach = (effect: Effect.Effect) => attachWith(effect, { instance, workspace }) const wrap = (effect: Effect.Effect) => attach(effect).pipe(Effect.provide(ctx)) as Effect.Effect @@ -72,6 +88,10 @@ export function make(): Effect.Effect { ), ) }), + bind: + (fn: (...args: Args) => Result) => + (...args: Args) => + restore(instance, workspace, () => fn(...args)), } satisfies Shape }) } diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index d940c7c4228f..6c3a611d2864 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -6,6 +6,7 @@ import { readdir, realpath } from "fs/promises" import path from "path" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" +import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" import { Flag } from "@opencode-ai/core/flag/flag" import { Git } from "@/git" @@ -88,13 +89,13 @@ export const layer = Layer.effect( if (!w) return log.info("watcher backend", { directory: ctx.directory, platform: process.platform, backend }) - + const bridge = yield* EffectBridge.make() const subs: ParcelWatcher.AsyncSubscription[] = [] yield* Effect.addFinalizer(() => Effect.promise(() => Promise.allSettled(subs.map((sub) => sub.unsubscribe()))), ) - const cb: ParcelWatcher.SubscribeCallback = InstanceState.bind((err, evts) => { + const cb: ParcelWatcher.SubscribeCallback = bridge.bind((err, evts) => { if (err) return for (const evt of evts) { if (evt.type === "create") void Bus.publish(Event.Updated, { file: evt.path, event: "add" }) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 551787888852..0cf3a2398f9b 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -256,7 +256,7 @@ const live: Layer.Layer< const bridge = yield* EffectBridge.make() const approvedToolsForSession = new Set() - workflowModel.approvalHandler = InstanceState.bind(async (approvalTools) => { + workflowModel.approvalHandler = bridge.bind(async (approvalTools) => { const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[] // Auto-approve tools that were already approved in this session // (prevents infinite approval loops for server-side MCP tools) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 6cb819a6fd0f..06f1f84a9ae7 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -11,7 +11,7 @@ import path from "path" import { readFileSync, readdirSync, existsSync } from "fs" import { Flag } from "@opencode-ai/core/flag/flag" import { InstallationChannel } from "@opencode-ai/core/installation/version" -import { InstanceState } from "@/effect/instance-state" +import { EffectBridge } from "@/effect/bridge" import { init } from "#db" import { Effect, Schema } from "effect" @@ -167,7 +167,7 @@ export function use(callback: (trx: TxOrDb) => T): T { } export function effect(fn: () => any | Promise) { - const bound = InstanceState.bind(fn) + const bound = EffectBridge.bind(fn) try { ctx.use().effects.push(bound) } catch { @@ -188,7 +188,7 @@ export function transaction( } catch (err) { if (err instanceof LocalContext.NotFound) { const effects: (() => void | Promise)[] = [] - const txCallback = InstanceState.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) + const txCallback = EffectBridge.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) const result = Client().transaction(txCallback, { behavior: options?.behavior }) for (const effect of effects) effect() return result as NotPromise