From ff55a40749fdb749e8519cca65c0dcf8e330349e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 30 Apr 2026 23:20:20 -0400 Subject: [PATCH] core: remove @effect/language-service plugin and optimize hot path type performance - Removed @effect/language-service from both packages/core and packages/opencode tsconfig files and dependencies - Wrapped mergeDeep calls in config loading and LLM streaming to avoid expensive remeda conditional merge type instantiations in hot paths - Narrowed Drizzle migrate() overload signature to avoid expensive variance checks during database initialization These changes reduce TypeScript type-checking overhead and improve startup and runtime performance for config loading, LLM streaming, and database migrations. --- bun.lock | 3 --- packages/core/tsconfig.json | 9 +-------- packages/opencode/package.json | 2 -- packages/opencode/src/config/config.ts | 21 ++++++++++++--------- packages/opencode/src/session/llm.ts | 14 ++++++++------ packages/opencode/src/storage/db.ts | 9 ++++++++- packages/opencode/tsconfig.json | 9 +-------- 7 files changed, 30 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index 64c372661f..093bb880a4 100644 --- a/bun.lock +++ b/bun.lock @@ -456,7 +456,6 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", "@opencode-ai/script": "workspace:*", @@ -1069,8 +1068,6 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], - "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.57", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.57" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-gdjZPEP0QQg4qmI1vd+443kheeQZKytrjJIzCJncy6ZEpyk/SfrqeStLqLXdTRcms3IB0ls0vOV7KNq7YmBRVA=="], "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.57", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.57", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.57", "ioredis": "^5.7.0" } }, "sha512-la0xxPSAYOsY0d+uVxEBxok3jYB31iPQmIaZZRUj2SNWqcGGHJc6KorKtI8guqSLuv9FGZ255kBWXRbG6hMeeg=="], diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d7745d7554..fe5c4d217b 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -2,13 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/bun/tsconfig.json", "compilerOptions": { - "noUncheckedIndexedAccess": false, - "plugins": [ - { - "name": "@effect/language-service", - "transform": "@effect/language-service/transform", - "namespaceImportPackages": ["effect", "@effect/*"] - } - ] + "noUncheckedIndexedAccess": false } } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 425ddea77a..cf2e574f51 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -6,7 +6,6 @@ "license": "MIT", "private": true, "scripts": { - "prepare": "effect-language-service patch || true", "typecheck": "tsgo --noEmit", "test": "bun test --timeout 30000", "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", @@ -42,7 +41,6 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", "@opencode-ai/core": "workspace:*", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index c79e950e90..44841fe6fc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -3,7 +3,7 @@ import path from "path" import { pathToFileURL } from "url" import os from "os" import z from "zod" -import { mergeDeep, pipe } from "remeda" +import { mergeDeep } from "remeda" import { Global } from "@opencode-ai/core/global" import fsNode from "fs/promises" import { NamedError } from "@opencode-ai/core/util/error" @@ -47,8 +47,13 @@ import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "config" }) // Custom merge function that concatenates array fields instead of replacing them +// Keep remeda's deep conditional merge type out of hot config-loading paths; TS profiling showed it dominates here. +function mergeConfig(target: Info, source: Info): Info { + return mergeDeep(target, source) as Info +} + function mergeConfigConcatArrays(target: Info, source: Info): Info { - const merged = mergeDeep(target, source) + const merged = mergeConfig(target, source) if (target.instructions && source.instructions) { merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions])) } @@ -387,12 +392,10 @@ export const layer = Layer.effect( }) const loadGlobal = Effect.fnUntraced(function* () { - let result: Info = pipe( - {}, - mergeDeep(yield* loadFile(path.join(Global.Path.config, "config.json"))), - mergeDeep(yield* loadFile(path.join(Global.Path.config, "opencode.json"))), - mergeDeep(yield* loadFile(path.join(Global.Path.config, "opencode.jsonc"))), - ) + let result: Info = {} + result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "config.json"))) + result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.json"))) + result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.jsonc"))) const legacy = path.join(Global.Path.config, "config") if (existsSync(legacy)) { @@ -402,7 +405,7 @@ export const layer = Layer.effect( const { provider, model, ...rest } = mod.default if (provider && model) result.model = `${provider}/${model}` result["$schema"] = "https://opencode.ai/config.json" - result = mergeDeep(result, rest) + result = mergeConfig(result, rest) await fsNode.writeFile(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2)) await fsNode.unlink(legacy) }) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 58677debc0..69b0b27c3a 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -3,7 +3,7 @@ import * as Log from "@opencode-ai/core/util/log" import { Context, Effect, Layer, Record } from "effect" import * as Stream from "effect/Stream" import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" -import { mergeDeep, pipe } from "remeda" +import { mergeDeep } from "remeda" import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" import { ProviderTransform } from "@/provider/transform" import { Config } from "@/config/config" @@ -29,6 +29,10 @@ const log = Log.create({ service: "llm" }) export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX type Result = Awaited> +// Avoid re-instantiating remeda's deep merge types in this hot LLM path; the runtime behavior is still mergeDeep. +const mergeOptions = (target: Record, source: Record | undefined): Record => + mergeDeep(target, source ?? {}) as Record + export type StreamInput = { user: MessageV2.User sessionID: string @@ -134,11 +138,9 @@ const live: Layer.Layer< sessionID: input.sessionID, providerOptions: item.options, }) - const options: Record = pipe( - base, - mergeDeep(input.model.options), - mergeDeep(input.agent.options), - mergeDeep(variant), + const options = mergeOptions( + mergeOptions(mergeOptions(base, input.model.options), input.agent.options), + variant, ) if (isOpenaiOauth) { options.instructions = system.join("\n") diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 95bb568bd3..de4683b751 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -48,6 +48,13 @@ type Client = SQLiteBunDatabase type Journal = { sql: string; timestamp: number; name: string }[] +// Drizzle's migrate overloads trigger expensive variance checks here; narrow to the journal overload we actually use. +const migrateFromJournal = migrate as unknown as (db: SQLiteBunDatabase, entries: Journal) => void + +function applyMigrations(db: SQLiteBunDatabase, entries: Journal) { + migrateFromJournal(db, entries) +} + function time(tag: string) { const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) if (!match) return 0 @@ -108,7 +115,7 @@ export const Client = lazy(() => { item.sql = "select 1;" } } - migrate(db, entries) + applyMigrations(db, entries) } return db diff --git a/packages/opencode/tsconfig.json b/packages/opencode/tsconfig.json index 5cb51012ae..f09fca6878 100644 --- a/packages/opencode/tsconfig.json +++ b/packages/opencode/tsconfig.json @@ -12,13 +12,6 @@ "@/*": ["./src/*"], "@tui/*": ["./src/cli/cmd/tui/*"], "@test/*": ["./test/*"] - }, - "plugins": [ - { - "name": "@effect/language-service", - "transform": "@effect/language-service/transform", - "namespaceImportPackages": ["effect", "@effect/*"] - } - ] + } } }