diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts index b8c8a142be..7f7d8d1118 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts @@ -19,7 +19,7 @@ import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields, } from "../middleware/workspace-routing" -import { ApiNotFoundError } from "../errors" +import { ApiNotFoundError, SessionBusyError } from "../errors" import { described } from "./metadata" import { QueryBoolean } from "./query" @@ -354,7 +354,7 @@ export const SessionApi = HttpApi.make("session") query: WorkspaceRoutingQuery, payload: ShellPayload, success: described(MessageV2.WithParts, "Created message"), - error: [HttpApiError.BadRequest, ApiNotFoundError], + error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError], }).annotateMerge( OpenApi.annotations({ identifier: "session.shell", @@ -367,7 +367,7 @@ export const SessionApi = HttpApi.make("session") query: WorkspaceRoutingQuery, payload: RevertPayload, success: described(Session.Info, "Updated session"), - error: [HttpApiError.BadRequest, ApiNotFoundError], + error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError], }).annotateMerge( OpenApi.annotations({ identifier: "session.revert", @@ -380,7 +380,7 @@ export const SessionApi = HttpApi.make("session") params: { sessionID: SessionID }, query: WorkspaceRoutingQuery, success: described(Session.Info, "Updated session"), - error: [HttpApiError.BadRequest, ApiNotFoundError], + error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError], }).annotateMerge( OpenApi.annotations({ identifier: "session.unrevert", @@ -406,7 +406,7 @@ export const SessionApi = HttpApi.make("session") params: { sessionID: SessionID, messageID: MessageID }, query: WorkspaceRoutingQuery, success: described(Schema.Boolean, "Successfully deleted message"), - error: [HttpApiError.BadRequest, ApiNotFoundError], + error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError], }).annotateMerge( OpenApi.annotations({ identifier: "session.deleteMessage", diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts index d4ab0eb599..e297bdd0a8 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts @@ -1,7 +1,6 @@ import type { NotFoundError as StorageNotFoundError } from "@/storage/storage" import type { Session } from "@/session/session" import { Effect } from "effect" -import { HttpApiError } from "effect/unstable/httpapi" import * as ApiError from "../errors" export function mapStorageNotFound(self: Effect.Effect) { @@ -9,5 +8,14 @@ export function mapStorageNotFound(self: Effect.Effect(self: Effect.Effect) { - return self.pipe(Effect.catchTag("SessionBusyError", () => Effect.fail(new HttpApiError.BadRequest({})))) + return self.pipe( + Effect.catchTag("SessionBusyError", (error) => + Effect.fail( + new ApiError.SessionBusyError({ + sessionID: error.sessionID, + message: `Session is busy: ${error.sessionID}`, + }), + ), + ), + ) } diff --git a/packages/opencode/test/server/httpapi-public-openapi.test.ts b/packages/opencode/test/server/httpapi-public-openapi.test.ts index bd3b8c18ec..547beb9799 100644 --- a/packages/opencode/test/server/httpapi-public-openapi.test.ts +++ b/packages/opencode/test/server/httpapi-public-openapi.test.ts @@ -142,4 +142,19 @@ describe("PublicApi OpenAPI v2 errors", () => { ) } }) + + test("documents session busy errors", () => { + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + + for (const route of [ + ["post", "/session/{sessionID}/shell"], + ["post", "/session/{sessionID}/revert"], + ["post", "/session/{sessionID}/unrevert"], + ["delete", "/session/{sessionID}/message/{messageID}"], + ] as const) { + expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["409"]) ?? "")).toBe( + "SessionBusyError", + ) + } + }) }) diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index cdb50227ef..a13f986b4d 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect } from "bun:test" import { mkdir } from "node:fs/promises" import path from "node:path" -import { Effect, Layer } from "effect" +import { Cause, Effect, Exit, Layer } from "effect" import { Flag } from "@opencode-ai/core/flag/flag" import { registerAdapter } from "../../src/control-plane/adapters" import type { WorkspaceAdapter } from "../../src/control-plane/types" @@ -13,6 +13,7 @@ import { InstanceBootstrap as InstanceBootstrapService } from "../../src/project import { InstanceStore } from "../../src/project/instance-store" import { Project } from "../../src/project/project" import { Server } from "../../src/server/server" +import * as HttpSessionError from "../../src/server/routes/instance/httpapi/handlers/session-errors" import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session" import { Session } from "@/session/session" import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema" @@ -215,6 +216,22 @@ afterEach(async () => { }) describe("session HttpApi", () => { + it.effect("maps busy sessions to public session busy errors", () => + Effect.gen(function* () { + const sessionID = SessionID.descending() + const exit = yield* HttpSessionError.mapBusy(Effect.fail(new Session.BusyError({ sessionID }))).pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) { + expect(Cause.squash(exit.cause)).toMatchObject({ + _tag: "SessionBusyError", + sessionID, + message: `Session is busy: ${sessionID}`, + }) + } + }), + ) + it.instance( "returns declared not found errors for read routes", () =>