From b0448bfdb98209ab4f2431c2488a99e214d73f50 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 31 May 2026 16:05:27 +0000 Subject: [PATCH 1/2] Use more idiomatic Effect runtime primitives Co-authored-by: Julius Marminge --- .../src/provider/Layers/OpenCodeAdapter.ts | 3 +- apps/server/src/provider/opencodeRuntime.ts | 41 ++++++++++--------- .../src/provider/providerStatusCache.test.ts | 11 ++++- .../src/provider/providerStatusCache.ts | 12 ++++-- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.ts index 34a67199125..eb66a3d55b6 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.ts @@ -37,6 +37,7 @@ import { import { type OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; import { buildOpenCodePermissionRules, + isOpenCodeRuntimeError, OpenCodeRuntime, OpenCodeRuntimeError, openCodeQuestionId, @@ -130,7 +131,7 @@ const toProcessError = (threadId: ThreadId, cause: unknown): ProviderAdapterProc new ProviderAdapterProcessError({ provider: PROVIDER, threadId, - detail: OpenCodeRuntimeError.is(cause) ? cause.detail : openCodeRuntimeErrorDetail(cause), + detail: isOpenCodeRuntimeError(cause) ? cause.detail : openCodeRuntimeErrorDetail(cause), cause, }); diff --git a/apps/server/src/provider/opencodeRuntime.ts b/apps/server/src/provider/opencodeRuntime.ts index 9c48e441032..da49e8537e3 100644 --- a/apps/server/src/provider/opencodeRuntime.ts +++ b/apps/server/src/provider/opencodeRuntime.ts @@ -13,14 +13,13 @@ import { } from "@opencode-ai/sdk/v2"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; -import * as Data from "effect/Data"; import * as Deferred from "effect/Deferred"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; -import * as P from "effect/Predicate"; import * as Ref from "effect/Ref"; import * as Result from "effect/Result"; import * as Scope from "effect/Scope"; @@ -35,7 +34,7 @@ const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJ const OPENCODE_EMPTY_CONFIG_CONTENT = "{}"; const OPENCODE_SERVER_READY_PREFIX = "opencode server listening"; -const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000; +const DEFAULT_OPENCODE_SERVER_TIMEOUT = Duration.seconds(5); const DEFAULT_HOSTNAME = "127.0.0.1"; export interface OpenCodeServerProcess { readonly url: string; @@ -48,15 +47,19 @@ export interface OpenCodeServerConnection { readonly external: boolean; } -const OPENCODE_RUNTIME_ERROR_TAG = "OpenCodeRuntimeError"; -export class OpenCodeRuntimeError extends Data.TaggedError(OPENCODE_RUNTIME_ERROR_TAG)<{ - readonly operation: string; - readonly cause?: unknown; - readonly detail: string; -}> { - static readonly is = (u: unknown): u is OpenCodeRuntimeError => - P.isTagged(u, OPENCODE_RUNTIME_ERROR_TAG); +export class OpenCodeRuntimeError extends Schema.TaggedErrorClass()( + "OpenCodeRuntimeError", + { + operation: Schema.String, + detail: Schema.String, + cause: Schema.optional(Schema.Defect), + }, +) { + override get message() { + return this.detail; + } } +export const isOpenCodeRuntimeError = Schema.is(OpenCodeRuntimeError); function encodeJsonStringForDiagnostics(input: unknown): string | undefined { const result = encodeUnknownJsonStringExit(input); @@ -64,7 +67,7 @@ function encodeJsonStringForDiagnostics(input: unknown): string | undefined { } export function openCodeRuntimeErrorDetail(cause: unknown): string { - if (OpenCodeRuntimeError.is(cause)) return cause.detail; + if (isOpenCodeRuntimeError(cause)) return cause.detail; if (cause instanceof Error && cause.message.trim().length > 0) return cause.message.trim(); if (cause && typeof cause === "object") { // SDK v2 throws { response, request, error? } shapes — extract what's useful @@ -117,7 +120,7 @@ export interface OpenCodeRuntimeShape { readonly environment?: NodeJS.ProcessEnv; readonly port?: number; readonly hostname?: string; - readonly timeoutMs?: number; + readonly timeout?: Duration.Input; }) => Effect.Effect; /** * Returns a handle to either an externally-managed OpenCode server (when @@ -130,7 +133,7 @@ export interface OpenCodeRuntimeShape { readonly environment?: NodeJS.ProcessEnv; readonly port?: number; readonly hostname?: string; - readonly timeoutMs?: number; + readonly timeout?: Duration.Input; }) => Effect.Effect; readonly runOpenCodeCommand: (input: { readonly binaryPath: string; @@ -268,7 +271,7 @@ function ensureRuntimeError( detail: string, cause: unknown, ): OpenCodeRuntimeError { - return OpenCodeRuntimeError.is(cause) + return isOpenCodeRuntimeError(cause) ? cause : new OpenCodeRuntimeError({ operation, detail, cause }); } @@ -332,7 +335,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () { }), ), )); - const timeoutMs = input.timeoutMs ?? DEFAULT_OPENCODE_SERVER_TIMEOUT_MS; + const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_OPENCODE_SERVER_TIMEOUT); const args = ["serve", `--hostname=${hostname}`, `--port=${port}`]; const child = yield* spawner @@ -431,7 +434,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () { ); const readyExit = yield* Effect.exit( - Deferred.await(readyDeferred).pipe(Effect.timeoutOption(timeoutMs)), + Deferred.await(readyDeferred).pipe(Effect.timeoutOption(timeout)), ); // Startup-time fibers are no longer needed once ready has resolved (either @@ -455,7 +458,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () { yield* Fiber.interrupt(exitFiber).pipe(Effect.ignore); return yield* new OpenCodeRuntimeError({ operation: "startOpenCodeServerProcess", - detail: `Timed out waiting for OpenCode server start after ${timeoutMs}ms.`, + detail: `Timed out waiting for OpenCode server start after ${Duration.format(timeout)}.`, }); } @@ -484,7 +487,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () { ...(input.environment !== undefined ? { environment: input.environment } : {}), ...(input.port !== undefined ? { port: input.port } : {}), ...(input.hostname !== undefined ? { hostname: input.hostname } : {}), - ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}), + ...(input.timeout !== undefined ? { timeout: input.timeout } : {}), }).pipe( Effect.map((server) => ({ url: server.url, diff --git a/apps/server/src/provider/providerStatusCache.test.ts b/apps/server/src/provider/providerStatusCache.test.ts index 64cb9ccd417..f1fbe44364f 100644 --- a/apps/server/src/provider/providerStatusCache.test.ts +++ b/apps/server/src/provider/providerStatusCache.test.ts @@ -50,6 +50,11 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => { const claudeProvider = makeProvider(CLAUDE_AGENT_DRIVER, { status: "warning", auth: { status: "unknown" }, + updateState: { + status: "queued", + startedAt: null, + finishedAt: null, + }, }); const openCodeProvider = makeProvider(OPENCODE_DRIVER, { status: "warning", @@ -81,9 +86,13 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => { provider: openCodeProvider, }); + const { updateState: _updateState, ...cacheableClaudeProvider } = claudeProvider; assert.deepStrictEqual(yield* readProviderStatusCache(codexPath), codexProvider); - assert.deepStrictEqual(yield* readProviderStatusCache(claudePath), claudeProvider); + assert.deepStrictEqual(yield* readProviderStatusCache(claudePath), cacheableClaudeProvider); assert.deepStrictEqual(yield* readProviderStatusCache(openCodePath), openCodeProvider); + + const claudeCacheContents = yield* fs.readFileString(claudePath); + assert.strictEqual(claudeCacheContents.includes("updateState"), false); }), ); diff --git a/apps/server/src/provider/providerStatusCache.ts b/apps/server/src/provider/providerStatusCache.ts index 0b9b365f360..6b0be685090 100644 --- a/apps/server/src/provider/providerStatusCache.ts +++ b/apps/server/src/provider/providerStatusCache.ts @@ -9,12 +9,15 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; +import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; import { writeFileStringAtomically } from "../atomicWrite.ts"; const decodeProviderStatusCache = Schema.decodeUnknownEffect( Schema.fromJsonString(ServerProviderSchema), ); +const ProviderStatusCacheJson = fromJsonStringPretty(ServerProviderSchema); +const encodeProviderStatusCache = Schema.encodeEffect(ProviderStatusCacheJson); const mergeProviderModels = ( fallbackModels: ReadonlyArray, @@ -146,8 +149,11 @@ export const writeProviderStatusCache = (input: { readonly provider: ServerProvider; }) => { const { updateState: _updateState, ...cacheableProvider } = input.provider; - return writeFileStringAtomically({ - filePath: input.filePath, - contents: `${JSON.stringify(cacheableProvider, null, 2)}\n`, + return Effect.gen(function* () { + const contents = yield* encodeProviderStatusCache(cacheableProvider); + yield* writeFileStringAtomically({ + filePath: input.filePath, + contents: `${contents}\n`, + }); }); }; From 5daf27ac72a98d5235c11119f556b70debc6c764 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 31 May 2026 16:06:33 +0000 Subject: [PATCH 2/2] Fix provider cache test fixture Co-authored-by: Julius Marminge --- apps/server/src/provider/providerStatusCache.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/src/provider/providerStatusCache.test.ts b/apps/server/src/provider/providerStatusCache.test.ts index f1fbe44364f..b7be653db8b 100644 --- a/apps/server/src/provider/providerStatusCache.test.ts +++ b/apps/server/src/provider/providerStatusCache.test.ts @@ -54,6 +54,8 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => { status: "queued", startedAt: null, finishedAt: null, + message: null, + output: null, }, }); const openCodeProvider = makeProvider(OPENCODE_DRIVER, {