refactor(core): replace legacy logger with Effect logging (#31310)

This commit is contained in:
Dax
2026-06-08 15:41:56 -04:00
committed by GitHub
parent 0a7cb20e66
commit c06ad7c881
152 changed files with 698 additions and 2243 deletions
+2 -9
View File
@@ -1,11 +1,9 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import type { Model } from "@opencode-ai/sdk/v2"
import * as Log from "@opencode-ai/core/util/log"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { createServer } from "http"
import open from "open"
const log = Log.create({ service: "plugin.digitalocean" })
const DO_OAUTH_CLIENT_ID = "b1a6c5158156caac821fd1b30253ca8acb52454a48fa744420e41889cb589f82"
const DO_AUTHORIZE_URL = "https://cloud.digitalocean.com/v1/oauth/authorize"
@@ -188,7 +186,6 @@ async function startOAuthServer(): Promise<void> {
await new Promise<void>((resolve, reject) => {
oauthServer!.listen(OAUTH_PORT, () => {
log.info("digitalocean oauth server started", { port: OAUTH_PORT })
resolve()
})
oauthServer!.on("error", reject)
@@ -197,7 +194,7 @@ async function startOAuthServer(): Promise<void> {
function stopOAuthServer() {
if (!oauthServer) return
oauthServer.close(() => log.info("digitalocean oauth server stopped"))
oauthServer.close()
oauthServer = undefined
}
@@ -315,11 +312,9 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
path: { id: "digitalocean" },
body: { type: "api", key: ctx.auth.key, metadata: updated },
})
.catch((err) => log.warn("failed to persist refreshed routers", { error: err }))
.catch(() => {})
} else if (result.status === 401 || result.status === 403) {
log.warn("digitalocean oauth bearer rejected; using cached routers", { status: result.status })
} else if (result.status !== 0) {
log.warn("digitalocean router refresh failed", { status: result.status })
}
}
@@ -355,7 +350,6 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
const routerResult = await listRouters(tokens.access_token)
const routers = routerResult.ok ? routerResult.routers : []
if (!routerResult.ok) {
log.warn("digitalocean initial router fetch failed", { status: routerResult.status })
}
return {
type: "success" as const,
@@ -372,7 +366,6 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
},
}
} catch (err) {
log.error("digitalocean oauth callback failed", { error: err })
return { type: "failed" as const }
} finally {
stopOAuthServer()
@@ -2,12 +2,10 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import type { Model } from "@opencode-ai/sdk/v2"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { iife } from "@/util/iife"
import * as Log from "@opencode-ai/core/util/log"
import { setTimeout as sleep } from "node:timers/promises"
import { CopilotModels } from "./models"
import { MessageV2 } from "@/session/message-v2"
const log = Log.create({ service: "plugin.copilot" })
const CLIENT_ID = "Ov23li8tweQw6odWQebz"
const API_VERSION = "2026-06-01"
@@ -87,7 +85,6 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
})
.catch((error) => {
models = {}
log.error("failed to fetch copilot models", { error })
return Object.fromEntries(
Object.entries(provider.models).map(([id, model]) => [id, fix(model, base(auth.enterpriseUrl))]),
)
+5 -17
View File
@@ -6,7 +6,6 @@ import type {
WorkspaceAdapter as PluginWorkspaceAdapter,
} from "@opencode-ai/plugin"
import { Config } from "@/config/config"
import * as Log from "@opencode-ai/core/util/log"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { ServerAuth } from "@/server/auth"
import { CodexAuthPlugin } from "./openai/codex"
@@ -31,7 +30,6 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { InstallationChannel } from "@opencode-ai/core/installation/version"
const log = Log.create({ service: "plugin" })
type State = {
hooks: Hooks[]
@@ -163,11 +161,9 @@ export const layer = Layer.effect(
}
for (const plugin of flags.disableDefaultPlugins ? [] : internalPlugins(flags)) {
log.info("loading internal plugin", { name: plugin.name })
const init = yield* Effect.tryPromise({
try: () => plugin(input),
catch: (err) => {
log.error("failed to load internal plugin", { name: plugin.name, error: err })
},
}).pipe(Effect.option)
if (init._tag === "Some") hooks.push(init.value)
@@ -175,7 +171,6 @@ export const layer = Layer.effect(
const plugins = flags.pure ? [] : (cfg.plugin_origins ?? [])
if (flags.pure && cfg.plugin_origins?.length) {
log.info("skipping external plugins in pure mode", { count: cfg.plugin_origins.length })
}
if (plugins.length) yield* config.waitForDependencies()
@@ -185,10 +180,8 @@ export const layer = Layer.effect(
kind: "server",
report: {
start(candidate) {
log.info("loading plugin", { path: candidate.plan.spec })
},
missing(candidate, _retry, message) {
log.warn("plugin has no server entrypoint", { path: candidate.plan.spec, message })
},
error(candidate, _retry, stage, error, resolved) {
const spec = candidate.plan.spec
@@ -197,24 +190,20 @@ export const layer = Layer.effect(
if (stage === "install") {
const parsed = parsePluginSpecifier(spec)
log.error("failed to install plugin", { pkg: parsed.pkg, version: parsed.version, error: message })
publishPluginError(`Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`)
return
}
if (stage === "compatibility") {
log.warn("plugin incompatible", { path: spec, error: message })
publishPluginError(`Plugin ${spec} skipped: ${message}`)
return
}
if (stage === "entry") {
log.error("failed to resolve plugin server entry", { path: spec, error: message })
publishPluginError(`Failed to load plugin ${spec}: ${message}`)
return
}
log.error("failed to load plugin", { path: spec, target: resolved?.entry, error: message })
publishPluginError(`Failed to load plugin ${spec}: ${message}`)
},
},
@@ -229,7 +218,6 @@ export const layer = Layer.effect(
try: () => applyPlugin(load, input, hooks),
catch: (err) => {
const message = errorMessage(err)
log.error("failed to load plugin", { path: load.spec, error: message })
return message
},
}).pipe(
@@ -249,10 +237,11 @@ export const layer = Layer.effect(
for (const hook of hooks) {
yield* Effect.tryPromise({
try: () => Promise.resolve((hook as any).config?.(cfg)),
catch: (err) => {
log.error("plugin config hook failed", { error: err })
},
}).pipe(Effect.ignore)
catch: errorMessage,
}).pipe(
Effect.tapError((error) => Effect.logError("plugin config hook failed", { error })),
Effect.ignore,
)
}
const unsubscribe = yield* events.listen((event) => {
@@ -272,7 +261,6 @@ export const layer = Layer.effect(
Effect.tryPromise({
try: () => Promise.resolve(hook.dispose?.()),
catch: (error) => {
log.error("plugin dispose hook failed", { error })
},
}).pipe(Effect.ignore),
{ discard: true },
@@ -1,5 +1,4 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import * as Log from "@opencode-ai/core/util/log"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { OAUTH_DUMMY_KEY } from "../../auth"
import os from "os"
@@ -7,7 +6,6 @@ import { setTimeout as sleep } from "node:timers/promises"
import { createServer } from "http"
import { OpenAIWebSocketPool } from "./ws-pool"
const log = Log.create({ service: "plugin.codex" })
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
const ISSUER = "https://auth.openai.com"
@@ -306,7 +304,6 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
await new Promise<void>((resolve, reject) => {
oauthServer!.listen(OAUTH_PORT, () => {
log.info("codex oauth server started", { port: OAUTH_PORT })
resolve()
})
oauthServer!.on("error", reject)
@@ -318,7 +315,6 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
function stopOAuthServer() {
if (oauthServer) {
oauthServer.close(() => {
log.info("codex oauth server stopped")
})
oauthServer = undefined
}
@@ -442,7 +438,6 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
if (!currentAuth.access || currentAuth.expires < Date.now()) {
if (!refreshPromise) {
log.info("refreshing codex access token")
refreshPromise = refreshAccessToken(currentAuth.refresh, issuer)
.then(async (tokens) => {
const accountId = extractAccountId(tokens) || authWithAccount.accountId
@@ -1,12 +1,10 @@
import WebSocket from "ws"
import * as Log from "@opencode-ai/core/util/log"
import { ProviderError } from "@/provider/error"
import { isRecord } from "@/util/record"
import { OpenAIWebSocket } from "./ws"
export const TITLE_HEADER = "x-opencode-title"
const log = Log.create({ service: "plugin.openai.ws" })
export interface CreateWebSocketFetchOptions {
httpFetch?: typeof globalThis.fetch
@@ -63,13 +61,11 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
})()
if (!body?.stream) return httpFetch(input, httpInit)
if (internalHeaders[TITLE_HEADER] === "true") {
log.debug("http fallback", { reason: "title" })
return httpFetch(input, httpInit)
}
const sessionID = internalHeaders["x-session-affinity"] ?? internalHeaders["session-id"]
if (!sessionID) {
log.debug("http fallback", { reason: "missing_session" })
return httpFetch(input, httpInit)
}
const key = `${sessionID}:conversation`
@@ -78,11 +74,9 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
pool.set(key, entry)
if (entry.fallback) {
log.debug("http fallback", { key, reason: "fallback_active" })
return httpFetch(input, httpInit)
}
if (entry.busy) {
log.debug("http fallback", { key, reason: "busy" })
return httpFetch(input, httpInit)
}
@@ -114,12 +108,10 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
entry.lastUsedAt = Date.now()
entry.streamFailures = 0
if (event.type !== "response.completed" && event.type !== "response.done") {
log.warn("websocket terminal failure", { key, type: event.type })
invalidate(entry)
}
},
onConnectionInvalid: (error) => {
log.warn("websocket invalidated", { key, error: error.message })
entry.busy = false
entry.lastUsedAt = Date.now()
if (!entry.fallback) recordStreamFailure(entry)
@@ -127,7 +119,6 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
resolveFirstEvent(false)
},
onAbort: (error) => {
log.debug("websocket aborted", { key })
entry.busy = false
entry.lastUsedAt = Date.now()
entry.streamFailures = 0
@@ -137,7 +128,6 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
onRetryableTerminal: async (event) => {
const error = connectionLimitError(event)
if (!error) return undefined
log.warn("websocket connection limit reached", { key })
throw error
},
})
@@ -150,7 +140,6 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
})
}
if (!entry.fallback) return response
log.debug("http fallback", { key, reason: "websocket_retries_exhausted" })
return httpFetch(input, httpInit)
} catch (error) {
entry.busy = false
@@ -162,11 +151,6 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
}
recordStreamFailure(entry)
log.warn("websocket setup failed", {
key,
error: error instanceof Error ? error.message : String(error),
fallback: entry.fallback ? "http" : undefined,
})
invalidate(entry)
if (entry.fallback) return httpFetch(input, httpInit)
return failedResponse(
@@ -189,14 +173,12 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
if (entry.busy) continue
if (entry.fallback) continue
if (now - entry.lastUsedAt < idleTimeout) continue
log.debug("websocket idle prune", { key })
invalidate(entry)
pool.delete(key)
}
}
function close() {
log.debug("websocket pool close", { count: pool.size })
clearInterval(pruneTimer)
for (const entry of pool.values()) invalidate(entry)
pool.clear()
@@ -206,7 +188,6 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
const key = `${sessionID}:conversation`
const entry = pool.get(key)
if (!entry) return
log.debug("websocket pool remove", { key })
invalidate(entry)
pool.delete(key)
}
+8 -52
View File
@@ -14,7 +14,6 @@ import {
import path from "path"
import { fileURLToPath } from "url"
import { TuiConfig } from "@/config/tui"
import * as Log from "@opencode-ai/core/util/log"
import { errorData, errorMessage } from "@opencode-ai/tui/util/error"
import { isRecord } from "@opencode-ai/tui/util/record"
import { resolveHostAttentionSoundPaths } from "@/config/tui-host-attention"
@@ -119,7 +118,6 @@ type RuntimeState = {
dispose_timeout_ms: number
}
const log = Log.create({ service: "tui.plugin" })
const DISPOSE_TIMEOUT_MS = 5000
const KV_KEY = "plugin_enabled"
const EMPTY_TUI: TuiPluginModule = {
@@ -128,19 +126,16 @@ const EMPTY_TUI: TuiPluginModule = {
function fail(message: string, data: Record<string, unknown>) {
if (!("error" in data)) {
log.error(message, data)
console.error(`[tui.plugin] ${message}`, data)
return
}
const text = `${message}: ${errorMessage(data.error)}`
const next = { ...data, error: errorData(data.error) }
log.error(text, next)
console.error(`[tui.plugin] ${text}`, next)
}
function warn(message: string, data: Record<string, unknown>) {
log.warn(message, data)
console.warn(`[tui.plugin] ${message}`, data)
}
@@ -275,15 +270,7 @@ function createThemeInstaller(
await Flock.withLock(`tui-theme:${dest}`, async () => {
const save = async () => {
plugin.themes[name] = info
await PluginMeta.setTheme(plugin.id, name, info).catch((error) => {
log.warn("failed to track tui plugin theme", {
path: spec,
id: plugin.id,
theme: src,
dest,
error,
})
})
await PluginMeta.setTheme(plugin.id, name, info).catch(() => {})
}
const exists = hasTheme(name)
@@ -298,37 +285,26 @@ function createThemeInstaller(
if (prev?.dest === dest && prev.mtime === mtime && prev.size === size) return
}
const text = await Filesystem.readText(src).catch((error) => {
log.warn("failed to read tui plugin theme", { path: spec, theme: src, error })
return
})
const text = await Filesystem.readText(src).catch(() => undefined)
if (text === undefined) return
const fail = Symbol()
const data = await Promise.resolve(text)
.then((x) => JSON.parse(x))
.catch((error) => {
log.warn("failed to parse tui plugin theme", { path: spec, theme: src, error })
return fail
})
.catch(() => fail)
if (data === fail) return
if (!isTheme(data)) {
log.warn("invalid tui plugin theme", { path: spec, theme: src })
return
}
if (exists || !(await Filesystem.exists(dest))) {
await Filesystem.write(dest, text).catch((error) => {
log.warn("failed to persist tui plugin theme", { path: spec, theme: src, dest, error })
})
await Filesystem.write(dest, text).catch(() => {})
}
upsertTheme(name, data)
await save()
}).catch((error) => {
log.warn("failed to lock tui plugin theme install", { path: spec, theme: src, dest, error })
})
}).catch(() => {})
}
}
@@ -701,9 +677,7 @@ async function resolveExternalPlugins(list: ConfigPlugin.Origin[], wait: () => P
items: list,
kind: "tui",
wait: async () => {
await wait().catch((error) => {
log.warn("failed waiting for tui plugin dependencies", { error })
})
await wait().catch(() => {})
},
finish: async (loaded, origin, retry) => {
const mod = await Promise.resolve()
@@ -774,9 +748,7 @@ async function resolveExternalPlugins(list: ConfigPlugin.Origin[], wait: () => P
}
},
report: {
start(candidate, retry) {
log.info("loading tui plugin", { path: candidate.plan.spec, retry })
},
start() {},
missing(candidate, retry, message) {
warn("tui plugin has no entrypoint", { path: candidate.plan.spec, retry, message })
},
@@ -809,10 +781,7 @@ async function addExternalPluginEntries(state: RuntimeState, ready: PluginLoad[]
target: item.target,
id: item.id,
})),
).catch((error) => {
log.warn("failed to track tui plugins", { error })
return undefined
})
).catch(() => undefined)
const plugins: PluginEntry[] = []
let ok = true
@@ -820,17 +789,6 @@ async function addExternalPluginEntries(state: RuntimeState, ready: PluginLoad[]
const entry = ready[i]
if (!entry) continue
const hit = meta?.[i]
if (hit && hit.state !== "same") {
log.info("tui plugin metadata updated", {
path: entry.spec,
retry: entry.retry,
state: hit.state,
source: hit.entry.source,
version: hit.entry.version,
modified: hit.entry.modified,
})
}
const info = createMeta(entry.source, entry.spec, entry.target, hit, entry.id)
const themes = hit?.entry.themes ? { ...hit.entry.themes } : {}
const plugin: PluginEntry = {
@@ -1129,11 +1087,9 @@ async function load(input: {
const pluginOrigins = config.plugin_origins ?? (await TuiConfig.pluginOrigins())
const records = Flag.OPENCODE_PURE ? [] : pluginOrigins
if (Flag.OPENCODE_PURE && pluginOrigins.length) {
log.info("skipping external tui plugins in pure mode", { count: pluginOrigins.length })
}
for (const item of internalTuiPlugins(flags)) {
log.info("loading internal tui plugin", { id: item.id })
const entry = loadInternalPlugin(item)
const meta = createMeta(entry.source, entry.spec, entry.target, undefined, entry.id)
addPluginEntry(next, {
+2 -9
View File
@@ -1,10 +1,8 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import * as Log from "@opencode-ai/core/util/log"
import { OAUTH_DUMMY_KEY } from "../auth"
import { createServer } from "http"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
const log = Log.create({ service: "plugin.xai" })
// Public Grok-CLI OAuth client. xAI's auth server rejects loopback OAuth from
// non-allowlisted clients, so we reuse the Grok-CLI client_id that xAI ships
@@ -510,8 +508,6 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
// behavior and crash the entire opencode process. Matches the silent-
// swallow behavior the Codex plugin gets from its permanent
// `oauthServer!.on("error", reject)`.
server.on("error", (err) => log.warn("xai oauth server error", { error: err }))
log.info("xai oauth server started", { host: OAUTH_HOST, port: OAUTH_PORT })
resolve()
})
oauthServer = server
@@ -522,7 +518,7 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
function stopOAuthServer() {
if (oauthServer) {
oauthServer.close(() => log.info("xai oauth server stopped"))
oauthServer.close()
oauthServer = undefined
}
}
@@ -607,7 +603,6 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
if (expiresSoon) {
if (!refreshPromise) {
const refreshToken = currentAuth.refresh
log.info("refreshing xai access token")
refreshPromise = refreshAccessToken(refreshToken, options)
.then(async (tokens) => {
const refreshedExpires = Date.now() + (tokens.expires_in ?? 3600) * 1000
@@ -627,7 +622,7 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
expires: refreshedExpires,
},
})
.catch((err) => log.warn("failed to persist refreshed xai tokens", { error: err }))
.catch(() => {})
return { access: tokens.access_token, refresh: refreshedRefresh, expires: refreshedExpires }
})
.finally(() => {
@@ -688,7 +683,6 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
}
} catch (err) {
log.error("xai oauth callback failed", { error: err })
return { type: "failed" as const }
} finally {
stopOAuthServer()
@@ -725,7 +719,6 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
}
} catch (err) {
log.error("xai device code callback failed", { error: err })
return { type: "failed" as const }
}
},