From e24369eaf10412a9938d8159ba93be5c6453ed28 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 31 Mar 2026 16:01:23 -0400 Subject: [PATCH] fix: break installation cycle in database context binding --- packages/opencode/src/effect/instance-bind.ts | 14 +++++++++ packages/opencode/src/effect/instance-ref.ts | 6 ++++ .../opencode/src/effect/instance-state.ts | 18 +++-------- packages/opencode/src/effect/run-service.ts | 2 +- packages/opencode/src/installation/index.ts | 10 ++----- packages/opencode/src/installation/meta.ts | 7 +++++ packages/opencode/src/storage/db.ts | 16 +++++----- .../test/effect/instance-state.test.ts | 3 +- packages/opencode/test/fixture/fixture.ts | 5 ++-- packages/sdk/js/src/v2/gen/types.gen.ts | 30 +++++++++---------- 10 files changed, 62 insertions(+), 49 deletions(-) create mode 100644 packages/opencode/src/effect/instance-bind.ts create mode 100644 packages/opencode/src/effect/instance-ref.ts create mode 100644 packages/opencode/src/installation/meta.ts diff --git a/packages/opencode/src/effect/instance-bind.ts b/packages/opencode/src/effect/instance-bind.ts new file mode 100644 index 0000000000..b9f8402f1a --- /dev/null +++ b/packages/opencode/src/effect/instance-bind.ts @@ -0,0 +1,14 @@ +import { Fiber } from "effect" +import * as ServiceMap from "effect/ServiceMap" +import { Instance } from "@/project/instance" +import { InstanceRef } from "./instance-ref" + +export function bind any>(fn: F): F { + try { + return Instance.bind(fn) + } catch {} + const fiber = Fiber.getCurrent() + const ctx = fiber ? ServiceMap.getReferenceUnsafe(fiber.services, InstanceRef) : undefined + if (!ctx) return fn + return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F +} diff --git a/packages/opencode/src/effect/instance-ref.ts b/packages/opencode/src/effect/instance-ref.ts new file mode 100644 index 0000000000..d3939b2640 --- /dev/null +++ b/packages/opencode/src/effect/instance-ref.ts @@ -0,0 +1,6 @@ +import { ServiceMap } from "effect" +import type { InstanceContext } from "@/project/instance" + +export const InstanceRef = ServiceMap.Reference("~opencode/InstanceRef", { + defaultValue: () => undefined, +}) diff --git a/packages/opencode/src/effect/instance-state.ts b/packages/opencode/src/effect/instance-state.ts index a850b878d2..ec400a7714 100644 --- a/packages/opencode/src/effect/instance-state.ts +++ b/packages/opencode/src/effect/instance-state.ts @@ -1,28 +1,18 @@ -import { Effect, Fiber, ScopedCache, Scope, ServiceMap } from "effect" +import { Effect, ScopedCache, Scope } from "effect" import { Instance, type InstanceContext } from "@/project/instance" +import { bind as bindInstance } from "./instance-bind" +import { InstanceRef } from "./instance-ref" import { registerDisposer } from "./instance-registry" const TypeId = "~opencode/InstanceState" -export const InstanceRef = ServiceMap.Reference("~opencode/InstanceRef", { - defaultValue: () => undefined, -}) - export interface InstanceState { readonly [TypeId]: typeof TypeId readonly cache: ScopedCache.ScopedCache } export namespace InstanceState { - export const bind = any>(fn: F): F => { - try { - return Instance.bind(fn) - } catch {} - const fiber = Fiber.getCurrent() - const ctx = fiber ? ServiceMap.getReferenceUnsafe(fiber.services, InstanceRef) : undefined - if (!ctx) return fn - return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F - } + export const bind = bindInstance export const context = Effect.gen(function* () { const ref = yield* InstanceRef diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index 0e6abc9f1d..015ea23587 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -1,7 +1,7 @@ import { Effect, Layer, ManagedRuntime } from "effect" import * as ServiceMap from "effect/ServiceMap" import { Instance } from "@/project/instance" -import { InstanceRef } from "./instance-state" +import { InstanceRef } from "./instance-ref" export const memoMap = Layer.makeMemoMapUnsafe() diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 52c149c4fd..232fa14f54 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -9,11 +9,7 @@ import z from "zod" import { BusEvent } from "@/bus/bus-event" import { Flag } from "../flag/flag" import { Log } from "../util/log" - -declare global { - const OPENCODE_VERSION: string - const OPENCODE_CHANNEL: string -} +import { CHANNEL as channel, VERSION as version } from "./meta" import semver from "semver" @@ -60,8 +56,8 @@ export namespace Installation { }) export type Info = z.infer - export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local" - export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local" + export const VERSION = version + export const CHANNEL = channel export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}` export function isPreview() { diff --git a/packages/opencode/src/installation/meta.ts b/packages/opencode/src/installation/meta.ts new file mode 100644 index 0000000000..6a1315db27 --- /dev/null +++ b/packages/opencode/src/installation/meta.ts @@ -0,0 +1,7 @@ +declare global { + const OPENCODE_VERSION: string + const OPENCODE_CHANNEL: string +} + +export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local" +export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local" diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 8263c62b3c..917b3595e0 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -10,9 +10,9 @@ import { NamedError } from "@opencode-ai/util/error" import z from "zod" import path from "path" import { readFileSync, readdirSync, existsSync } from "fs" -import { Installation } from "../installation" import { Flag } from "../flag/flag" -import { InstanceState } from "@/effect/instance-state" +import { CHANNEL } from "../installation/meta" +import { bind } from "@/effect/instance-bind" import { iife } from "@/util/iife" import { init } from "#db" @@ -29,10 +29,9 @@ const log = Log.create({ service: "db" }) export namespace Database { export function getChannelPath() { - const channel = Installation.CHANNEL - if (["latest", "beta"].includes(channel) || Flag.OPENCODE_DISABLE_CHANNEL_DB) + if (["latest", "beta"].includes(CHANNEL) || Flag.OPENCODE_DISABLE_CHANNEL_DB) return path.join(Global.Path.data, "opencode.db") - const safe = channel.replace(/[^a-zA-Z0-9._-]/g, "-") + const safe = CHANNEL.replace(/[^a-zA-Z0-9._-]/g, "-") return path.join(Global.Path.data, `opencode-${safe}.db`) } @@ -143,10 +142,11 @@ export namespace Database { } export function effect(fn: () => any | Promise) { + const bound = bind(fn) try { - ctx.use().effects.push(InstanceState.bind(fn)) + ctx.use().effects.push(bound) } catch { - fn() + bound() } } @@ -163,7 +163,7 @@ export namespace Database { } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] - const txCallback = InstanceState.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) + const txCallback = 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 diff --git a/packages/opencode/test/effect/instance-state.test.ts b/packages/opencode/test/effect/instance-state.test.ts index b6b590c6af..914753312f 100644 --- a/packages/opencode/test/effect/instance-state.test.ts +++ b/packages/opencode/test/effect/instance-state.test.ts @@ -1,6 +1,7 @@ import { afterEach, expect, test } from "bun:test" import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, ServiceMap } from "effect" -import { InstanceRef, InstanceState } from "../../src/effect/instance-state" +import { InstanceState } from "../../src/effect/instance-state" +import { InstanceRef } from "../../src/effect/instance-ref" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index 99b3852715..54578f560b 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -7,7 +7,7 @@ import type * as PlatformError from "effect/PlatformError" import type * as Scope from "effect/Scope" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import type { Config } from "../../src/config/config" -import { InstanceRef } from "../../src/effect/instance-state" +import { InstanceRef } from "../../src/effect/instance-ref" import { Instance } from "../../src/project/instance" import { TestLLMServer } from "../lib/llm-server" @@ -115,8 +115,7 @@ export const provideInstance = Effect.promise(async () => Instance.provide({ directory, - fn: () => - Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, Instance.current))), + fn: () => Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, Instance.current))), }), ), ) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 318b8907a9..290c6fd5ec 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4,20 +4,6 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) } -export type EventInstallationUpdated = { - type: "installation.updated" - properties: { - version: string - } -} - -export type EventInstallationUpdateAvailable = { - type: "installation.update-available" - properties: { - version: string - } -} - export type Project = { id: string worktree: string @@ -47,6 +33,20 @@ export type EventProjectUpdated = { properties: Project } +export type EventInstallationUpdated = { + type: "installation.updated" + properties: { + version: string + } +} + +export type EventInstallationUpdateAvailable = { + type: "installation.update-available" + properties: { + version: string + } +} + export type EventServerInstanceDisposed = { type: "server.instance.disposed" properties: { @@ -964,9 +964,9 @@ export type EventSessionDeleted = { } export type Event = + | EventProjectUpdated | EventInstallationUpdated | EventInstallationUpdateAvailable - | EventProjectUpdated | EventServerInstanceDisposed | EventServerConnected | EventGlobalDisposed