From 451650b584e9eee3b8c7c488a7b3daafd6bdf487 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 30 Apr 2026 20:04:00 -0400 Subject: [PATCH] refactor(httpapi): preserve typed errors in session prompt handlers (#25181) --- packages/opencode/src/effect/bridge.ts | 11 +++++++- .../instance/httpapi/handlers/session.ts | 28 ++++++++----------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/effect/bridge.ts b/packages/opencode/src/effect/bridge.ts index 281cfa010c..3c310129f1 100644 --- a/packages/opencode/src/effect/bridge.ts +++ b/packages/opencode/src/effect/bridge.ts @@ -1,4 +1,4 @@ -import { Effect, Fiber } from "effect" +import { Effect, Exit, Fiber } from "effect" import { WorkspaceContext } from "@/control-plane/workspace-context" import { Instance, type InstanceContext } from "@/project/instance" import type { WorkspaceID } from "@/control-plane/schema" @@ -9,6 +9,7 @@ import { attachWith } from "./run-service" export interface Shape { readonly promise: (effect: Effect.Effect) => Promise readonly fork: (effect: Effect.Effect) => Fiber.Fiber + readonly run: (effect: Effect.Effect) => Effect.Effect } function restore(instance: InstanceContext | undefined, workspace: WorkspaceID | undefined, fn: () => R): R { @@ -43,6 +44,14 @@ export function make(): Effect.Effect { restore(instance, workspace, () => Effect.runPromise(wrap(effect))), fork: (effect: Effect.Effect) => restore(instance, workspace, () => Effect.runFork(wrap(effect))), + run: (effect: Effect.Effect) => + Effect.callback((resume) => { + restore(instance, workspace, () => + Effect.runPromiseExit(wrap(effect)).then((exit) => + resume(Exit.isSuccess(exit) ? Effect.succeed(exit.value) : Effect.failCause(exit.cause)), + ), + ) + }), } satisfies Shape }) } diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts index 39643e35ff..e08e09495a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts @@ -18,9 +18,8 @@ import { SessionSummary } from "@/session/summary" import { Todo } from "@/session/todo" import { MessageID, PartID, SessionID } from "@/session/schema" import { NotFoundError } from "@/storage/storage" -import * as Log from "@opencode-ai/core/util/log" import { NamedError } from "@opencode-ai/core/util/error" -import { Effect, Schema } from "effect" +import { Cause, Effect, Schema } from "effect" import * as Stream from "effect/Stream" import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi" @@ -40,8 +39,6 @@ import { UpdatePayload, } from "../groups/session" -const log = Log.create({ service: "server" }) - const mapNotFound = (self: Effect.Effect) => self.pipe( Effect.catchIf(NotFoundError.isInstance, () => Effect.fail(new HttpApiError.NotFound({}))), @@ -63,6 +60,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", const statusSvc = yield* SessionStatus.Service const todoSvc = yield* Todo.Service const summary = yield* SessionSummary.Service + const bus = yield* Bus.Service const list = Effect.fn("SessionHttpApi.list")(function* (ctx: { query: typeof ListQuery.Type }) { const instance = yield* InstanceState.context @@ -264,13 +262,11 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", const bridge = yield* EffectBridge.make() return HttpServerResponse.stream( Stream.fromEffect( - Effect.promise(() => - bridge.promise( - promptSvc.prompt({ - ...ctx.payload, - sessionID: ctx.params.sessionID, - }), - ), + bridge.run( + promptSvc.prompt({ + ...ctx.payload, + sessionID: ctx.params.sessionID, + }), ), ).pipe( Stream.map((message) => JSON.stringify(message)), @@ -288,12 +284,12 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", yield* Effect.sync(() => { bridge.fork( promptSvc.prompt({ ...ctx.payload, sessionID: ctx.params.sessionID }).pipe( - Effect.catchCause((error) => - Effect.sync(() => { - log.error("prompt_async failed", { sessionID: ctx.params.sessionID, error }) - void Bus.publish(Session.Event.Error, { + Effect.catchCause((cause) => + Effect.gen(function* () { + yield* Effect.logError("prompt_async failed", { sessionID: ctx.params.sessionID, cause }) + yield* bus.publish(Session.Event.Error, { sessionID: ctx.params.sessionID, - error: new NamedError.Unknown({ message: String(error) }).toObject(), + error: new NamedError.Unknown({ message: Cause.pretty(cause) }).toObject(), }) }), ),