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.
This commit is contained in:
Dax Raad
2026-04-30 23:20:20 -04:00
parent 8b56d77ea1
commit ff55a40749
7 changed files with 30 additions and 37 deletions

View File

@@ -456,7 +456,6 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.4", "@babel/core": "7.28.4",
"@effect/language-service": "0.84.2",
"@octokit/webhooks-types": "7.6.1", "@octokit/webhooks-types": "7.6.1",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
@@ -1069,8 +1068,6 @@
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@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/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=="], "@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=="],

View File

@@ -2,13 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json", "extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"noUncheckedIndexedAccess": false, "noUncheckedIndexedAccess": false
"plugins": [
{
"name": "@effect/language-service",
"transform": "@effect/language-service/transform",
"namespaceImportPackages": ["effect", "@effect/*"]
}
]
} }
} }

View File

@@ -6,7 +6,6 @@
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"scripts": { "scripts": {
"prepare": "effect-language-service patch || true",
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
"test": "bun test --timeout 30000", "test": "bun test --timeout 30000",
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
@@ -42,7 +41,6 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.4", "@babel/core": "7.28.4",
"@effect/language-service": "0.84.2",
"@octokit/webhooks-types": "7.6.1", "@octokit/webhooks-types": "7.6.1",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",

View File

@@ -3,7 +3,7 @@ import path from "path"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import os from "os" import os from "os"
import z from "zod" import z from "zod"
import { mergeDeep, pipe } from "remeda" import { mergeDeep } from "remeda"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import fsNode from "fs/promises" import fsNode from "fs/promises"
import { NamedError } from "@opencode-ai/core/util/error" 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" }) const log = Log.create({ service: "config" })
// Custom merge function that concatenates array fields instead of replacing them // 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 { function mergeConfigConcatArrays(target: Info, source: Info): Info {
const merged = mergeDeep(target, source) const merged = mergeConfig(target, source)
if (target.instructions && source.instructions) { if (target.instructions && source.instructions) {
merged.instructions = Array.from(new Set([...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* () { const loadGlobal = Effect.fnUntraced(function* () {
let result: Info = pipe( let result: Info = {}
{}, result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "config.json")))
mergeDeep(yield* loadFile(path.join(Global.Path.config, "config.json"))), result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.json")))
mergeDeep(yield* loadFile(path.join(Global.Path.config, "opencode.json"))), result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.jsonc")))
mergeDeep(yield* loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
)
const legacy = path.join(Global.Path.config, "config") const legacy = path.join(Global.Path.config, "config")
if (existsSync(legacy)) { if (existsSync(legacy)) {
@@ -402,7 +405,7 @@ export const layer = Layer.effect(
const { provider, model, ...rest } = mod.default const { provider, model, ...rest } = mod.default
if (provider && model) result.model = `${provider}/${model}` if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json" 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.writeFile(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await fsNode.unlink(legacy) await fsNode.unlink(legacy)
}) })

View File

@@ -3,7 +3,7 @@ import * as Log from "@opencode-ai/core/util/log"
import { Context, Effect, Layer, Record } from "effect" import { Context, Effect, Layer, Record } from "effect"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" 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 { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
import { ProviderTransform } from "@/provider/transform" import { ProviderTransform } from "@/provider/transform"
import { Config } from "@/config/config" import { Config } from "@/config/config"
@@ -29,6 +29,10 @@ const log = Log.create({ service: "llm" })
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
type Result = Awaited<ReturnType<typeof streamText>> type Result = Awaited<ReturnType<typeof streamText>>
// Avoid re-instantiating remeda's deep merge types in this hot LLM path; the runtime behavior is still mergeDeep.
const mergeOptions = (target: Record<string, any>, source: Record<string, any> | undefined): Record<string, any> =>
mergeDeep(target, source ?? {}) as Record<string, any>
export type StreamInput = { export type StreamInput = {
user: MessageV2.User user: MessageV2.User
sessionID: string sessionID: string
@@ -134,11 +138,9 @@ const live: Layer.Layer<
sessionID: input.sessionID, sessionID: input.sessionID,
providerOptions: item.options, providerOptions: item.options,
}) })
const options: Record<string, any> = pipe( const options = mergeOptions(
base, mergeOptions(mergeOptions(base, input.model.options), input.agent.options),
mergeDeep(input.model.options), variant,
mergeDeep(input.agent.options),
mergeDeep(variant),
) )
if (isOpenaiOauth) { if (isOpenaiOauth) {
options.instructions = system.join("\n") options.instructions = system.join("\n")

View File

@@ -48,6 +48,13 @@ type Client = SQLiteBunDatabase
type Journal = { sql: string; timestamp: number; name: string }[] 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) { function time(tag: string) {
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag)
if (!match) return 0 if (!match) return 0
@@ -108,7 +115,7 @@ export const Client = lazy(() => {
item.sql = "select 1;" item.sql = "select 1;"
} }
} }
migrate(db, entries) applyMigrations(db, entries)
} }
return db return db

View File

@@ -12,13 +12,6 @@
"@/*": ["./src/*"], "@/*": ["./src/*"],
"@tui/*": ["./src/cli/cmd/tui/*"], "@tui/*": ["./src/cli/cmd/tui/*"],
"@test/*": ["./test/*"] "@test/*": ["./test/*"]
}, }
"plugins": [
{
"name": "@effect/language-service",
"transform": "@effect/language-service/transform",
"namespaceImportPackages": ["effect", "@effect/*"]
}
]
} }
} }