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