From e6b68b401b454a3036f1aa76182baf397fb385f9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 9 Jun 2026 14:57:14 -0400 Subject: [PATCH] fix(opencode): restore effect error logging --- .../opencode/src/control-plane/workspace.ts | 14 ++++++++----- packages/opencode/src/mcp/index.ts | 20 ++++++++++++++++++- packages/opencode/src/plugin/index.ts | 17 ++++++++++++---- packages/opencode/src/session/llm.ts | 15 +++++++++++++- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index ed7b16353f47..f48c76b7f89b 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -586,7 +586,13 @@ export const layer = Layer.effect( if (target.type === "remote") { yield* syncHistory(previous, target.url, target.headers).pipe( - Effect.catch((error) => Effect.sync(() => {})), + Effect.catch((error) => + Effect.logWarning("session warp final source sync failed", { + workspaceID: previous.id, + sessionID: input.sessionID, + error: errorData(error), + }), + ), ) } else { yield* prompt.cancel(input.sessionID) @@ -739,9 +745,7 @@ export const layer = Layer.effect( ([type, adapter]) => WorkspaceAdapterRuntime.list(adapter).pipe( Effect.catchCause((error) => - Effect.sync(() => { - return [] - }), + Effect.logWarning("workspace adapter list failed", { type, error }).pipe(Effect.as([])), ), ), { concurrency: "unbounded" }, @@ -817,7 +821,7 @@ export const layer = Layer.effect( Effect.gen(function* () { yield* WorkspaceAdapterRuntime.remove(info) }), - () => Effect.sync(() => {}), + () => Effect.logError("adapter not available when removing workspace", { type: row.type }), ) yield* db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run().pipe(Effect.orDie) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 9a5a9e999391..7b9f4c4c101e 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -215,6 +215,12 @@ function fetchFromClient( return e }, }).pipe( + Effect.tapError((error) => + Effect.logWarning(`failed to get ${label}`, { + clientName, + error: error instanceof Error ? error.message : String(error), + }), + ), Effect.map((items) => { const out: Record = {} const sanitizedClient = sanitize(clientName) @@ -529,6 +535,7 @@ export const layer = Layer.effect( ([key, mcp]) => Effect.gen(function* () { if (!isMcpConfigured(mcp)) { + yield* Effect.logError("Ignoring MCP config entry without type", { key }) return } @@ -679,6 +686,7 @@ export const layer = Layer.effect( const listed = s.defs[clientName] if (!listed) { + yield* Effect.logWarning("missing cached tools for connected server", { clientName }) return } @@ -744,6 +752,7 @@ export const layer = Layer.effect( const s = yield* InstanceState.get(state) const client = s.clients[clientName] if (!client) { + yield* Effect.logWarning(`client not found for ${label}`, { clientName }) return undefined } return yield* Effect.tryPromise({ @@ -751,7 +760,16 @@ export const layer = Layer.effect( catch: (e: any) => { return e }, - }).pipe(Effect.orElseSucceed(() => undefined)) + }).pipe( + Effect.tapError((error) => + Effect.logError(`failed to ${label}`, { + clientName, + ...meta, + error: error instanceof Error ? error.message : String(error), + }), + ), + Effect.orElseSucceed(() => undefined), + ) }) const getPrompt = Effect.fn("MCP.getPrompt")(function* ( diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 326b37a36d5f..c4bff0fa6f3d 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -162,8 +162,13 @@ export const layer = Layer.effect( for (const plugin of flags.disableDefaultPlugins ? [] : internalPlugins(flags)) { const init = yield* Effect.tryPromise({ try: () => plugin(input), - catch: (err) => {}, - }).pipe(Effect.option) + catch: errorMessage, + }).pipe( + Effect.tapError((error) => + Effect.logError("failed to load internal plugin", { name: plugin.name, error }), + ), + Effect.option, + ) if (init._tag === "Some") hooks.push(init.value) } @@ -217,6 +222,7 @@ export const layer = Layer.effect( return message }, }).pipe( + Effect.tapError((error) => Effect.logError("failed to load plugin", { path: load.spec, error })), Effect.catch(() => { // TODO: make proper events for this // events.publish(Session.Event.Error, { @@ -256,8 +262,11 @@ export const layer = Layer.effect( (hook) => Effect.tryPromise({ try: () => Promise.resolve(hook.dispose?.()), - catch: (error) => {}, - }).pipe(Effect.ignore), + catch: errorMessage, + }).pipe( + Effect.tapError((error) => Effect.logError("plugin dispose hook failed", { error })), + Effect.ignore, + ), { discard: true }, ), ) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index cf284ce1ae6e..d8856e218f76 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -113,6 +113,7 @@ const live: Layer.Layer< // Wire up toolExecutor for DWS workflow models so that tool calls // from the workflow service are executed via opencode's tool system // and results sent back over the WebSocket. + const bridge = yield* EffectBridge.make() if (language instanceof GitLabWorkflowLanguageModel) { const workflowModel = language as GitLabWorkflowLanguageModel & { sessionID?: string @@ -149,7 +150,6 @@ const live: Layer.Layer< return !match || match.action !== "ask" }) - const bridge = yield* EffectBridge.make() const approvedToolsForSession = new Set() workflowModel.approvalHandler = bridge.bind(async (approvalTools) => { const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[] @@ -276,6 +276,19 @@ const live: Layer.Layer< return { type: "ai-sdk" as const, result: streamText({ + onError(error) { + bridge.fork( + Effect.logError("stream error", { + providerID: input.model.providerID, + modelID: input.model.id, + "session.id": input.sessionID, + small: (input.small ?? false).toString(), + agent: input.agent.name, + mode: input.agent.mode, + error, + }), + ) + }, // Copilot returns the authoritative billed amount only in provider-specific response fields. includeRawChunks: input.model.providerID.includes("github-copilot"), async experimental_repairToolCall(failed) {